Repository: guchengwuyue/supplierShop Branch: master Commit: 020cd5ad4000 Files: 3200 Total size: 31.5 MB Directory structure: gitextract_s5ijlxg7/ ├── .gitignore ├── LICENSE ├── README.md ├── yshop-drink-boot3/ │ ├── .gitignore │ ├── README.md │ ├── lombok.config │ ├── pom.xml │ ├── script/ │ │ ├── docker/ │ │ │ ├── Docker-HOWTO.md │ │ │ ├── docker-compose.yml │ │ │ └── docker.env │ │ └── shell/ │ │ └── deploy.sh │ ├── sql/ │ │ └── yixiang-drink-open.sql │ ├── yshop-dependencies/ │ │ └── pom.xml │ ├── yshop-framework/ │ │ ├── pom.xml │ │ ├── yshop-common/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ └── java/ │ │ │ │ └── co/ │ │ │ │ └── yixiang/ │ │ │ │ └── yshop/ │ │ │ │ └── framework/ │ │ │ │ └── common/ │ │ │ │ ├── constant/ │ │ │ │ │ ├── ShopConstants.java │ │ │ │ │ └── SystemConfigConstants.java │ │ │ │ ├── core/ │ │ │ │ │ ├── IntArrayValuable.java │ │ │ │ │ └── KeyValue.java │ │ │ │ ├── enums/ │ │ │ │ │ ├── CommonStatusEnum.java │ │ │ │ │ ├── DateIntervalEnum.java │ │ │ │ │ ├── DocumentEnum.java │ │ │ │ │ ├── OrderInfoEnum.java │ │ │ │ │ ├── OrderTypeEnum.java │ │ │ │ │ ├── PayIdEnum.java │ │ │ │ │ ├── ShopCommonEnum.java │ │ │ │ │ ├── TerminalEnum.java │ │ │ │ │ ├── UserTypeEnum.java │ │ │ │ │ └── WebFilterOrderEnum.java │ │ │ │ ├── exception/ │ │ │ │ │ ├── ErrorCode.java │ │ │ │ │ ├── ServerException.java │ │ │ │ │ ├── ServiceException.java │ │ │ │ │ ├── enums/ │ │ │ │ │ │ ├── GlobalErrorCodeConstants.java │ │ │ │ │ │ └── ServiceErrorCodeRange.java │ │ │ │ │ └── util/ │ │ │ │ │ └── ServiceExceptionUtil.java │ │ │ │ ├── params/ │ │ │ │ │ └── QueryParam.java │ │ │ │ ├── pojo/ │ │ │ │ │ ├── CommonResult.java │ │ │ │ │ ├── PageParam.java │ │ │ │ │ ├── PageResult.java │ │ │ │ │ ├── SortablePageParam.java │ │ │ │ │ └── SortingField.java │ │ │ │ ├── serializer/ │ │ │ │ │ ├── BigDecimalSerializer.java │ │ │ │ │ └── DoubleSerializer.java │ │ │ │ ├── util/ │ │ │ │ │ ├── cache/ │ │ │ │ │ │ └── CacheUtils.java │ │ │ │ │ ├── collection/ │ │ │ │ │ │ ├── ArrayUtils.java │ │ │ │ │ │ ├── CollectionUtils.java │ │ │ │ │ │ ├── MapUtils.java │ │ │ │ │ │ └── SetUtils.java │ │ │ │ │ ├── date/ │ │ │ │ │ │ ├── DateUtils.java │ │ │ │ │ │ └── LocalDateTimeUtils.java │ │ │ │ │ ├── http/ │ │ │ │ │ │ └── HttpUtils.java │ │ │ │ │ ├── io/ │ │ │ │ │ │ ├── FileUtils.java │ │ │ │ │ │ └── IoUtils.java │ │ │ │ │ ├── json/ │ │ │ │ │ │ └── JsonUtils.java │ │ │ │ │ ├── monitor/ │ │ │ │ │ │ └── TracerUtils.java │ │ │ │ │ ├── number/ │ │ │ │ │ │ ├── MoneyUtils.java │ │ │ │ │ │ └── NumberUtils.java │ │ │ │ │ ├── object/ │ │ │ │ │ │ ├── BeanUtils.java │ │ │ │ │ │ ├── ObjectUtils.java │ │ │ │ │ │ └── PageUtils.java │ │ │ │ │ ├── servlet/ │ │ │ │ │ │ └── ServletUtils.java │ │ │ │ │ ├── spring/ │ │ │ │ │ │ ├── SpringExpressionUtils.java │ │ │ │ │ │ └── SpringUtils.java │ │ │ │ │ ├── string/ │ │ │ │ │ │ └── StrUtils.java │ │ │ │ │ ├── validation/ │ │ │ │ │ │ └── ValidationUtils.java │ │ │ │ │ └── yshop/ │ │ │ │ │ └── LocationUtils.java │ │ │ │ └── validation/ │ │ │ │ ├── InEnum.java │ │ │ │ ├── InEnumCollectionValidator.java │ │ │ │ ├── InEnumValidator.java │ │ │ │ ├── Mobile.java │ │ │ │ ├── MobileValidator.java │ │ │ │ ├── Telephone.java │ │ │ │ ├── TelephoneValidator.java │ │ │ │ └── package-info.java │ │ │ └── test/ │ │ │ └── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── framework/ │ │ │ └── common/ │ │ │ └── util/ │ │ │ └── collection/ │ │ │ └── CollectionUtilsTest.java │ │ ├── yshop-spring-boot-starter-biz-data-permission/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ ├── java/ │ │ │ │ │ └── co/ │ │ │ │ │ └── yixiang/ │ │ │ │ │ └── yshop/ │ │ │ │ │ └── framework/ │ │ │ │ │ └── datapermission/ │ │ │ │ │ ├── config/ │ │ │ │ │ │ ├── YshopDataPermissionAutoConfiguration.java │ │ │ │ │ │ └── YshopDeptDataPermissionAutoConfiguration.java │ │ │ │ │ └── core/ │ │ │ │ │ ├── annotation/ │ │ │ │ │ │ └── DataPermission.java │ │ │ │ │ ├── aop/ │ │ │ │ │ │ ├── DataPermissionAnnotationAdvisor.java │ │ │ │ │ │ ├── DataPermissionAnnotationInterceptor.java │ │ │ │ │ │ └── DataPermissionContextHolder.java │ │ │ │ │ ├── db/ │ │ │ │ │ │ └── DataPermissionDatabaseInterceptor.java │ │ │ │ │ ├── rule/ │ │ │ │ │ │ ├── DataPermissionRule.java │ │ │ │ │ │ ├── DataPermissionRuleFactory.java │ │ │ │ │ │ ├── DataPermissionRuleFactoryImpl.java │ │ │ │ │ │ └── dept/ │ │ │ │ │ │ ├── DeptDataPermissionRule.java │ │ │ │ │ │ ├── DeptDataPermissionRuleCustomizer.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ └── util/ │ │ │ │ │ └── DataPermissionUtils.java │ │ │ │ └── resources/ │ │ │ │ └── META-INF/ │ │ │ │ └── spring/ │ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ │ └── test/ │ │ │ └── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── framework/ │ │ │ └── datapermission/ │ │ │ └── core/ │ │ │ ├── aop/ │ │ │ │ ├── DataPermissionAnnotationInterceptorTest.java │ │ │ │ └── DataPermissionContextHolderTest.java │ │ │ ├── db/ │ │ │ │ ├── DataPermissionDatabaseInterceptorTest.java │ │ │ │ └── DataPermissionDatabaseInterceptorTest2.java │ │ │ ├── rule/ │ │ │ │ ├── DataPermissionRuleFactoryImplTest.java │ │ │ │ └── dept/ │ │ │ │ └── DeptDataPermissionRuleTest.java │ │ │ └── util/ │ │ │ └── DataPermissionUtilsTest.java │ │ ├── yshop-spring-boot-starter-biz-ip/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ ├── java/ │ │ │ │ │ └── co/ │ │ │ │ │ └── yixiang/ │ │ │ │ │ └── yshop/ │ │ │ │ │ └── framework/ │ │ │ │ │ └── ip/ │ │ │ │ │ └── core/ │ │ │ │ │ ├── Area.java │ │ │ │ │ ├── enums/ │ │ │ │ │ │ └── AreaTypeEnum.java │ │ │ │ │ └── utils/ │ │ │ │ │ ├── AreaUtils.java │ │ │ │ │ └── IPUtils.java │ │ │ │ └── resources/ │ │ │ │ ├── area.csv │ │ │ │ └── ip2region.xdb │ │ │ └── test/ │ │ │ └── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── framework/ │ │ │ └── ip/ │ │ │ └── core/ │ │ │ └── utils/ │ │ │ ├── AreaUtilsTest.java │ │ │ └── IPUtilsTest.java │ │ ├── yshop-spring-boot-starter-biz-tenant/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ ├── co/ │ │ │ │ │ └── yixiang/ │ │ │ │ │ └── yshop/ │ │ │ │ │ └── framework/ │ │ │ │ │ └── tenant/ │ │ │ │ │ ├── config/ │ │ │ │ │ │ ├── TenantProperties.java │ │ │ │ │ │ └── YshopTenantAutoConfiguration.java │ │ │ │ │ └── core/ │ │ │ │ │ ├── aop/ │ │ │ │ │ │ ├── TenantIgnore.java │ │ │ │ │ │ └── TenantIgnoreAspect.java │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── TenantContextHolder.java │ │ │ │ │ ├── db/ │ │ │ │ │ │ ├── TenantBaseDO.java │ │ │ │ │ │ └── TenantDatabaseInterceptor.java │ │ │ │ │ ├── job/ │ │ │ │ │ │ ├── TenantJob.java │ │ │ │ │ │ └── TenantJobAspect.java │ │ │ │ │ ├── mq/ │ │ │ │ │ │ ├── kafka/ │ │ │ │ │ │ │ ├── TenantKafkaEnvironmentPostProcessor.java │ │ │ │ │ │ │ └── TenantKafkaProducerInterceptor.java │ │ │ │ │ │ ├── rabbitmq/ │ │ │ │ │ │ │ ├── TenantRabbitMQInitializer.java │ │ │ │ │ │ │ └── TenantRabbitMQMessagePostProcessor.java │ │ │ │ │ │ ├── redis/ │ │ │ │ │ │ │ └── TenantRedisMessageInterceptor.java │ │ │ │ │ │ └── rocketmq/ │ │ │ │ │ │ ├── TenantRocketMQConsumeMessageHook.java │ │ │ │ │ │ ├── TenantRocketMQInitializer.java │ │ │ │ │ │ └── TenantRocketMQSendMessageHook.java │ │ │ │ │ ├── redis/ │ │ │ │ │ │ └── TenantRedisCacheManager.java │ │ │ │ │ ├── security/ │ │ │ │ │ │ └── TenantSecurityWebFilter.java │ │ │ │ │ ├── service/ │ │ │ │ │ │ ├── TenantFrameworkService.java │ │ │ │ │ │ └── TenantFrameworkServiceImpl.java │ │ │ │ │ ├── util/ │ │ │ │ │ │ └── TenantUtils.java │ │ │ │ │ └── web/ │ │ │ │ │ └── TenantContextWebFilter.java │ │ │ │ └── org/ │ │ │ │ └── springframework/ │ │ │ │ └── messaging/ │ │ │ │ └── handler/ │ │ │ │ └── invocation/ │ │ │ │ └── InvocableHandlerMethod.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ ├── spring/ │ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ │ └── spring.factories │ │ ├── yshop-spring-boot-starter-excel/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ ├── java/ │ │ │ │ │ └── co/ │ │ │ │ │ └── yixiang/ │ │ │ │ │ └── yshop/ │ │ │ │ │ └── framework/ │ │ │ │ │ ├── dict/ │ │ │ │ │ │ ├── config/ │ │ │ │ │ │ │ └── YshopDictAutoConfiguration.java │ │ │ │ │ │ └── core/ │ │ │ │ │ │ └── DictFrameworkUtils.java │ │ │ │ │ └── excel/ │ │ │ │ │ └── core/ │ │ │ │ │ ├── annotations/ │ │ │ │ │ │ ├── DictFormat.java │ │ │ │ │ │ └── ExcelColumnSelect.java │ │ │ │ │ ├── convert/ │ │ │ │ │ │ ├── AreaConvert.java │ │ │ │ │ │ ├── DictConvert.java │ │ │ │ │ │ ├── JsonConvert.java │ │ │ │ │ │ └── MoneyConvert.java │ │ │ │ │ ├── function/ │ │ │ │ │ │ └── ExcelColumnSelectFunction.java │ │ │ │ │ ├── handler/ │ │ │ │ │ │ └── SelectSheetWriteHandler.java │ │ │ │ │ └── util/ │ │ │ │ │ └── ExcelUtils.java │ │ │ │ └── resources/ │ │ │ │ └── META-INF/ │ │ │ │ └── spring/ │ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ │ └── test/ │ │ │ └── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── framework/ │ │ │ └── dict/ │ │ │ └── core/ │ │ │ └── util/ │ │ │ └── DictFrameworkUtilsTest.java │ │ ├── yshop-spring-boot-starter-job/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── co/ │ │ │ │ └── yixiang/ │ │ │ │ └── yshop/ │ │ │ │ └── framework/ │ │ │ │ └── quartz/ │ │ │ │ ├── config/ │ │ │ │ │ ├── YshopAsyncAutoConfiguration.java │ │ │ │ │ └── YshopQuartzAutoConfiguration.java │ │ │ │ └── core/ │ │ │ │ ├── enums/ │ │ │ │ │ └── JobDataKeyEnum.java │ │ │ │ ├── handler/ │ │ │ │ │ ├── JobHandler.java │ │ │ │ │ └── JobHandlerInvoker.java │ │ │ │ ├── scheduler/ │ │ │ │ │ └── SchedulerManager.java │ │ │ │ ├── service/ │ │ │ │ │ └── JobLogFrameworkService.java │ │ │ │ └── util/ │ │ │ │ └── CronUtils.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ ├── yshop-spring-boot-starter-monitor/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── co/ │ │ │ │ └── yixiang/ │ │ │ │ └── yshop/ │ │ │ │ └── framework/ │ │ │ │ └── tracer/ │ │ │ │ ├── config/ │ │ │ │ │ ├── TracerProperties.java │ │ │ │ │ ├── YshopMetricsAutoConfiguration.java │ │ │ │ │ └── YshopTracerAutoConfiguration.java │ │ │ │ └── core/ │ │ │ │ ├── annotation/ │ │ │ │ │ └── BizTrace.java │ │ │ │ ├── aop/ │ │ │ │ │ └── BizTraceAspect.java │ │ │ │ ├── filter/ │ │ │ │ │ └── TraceFilter.java │ │ │ │ └── util/ │ │ │ │ └── TracerFrameworkUtils.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ ├── yshop-spring-boot-starter-mq/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── co/ │ │ │ │ └── yixiang/ │ │ │ │ └── yshop/ │ │ │ │ └── framework/ │ │ │ │ └── mq/ │ │ │ │ ├── rabbitmq/ │ │ │ │ │ └── config/ │ │ │ │ │ └── YshopRabbitMQAutoConfiguration.java │ │ │ │ └── redis/ │ │ │ │ ├── config/ │ │ │ │ │ ├── YshopRedisMQConsumerAutoConfiguration.java │ │ │ │ │ └── YshopRedisMQProducerAutoConfiguration.java │ │ │ │ └── core/ │ │ │ │ ├── RedisMQTemplate.java │ │ │ │ ├── interceptor/ │ │ │ │ │ └── RedisMessageInterceptor.java │ │ │ │ ├── job/ │ │ │ │ │ └── RedisPendingMessageResendJob.java │ │ │ │ ├── message/ │ │ │ │ │ └── AbstractRedisMessage.java │ │ │ │ ├── pubsub/ │ │ │ │ │ ├── AbstractRedisChannelMessage.java │ │ │ │ │ └── AbstractRedisChannelMessageListener.java │ │ │ │ └── stream/ │ │ │ │ ├── AbstractRedisStreamMessage.java │ │ │ │ └── AbstractRedisStreamMessageListener.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ ├── yshop-spring-boot-starter-mybatis/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── co/ │ │ │ │ └── yixiang/ │ │ │ │ └── yshop/ │ │ │ │ └── framework/ │ │ │ │ ├── datasource/ │ │ │ │ │ ├── config/ │ │ │ │ │ │ └── YshopDataSourceAutoConfiguration.java │ │ │ │ │ └── core/ │ │ │ │ │ ├── enums/ │ │ │ │ │ │ └── DataSourceEnum.java │ │ │ │ │ └── filter/ │ │ │ │ │ └── DruidAdRemoveFilter.java │ │ │ │ ├── mybatis/ │ │ │ │ │ ├── config/ │ │ │ │ │ │ ├── IdTypeEnvironmentPostProcessor.java │ │ │ │ │ │ └── YshopMybatisAutoConfiguration.java │ │ │ │ │ └── core/ │ │ │ │ │ ├── dataobject/ │ │ │ │ │ │ └── BaseDO.java │ │ │ │ │ ├── enums/ │ │ │ │ │ │ └── SqlConstants.java │ │ │ │ │ ├── handler/ │ │ │ │ │ │ └── DefaultDBFieldHandler.java │ │ │ │ │ ├── mapper/ │ │ │ │ │ │ └── BaseMapperX.java │ │ │ │ │ ├── query/ │ │ │ │ │ │ ├── LambdaQueryWrapperX.java │ │ │ │ │ │ ├── MPJLambdaWrapperX.java │ │ │ │ │ │ └── QueryWrapperX.java │ │ │ │ │ ├── type/ │ │ │ │ │ │ ├── EncryptTypeHandler.java │ │ │ │ │ │ ├── IntegerListTypeHandler.java │ │ │ │ │ │ ├── JsonLongSetTypeHandler.java │ │ │ │ │ │ ├── LongListTypeHandler.java │ │ │ │ │ │ └── StringListTypeHandler.java │ │ │ │ │ └── util/ │ │ │ │ │ ├── JdbcUtils.java │ │ │ │ │ └── MyBatisUtils.java │ │ │ │ └── translate/ │ │ │ │ ├── config/ │ │ │ │ │ └── YshopTranslateAutoConfiguration.java │ │ │ │ └── core/ │ │ │ │ └── TranslateUtils.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ ├── spring/ │ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ │ └── spring.factories │ │ ├── yshop-spring-boot-starter-protection/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── co/ │ │ │ │ └── yixiang/ │ │ │ │ └── yshop/ │ │ │ │ └── framework/ │ │ │ │ ├── idempotent/ │ │ │ │ │ ├── config/ │ │ │ │ │ │ └── YshopIdempotentConfiguration.java │ │ │ │ │ └── core/ │ │ │ │ │ ├── annotation/ │ │ │ │ │ │ └── Idempotent.java │ │ │ │ │ ├── aop/ │ │ │ │ │ │ └── IdempotentAspect.java │ │ │ │ │ ├── keyresolver/ │ │ │ │ │ │ ├── IdempotentKeyResolver.java │ │ │ │ │ │ └── impl/ │ │ │ │ │ │ ├── DefaultIdempotentKeyResolver.java │ │ │ │ │ │ ├── ExpressionIdempotentKeyResolver.java │ │ │ │ │ │ └── UserIdempotentKeyResolver.java │ │ │ │ │ └── redis/ │ │ │ │ │ └── IdempotentRedisDAO.java │ │ │ │ ├── lock4j/ │ │ │ │ │ ├── config/ │ │ │ │ │ │ └── YshopLock4jConfiguration.java │ │ │ │ │ └── core/ │ │ │ │ │ ├── DefaultLockFailureStrategy.java │ │ │ │ │ └── Lock4jRedisKeyConstants.java │ │ │ │ └── ratelimiter/ │ │ │ │ ├── config/ │ │ │ │ │ └── YshopRateLimiterConfiguration.java │ │ │ │ └── core/ │ │ │ │ ├── annotation/ │ │ │ │ │ └── RateLimiter.java │ │ │ │ ├── aop/ │ │ │ │ │ └── RateLimiterAspect.java │ │ │ │ ├── keyresolver/ │ │ │ │ │ ├── RateLimiterKeyResolver.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── ClientIpRateLimiterKeyResolver.java │ │ │ │ │ ├── DefaultRateLimiterKeyResolver.java │ │ │ │ │ ├── ExpressionRateLimiterKeyResolver.java │ │ │ │ │ ├── ServerNodeRateLimiterKeyResolver.java │ │ │ │ │ └── UserRateLimiterKeyResolver.java │ │ │ │ └── redis/ │ │ │ │ └── RateLimiterRedisDAO.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ ├── yshop-spring-boot-starter-redis/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── co/ │ │ │ │ └── yixiang/ │ │ │ │ └── yshop/ │ │ │ │ └── framework/ │ │ │ │ └── redis/ │ │ │ │ ├── config/ │ │ │ │ │ ├── RedissonConfig.java │ │ │ │ │ ├── YshopCacheAutoConfiguration.java │ │ │ │ │ ├── YshopCacheProperties.java │ │ │ │ │ └── YshopRedisAutoConfiguration.java │ │ │ │ ├── core/ │ │ │ │ │ ├── RedisKeyDefine.java │ │ │ │ │ ├── RedisKeyRegistry.java │ │ │ │ │ └── TimeoutRedisCacheManager.java │ │ │ │ └── util/ │ │ │ │ └── redis/ │ │ │ │ └── RedisUtil.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ ├── yshop-spring-boot-starter-security/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── co/ │ │ │ │ └── yixiang/ │ │ │ │ └── yshop/ │ │ │ │ └── framework/ │ │ │ │ ├── operatelog/ │ │ │ │ │ ├── config/ │ │ │ │ │ │ └── YshopOperateLogConfiguration.java │ │ │ │ │ └── core/ │ │ │ │ │ └── service/ │ │ │ │ │ └── LogRecordServiceImpl.java │ │ │ │ └── security/ │ │ │ │ ├── config/ │ │ │ │ │ ├── AuthorizeRequestsCustomizer.java │ │ │ │ │ ├── SecurityProperties.java │ │ │ │ │ ├── YshopSecurityAutoConfiguration.java │ │ │ │ │ └── YshopWebSecurityConfigurerAdapter.java │ │ │ │ └── core/ │ │ │ │ ├── LoginUser.java │ │ │ │ ├── annotations/ │ │ │ │ │ └── PreAuthenticated.java │ │ │ │ ├── aop/ │ │ │ │ │ └── PreAuthenticatedAspect.java │ │ │ │ ├── context/ │ │ │ │ │ └── TransmittableThreadLocalSecurityContextHolderStrategy.java │ │ │ │ ├── filter/ │ │ │ │ │ └── TokenAuthenticationFilter.java │ │ │ │ ├── handler/ │ │ │ │ │ ├── AccessDeniedHandlerImpl.java │ │ │ │ │ └── AuthenticationEntryPointImpl.java │ │ │ │ ├── service/ │ │ │ │ │ ├── SecurityFrameworkService.java │ │ │ │ │ └── SecurityFrameworkServiceImpl.java │ │ │ │ └── util/ │ │ │ │ └── SecurityFrameworkUtils.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ ├── yshop-spring-boot-starter-test/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── framework/ │ │ │ └── test/ │ │ │ ├── config/ │ │ │ │ ├── RedisTestConfiguration.java │ │ │ │ └── SqlInitializationTestConfiguration.java │ │ │ └── core/ │ │ │ ├── ut/ │ │ │ │ ├── BaseDbAndRedisUnitTest.java │ │ │ │ ├── BaseDbUnitTest.java │ │ │ │ ├── BaseMockitoUnitTest.java │ │ │ │ └── BaseRedisUnitTest.java │ │ │ └── util/ │ │ │ ├── AssertUtils.java │ │ │ └── RandomUtils.java │ │ ├── yshop-spring-boot-starter-web/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ ├── java/ │ │ │ │ │ └── co/ │ │ │ │ │ └── yixiang/ │ │ │ │ │ └── yshop/ │ │ │ │ │ └── framework/ │ │ │ │ │ ├── apilog/ │ │ │ │ │ │ ├── config/ │ │ │ │ │ │ │ └── YshopApiLogAutoConfiguration.java │ │ │ │ │ │ └── core/ │ │ │ │ │ │ ├── annotation/ │ │ │ │ │ │ │ └── ApiAccessLog.java │ │ │ │ │ │ ├── enums/ │ │ │ │ │ │ │ └── OperateTypeEnum.java │ │ │ │ │ │ ├── filter/ │ │ │ │ │ │ │ └── ApiAccessLogFilter.java │ │ │ │ │ │ ├── interceptor/ │ │ │ │ │ │ │ └── ApiAccessLogInterceptor.java │ │ │ │ │ │ └── service/ │ │ │ │ │ │ ├── ApiAccessLogFrameworkService.java │ │ │ │ │ │ ├── ApiAccessLogFrameworkServiceImpl.java │ │ │ │ │ │ ├── ApiErrorLogFrameworkService.java │ │ │ │ │ │ └── ApiErrorLogFrameworkServiceImpl.java │ │ │ │ │ ├── banner/ │ │ │ │ │ │ ├── config/ │ │ │ │ │ │ │ └── YshopBannerAutoConfiguration.java │ │ │ │ │ │ └── core/ │ │ │ │ │ │ └── BannerApplicationRunner.java │ │ │ │ │ ├── desensitize/ │ │ │ │ │ │ └── core/ │ │ │ │ │ │ ├── base/ │ │ │ │ │ │ │ ├── annotation/ │ │ │ │ │ │ │ │ └── DesensitizeBy.java │ │ │ │ │ │ │ ├── handler/ │ │ │ │ │ │ │ │ └── DesensitizationHandler.java │ │ │ │ │ │ │ └── serializer/ │ │ │ │ │ │ │ └── StringDesensitizeSerializer.java │ │ │ │ │ │ ├── regex/ │ │ │ │ │ │ │ ├── annotation/ │ │ │ │ │ │ │ │ ├── EmailDesensitize.java │ │ │ │ │ │ │ │ └── RegexDesensitize.java │ │ │ │ │ │ │ └── handler/ │ │ │ │ │ │ │ ├── AbstractRegexDesensitizationHandler.java │ │ │ │ │ │ │ ├── DefaultRegexDesensitizationHandler.java │ │ │ │ │ │ │ └── EmailDesensitizationHandler.java │ │ │ │ │ │ └── slider/ │ │ │ │ │ │ ├── annotation/ │ │ │ │ │ │ │ ├── BankCardDesensitize.java │ │ │ │ │ │ │ ├── CarLicenseDesensitize.java │ │ │ │ │ │ │ ├── ChineseNameDesensitize.java │ │ │ │ │ │ │ ├── FixedPhoneDesensitize.java │ │ │ │ │ │ │ ├── IdCardDesensitize.java │ │ │ │ │ │ │ ├── MobileDesensitize.java │ │ │ │ │ │ │ ├── PasswordDesensitize.java │ │ │ │ │ │ │ └── SliderDesensitize.java │ │ │ │ │ │ └── handler/ │ │ │ │ │ │ ├── AbstractSliderDesensitizationHandler.java │ │ │ │ │ │ ├── BankCardDesensitization.java │ │ │ │ │ │ ├── CarLicenseDesensitization.java │ │ │ │ │ │ ├── ChineseNameDesensitization.java │ │ │ │ │ │ ├── DefaultDesensitizationHandler.java │ │ │ │ │ │ ├── FixedPhoneDesensitization.java │ │ │ │ │ │ ├── IdCardDesensitization.java │ │ │ │ │ │ ├── MobileDesensitization.java │ │ │ │ │ │ └── PasswordDesensitization.java │ │ │ │ │ ├── jackson/ │ │ │ │ │ │ ├── config/ │ │ │ │ │ │ │ └── YshopJacksonAutoConfiguration.java │ │ │ │ │ │ └── core/ │ │ │ │ │ │ └── databind/ │ │ │ │ │ │ ├── NumberSerializer.java │ │ │ │ │ │ ├── TimestampLocalDateTimeDeserializer.java │ │ │ │ │ │ └── TimestampLocalDateTimeSerializer.java │ │ │ │ │ ├── swagger/ │ │ │ │ │ │ └── config/ │ │ │ │ │ │ ├── SwaggerProperties.java │ │ │ │ │ │ └── YshopSwaggerAutoConfiguration.java │ │ │ │ │ ├── web/ │ │ │ │ │ │ ├── config/ │ │ │ │ │ │ │ ├── WebProperties.java │ │ │ │ │ │ │ └── YshopWebAutoConfiguration.java │ │ │ │ │ │ └── core/ │ │ │ │ │ │ ├── filter/ │ │ │ │ │ │ │ ├── ApiRequestFilter.java │ │ │ │ │ │ │ ├── CacheRequestBodyFilter.java │ │ │ │ │ │ │ ├── CacheRequestBodyWrapper.java │ │ │ │ │ │ │ └── DemoFilter.java │ │ │ │ │ │ ├── handler/ │ │ │ │ │ │ │ ├── GlobalExceptionHandler.java │ │ │ │ │ │ │ └── GlobalResponseBodyHandler.java │ │ │ │ │ │ └── util/ │ │ │ │ │ │ └── WebFrameworkUtils.java │ │ │ │ │ └── xss/ │ │ │ │ │ ├── config/ │ │ │ │ │ │ ├── XssProperties.java │ │ │ │ │ │ └── YshopXssAutoConfiguration.java │ │ │ │ │ └── core/ │ │ │ │ │ ├── clean/ │ │ │ │ │ │ ├── JsoupXssCleaner.java │ │ │ │ │ │ └── XssCleaner.java │ │ │ │ │ ├── filter/ │ │ │ │ │ │ ├── XssFilter.java │ │ │ │ │ │ └── XssRequestWrapper.java │ │ │ │ │ └── json/ │ │ │ │ │ └── XssStringJsonDeserializer.java │ │ │ │ └── resources/ │ │ │ │ ├── META-INF/ │ │ │ │ │ └── spring/ │ │ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ │ │ └── banner.txt │ │ │ └── test/ │ │ │ └── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── framework/ │ │ │ └── desensitize/ │ │ │ └── core/ │ │ │ ├── DesensitizeTest.java │ │ │ ├── annotation/ │ │ │ │ └── Address.java │ │ │ └── handler/ │ │ │ └── AddressHandler.java │ │ └── yshop-spring-boot-starter-websocket/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── framework/ │ │ │ └── websocket/ │ │ │ ├── config/ │ │ │ │ ├── WebSocketProperties.java │ │ │ │ └── YshopWebSocketAutoConfiguration.java │ │ │ └── core/ │ │ │ ├── handler/ │ │ │ │ └── JsonWebSocketMessageHandler.java │ │ │ ├── listener/ │ │ │ │ └── WebSocketMessageListener.java │ │ │ ├── message/ │ │ │ │ └── JsonWebSocketMessage.java │ │ │ ├── security/ │ │ │ │ ├── LoginUserHandshakeInterceptor.java │ │ │ │ └── WebSocketAuthorizeRequestsCustomizer.java │ │ │ ├── sender/ │ │ │ │ ├── AbstractWebSocketMessageSender.java │ │ │ │ ├── WebSocketMessageSender.java │ │ │ │ ├── kafka/ │ │ │ │ │ ├── KafkaWebSocketMessage.java │ │ │ │ │ ├── KafkaWebSocketMessageConsumer.java │ │ │ │ │ └── KafkaWebSocketMessageSender.java │ │ │ │ ├── local/ │ │ │ │ │ └── LocalWebSocketMessageSender.java │ │ │ │ ├── rabbitmq/ │ │ │ │ │ ├── RabbitMQWebSocketMessage.java │ │ │ │ │ ├── RabbitMQWebSocketMessageConsumer.java │ │ │ │ │ └── RabbitMQWebSocketMessageSender.java │ │ │ │ ├── redis/ │ │ │ │ │ ├── RedisWebSocketMessage.java │ │ │ │ │ ├── RedisWebSocketMessageConsumer.java │ │ │ │ │ └── RedisWebSocketMessageSender.java │ │ │ │ └── rocketmq/ │ │ │ │ ├── RocketMQWebSocketMessage.java │ │ │ │ ├── RocketMQWebSocketMessageConsumer.java │ │ │ │ └── RocketMQWebSocketMessageSender.java │ │ │ ├── session/ │ │ │ │ ├── WebSocketSessionHandlerDecorator.java │ │ │ │ ├── WebSocketSessionManager.java │ │ │ │ └── WebSocketSessionManagerImpl.java │ │ │ └── util/ │ │ │ └── WebSocketFrameworkUtils.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── spring/ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ ├── yshop-module-express/ │ │ ├── pom.xml │ │ ├── yshop-module-express-api/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── express/ │ │ │ ├── enums/ │ │ │ │ └── ErrorCodeConstants.java │ │ │ └── kdniao/ │ │ │ ├── enums/ │ │ │ │ ├── KdniaoLogisticsCodeEnum.java │ │ │ │ └── KdniaoLogisticsStatusEnum.java │ │ │ ├── model/ │ │ │ │ ├── dto/ │ │ │ │ │ ├── KdniaoApiBaseDTO.java │ │ │ │ │ ├── KdniaoApiDTO.java │ │ │ │ │ ├── KdniaoElectronicsOrderDTO.java │ │ │ │ │ └── KdniaoElectronicsOrderGoodsDTO.java │ │ │ │ └── vo/ │ │ │ │ ├── KdniaoApiVO.java │ │ │ │ ├── KdniaoLogisticsVO.java │ │ │ │ └── KdniaoOrderVO.java │ │ │ └── util/ │ │ │ └── KdniaoUtil.java │ │ └── yshop-module-express-biz/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── co/ │ │ └── yixiang/ │ │ └── yshop/ │ │ └── module/ │ │ └── express/ │ │ ├── controller/ │ │ │ ├── admin/ │ │ │ │ └── express/ │ │ │ │ ├── ExpressController.java │ │ │ │ └── vo/ │ │ │ │ ├── ExpressBaseVO.java │ │ │ │ ├── ExpressCreateReqVO.java │ │ │ │ ├── ExpressExcelVO.java │ │ │ │ ├── ExpressExportReqVO.java │ │ │ │ ├── ExpressPageReqVO.java │ │ │ │ ├── ExpressRespVO.java │ │ │ │ └── ExpressUpdateReqVO.java │ │ │ └── app/ │ │ │ └── express/ │ │ │ └── AppExpressController.java │ │ ├── convert/ │ │ │ └── express/ │ │ │ └── ExpressConvert.java │ │ ├── dal/ │ │ │ ├── dataobject/ │ │ │ │ └── express/ │ │ │ │ └── ExpressDO.java │ │ │ ├── mysql/ │ │ │ │ └── express/ │ │ │ │ └── ExpressMapper.java │ │ │ └── redis/ │ │ │ ├── RedisKeyConstants.java │ │ │ └── express/ │ │ │ └── ExpressRedisDAO.java │ │ └── service/ │ │ └── express/ │ │ ├── ExpressService.java │ │ └── ExpressServiceImpl.java │ ├── yshop-module-infra/ │ │ ├── pom.xml │ │ ├── yshop-module-infra-api/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── infra/ │ │ │ ├── api/ │ │ │ │ ├── file/ │ │ │ │ │ └── FileApi.java │ │ │ │ ├── logger/ │ │ │ │ │ ├── ApiAccessLogApi.java │ │ │ │ │ ├── ApiErrorLogApi.java │ │ │ │ │ └── dto/ │ │ │ │ │ ├── ApiAccessLogCreateReqDTO.java │ │ │ │ │ └── ApiErrorLogCreateReqDTO.java │ │ │ │ └── websocket/ │ │ │ │ └── WebSocketSenderApi.java │ │ │ └── enums/ │ │ │ ├── DictTypeConstants.java │ │ │ └── ErrorCodeConstants.java │ │ └── yshop-module-infra-biz/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── infra/ │ │ │ ├── api/ │ │ │ │ ├── file/ │ │ │ │ │ └── FileApiImpl.java │ │ │ │ ├── logger/ │ │ │ │ │ ├── ApiAccessLogApiImpl.java │ │ │ │ │ └── ApiErrorLogApiImpl.java │ │ │ │ └── websocket/ │ │ │ │ └── WebSocketSenderApiImpl.java │ │ │ ├── controller/ │ │ │ │ ├── admin/ │ │ │ │ │ ├── codegen/ │ │ │ │ │ │ ├── CodegenController.java │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ ├── CodegenCreateListReqVO.java │ │ │ │ │ │ ├── CodegenDetailRespVO.java │ │ │ │ │ │ ├── CodegenPreviewRespVO.java │ │ │ │ │ │ ├── CodegenUpdateReqVO.java │ │ │ │ │ │ ├── column/ │ │ │ │ │ │ │ ├── CodegenColumnRespVO.java │ │ │ │ │ │ │ └── CodegenColumnSaveReqVO.java │ │ │ │ │ │ └── table/ │ │ │ │ │ │ ├── CodegenTablePageReqVO.java │ │ │ │ │ │ ├── CodegenTableRespVO.java │ │ │ │ │ │ ├── CodegenTableSaveReqVO.java │ │ │ │ │ │ └── DatabaseTableRespVO.java │ │ │ │ │ ├── config/ │ │ │ │ │ │ ├── ConfigController.java │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ ├── ConfigPageReqVO.java │ │ │ │ │ │ ├── ConfigRespVO.java │ │ │ │ │ │ └── ConfigSaveReqVO.java │ │ │ │ │ ├── db/ │ │ │ │ │ │ ├── DataSourceConfigController.java │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ ├── DataSourceConfigRespVO.java │ │ │ │ │ │ └── DataSourceConfigSaveReqVO.java │ │ │ │ │ ├── demo/ │ │ │ │ │ │ ├── demo01/ │ │ │ │ │ │ │ ├── Demo01ContactController.java │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ ├── Demo01ContactPageReqVO.java │ │ │ │ │ │ │ ├── Demo01ContactRespVO.java │ │ │ │ │ │ │ └── Demo01ContactSaveReqVO.java │ │ │ │ │ │ ├── demo02/ │ │ │ │ │ │ │ ├── Demo02CategoryController.java │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ ├── Demo02CategoryListReqVO.java │ │ │ │ │ │ │ ├── Demo02CategoryRespVO.java │ │ │ │ │ │ │ └── Demo02CategorySaveReqVO.java │ │ │ │ │ │ └── demo03/ │ │ │ │ │ │ ├── Demo03StudentController.java │ │ │ │ │ │ ├── package-info.java │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ ├── Demo03StudentPageReqVO.java │ │ │ │ │ │ ├── Demo03StudentRespVO.java │ │ │ │ │ │ └── Demo03StudentSaveReqVO.java │ │ │ │ │ ├── file/ │ │ │ │ │ │ ├── FileConfigController.java │ │ │ │ │ │ ├── FileController.java │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ ├── config/ │ │ │ │ │ │ │ ├── FileConfigPageReqVO.java │ │ │ │ │ │ │ ├── FileConfigRespVO.java │ │ │ │ │ │ │ └── FileConfigSaveReqVO.java │ │ │ │ │ │ └── file/ │ │ │ │ │ │ ├── FileCreateReqVO.java │ │ │ │ │ │ ├── FilePageReqVO.java │ │ │ │ │ │ ├── FilePresignedUrlRespVO.java │ │ │ │ │ │ ├── FileRespVO.java │ │ │ │ │ │ └── FileUploadReqVO.java │ │ │ │ │ ├── job/ │ │ │ │ │ │ ├── JobController.http │ │ │ │ │ │ ├── JobController.java │ │ │ │ │ │ ├── JobLogController.java │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ ├── job/ │ │ │ │ │ │ │ ├── JobPageReqVO.java │ │ │ │ │ │ │ ├── JobRespVO.java │ │ │ │ │ │ │ └── JobSaveReqVO.java │ │ │ │ │ │ └── log/ │ │ │ │ │ │ ├── JobLogPageReqVO.java │ │ │ │ │ │ └── JobLogRespVO.java │ │ │ │ │ ├── logger/ │ │ │ │ │ │ ├── ApiAccessLogController.java │ │ │ │ │ │ ├── ApiErrorLogController.java │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ ├── apiaccesslog/ │ │ │ │ │ │ │ ├── ApiAccessLogPageReqVO.java │ │ │ │ │ │ │ └── ApiAccessLogRespVO.java │ │ │ │ │ │ └── apierrorlog/ │ │ │ │ │ │ ├── ApiErrorLogPageReqVO.java │ │ │ │ │ │ └── ApiErrorLogRespVO.java │ │ │ │ │ └── redis/ │ │ │ │ │ ├── RedisController.http │ │ │ │ │ ├── RedisController.java │ │ │ │ │ └── vo/ │ │ │ │ │ └── RedisMonitorRespVO.java │ │ │ │ └── app/ │ │ │ │ └── file/ │ │ │ │ ├── AppFileController.java │ │ │ │ └── vo/ │ │ │ │ └── AppFileUploadReqVO.java │ │ │ ├── convert/ │ │ │ │ ├── codegen/ │ │ │ │ │ └── CodegenConvert.java │ │ │ │ ├── config/ │ │ │ │ │ └── ConfigConvert.java │ │ │ │ ├── file/ │ │ │ │ │ └── FileConfigConvert.java │ │ │ │ └── redis/ │ │ │ │ └── RedisConvert.java │ │ │ ├── dal/ │ │ │ │ ├── dataobject/ │ │ │ │ │ ├── codegen/ │ │ │ │ │ │ ├── CodegenColumnDO.java │ │ │ │ │ │ └── CodegenTableDO.java │ │ │ │ │ ├── config/ │ │ │ │ │ │ └── ConfigDO.java │ │ │ │ │ ├── db/ │ │ │ │ │ │ └── DataSourceConfigDO.java │ │ │ │ │ ├── demo/ │ │ │ │ │ │ ├── demo01/ │ │ │ │ │ │ │ └── Demo01ContactDO.java │ │ │ │ │ │ ├── demo02/ │ │ │ │ │ │ │ └── Demo02CategoryDO.java │ │ │ │ │ │ └── demo03/ │ │ │ │ │ │ ├── Demo03CourseDO.java │ │ │ │ │ │ ├── Demo03GradeDO.java │ │ │ │ │ │ └── Demo03StudentDO.java │ │ │ │ │ ├── file/ │ │ │ │ │ │ ├── FileConfigDO.java │ │ │ │ │ │ ├── FileContentDO.java │ │ │ │ │ │ └── FileDO.java │ │ │ │ │ ├── job/ │ │ │ │ │ │ ├── JobDO.java │ │ │ │ │ │ └── JobLogDO.java │ │ │ │ │ └── logger/ │ │ │ │ │ ├── ApiAccessLogDO.java │ │ │ │ │ └── ApiErrorLogDO.java │ │ │ │ └── mysql/ │ │ │ │ ├── codegen/ │ │ │ │ │ ├── CodegenColumnMapper.java │ │ │ │ │ └── CodegenTableMapper.java │ │ │ │ ├── config/ │ │ │ │ │ └── ConfigMapper.java │ │ │ │ ├── db/ │ │ │ │ │ └── DataSourceConfigMapper.java │ │ │ │ ├── demo/ │ │ │ │ │ ├── demo01/ │ │ │ │ │ │ └── Demo01ContactMapper.java │ │ │ │ │ ├── demo02/ │ │ │ │ │ │ └── Demo02CategoryMapper.java │ │ │ │ │ └── demo03/ │ │ │ │ │ ├── Demo03CourseMapper.java │ │ │ │ │ ├── Demo03GradeMapper.java │ │ │ │ │ └── Demo03StudentMapper.java │ │ │ │ ├── file/ │ │ │ │ │ ├── FileConfigMapper.java │ │ │ │ │ ├── FileContentMapper.java │ │ │ │ │ └── FileMapper.java │ │ │ │ ├── job/ │ │ │ │ │ ├── JobLogMapper.java │ │ │ │ │ └── JobMapper.java │ │ │ │ └── logger/ │ │ │ │ ├── ApiAccessLogMapper.java │ │ │ │ └── ApiErrorLogMapper.java │ │ │ ├── enums/ │ │ │ │ ├── codegen/ │ │ │ │ │ ├── CodegenColumnHtmlTypeEnum.java │ │ │ │ │ ├── CodegenColumnListConditionEnum.java │ │ │ │ │ ├── CodegenFrontTypeEnum.java │ │ │ │ │ ├── CodegenSceneEnum.java │ │ │ │ │ └── CodegenTemplateTypeEnum.java │ │ │ │ ├── config/ │ │ │ │ │ └── ConfigTypeEnum.java │ │ │ │ ├── job/ │ │ │ │ │ ├── JobLogStatusEnum.java │ │ │ │ │ └── JobStatusEnum.java │ │ │ │ └── logger/ │ │ │ │ └── ApiErrorLogProcessStatusEnum.java │ │ │ ├── framework/ │ │ │ │ ├── codegen/ │ │ │ │ │ └── config/ │ │ │ │ │ ├── CodegenConfiguration.java │ │ │ │ │ └── CodegenProperties.java │ │ │ │ ├── file/ │ │ │ │ │ ├── config/ │ │ │ │ │ │ └── YshopFileAutoConfiguration.java │ │ │ │ │ └── core/ │ │ │ │ │ ├── client/ │ │ │ │ │ │ ├── AbstractFileClient.java │ │ │ │ │ │ ├── FileClient.java │ │ │ │ │ │ ├── FileClientConfig.java │ │ │ │ │ │ ├── FileClientFactory.java │ │ │ │ │ │ ├── FileClientFactoryImpl.java │ │ │ │ │ │ ├── db/ │ │ │ │ │ │ │ ├── DBFileClient.java │ │ │ │ │ │ │ └── DBFileClientConfig.java │ │ │ │ │ │ ├── ftp/ │ │ │ │ │ │ │ ├── FtpFileClient.java │ │ │ │ │ │ │ └── FtpFileClientConfig.java │ │ │ │ │ │ ├── local/ │ │ │ │ │ │ │ ├── LocalFileClient.java │ │ │ │ │ │ │ └── LocalFileClientConfig.java │ │ │ │ │ │ ├── s3/ │ │ │ │ │ │ │ ├── FilePresignedUrlRespDTO.java │ │ │ │ │ │ │ ├── S3FileClient.java │ │ │ │ │ │ │ └── S3FileClientConfig.java │ │ │ │ │ │ └── sftp/ │ │ │ │ │ │ ├── SftpFileClient.java │ │ │ │ │ │ └── SftpFileClientConfig.java │ │ │ │ │ ├── enums/ │ │ │ │ │ │ └── FileStorageEnum.java │ │ │ │ │ └── utils/ │ │ │ │ │ └── FileTypeUtils.java │ │ │ │ ├── monitor/ │ │ │ │ │ └── config/ │ │ │ │ │ └── AdminServerConfiguration.java │ │ │ │ ├── security/ │ │ │ │ │ └── config/ │ │ │ │ │ └── SecurityConfiguration.java │ │ │ │ └── web/ │ │ │ │ └── config/ │ │ │ │ └── InfraWebConfiguration.java │ │ │ ├── job/ │ │ │ │ ├── job/ │ │ │ │ │ └── JobLogCleanJob.java │ │ │ │ └── logger/ │ │ │ │ ├── AccessLogCleanJob.java │ │ │ │ └── ErrorLogCleanJob.java │ │ │ ├── service/ │ │ │ │ ├── codegen/ │ │ │ │ │ ├── CodegenService.java │ │ │ │ │ ├── CodegenServiceImpl.java │ │ │ │ │ └── inner/ │ │ │ │ │ ├── CodegenBuilder.java │ │ │ │ │ └── CodegenEngine.java │ │ │ │ ├── config/ │ │ │ │ │ ├── ConfigService.java │ │ │ │ │ └── ConfigServiceImpl.java │ │ │ │ ├── db/ │ │ │ │ │ ├── DataSourceConfigService.java │ │ │ │ │ ├── DataSourceConfigServiceImpl.java │ │ │ │ │ ├── DatabaseTableService.java │ │ │ │ │ └── DatabaseTableServiceImpl.java │ │ │ │ ├── demo/ │ │ │ │ │ ├── demo01/ │ │ │ │ │ │ ├── Demo01ContactService.java │ │ │ │ │ │ └── Demo01ContactServiceImpl.java │ │ │ │ │ ├── demo02/ │ │ │ │ │ │ ├── Demo02CategoryService.java │ │ │ │ │ │ └── Demo02CategoryServiceImpl.java │ │ │ │ │ └── demo03/ │ │ │ │ │ ├── Demo03StudentService.java │ │ │ │ │ └── Demo03StudentServiceImpl.java │ │ │ │ ├── file/ │ │ │ │ │ ├── FileConfigService.java │ │ │ │ │ ├── FileConfigServiceImpl.java │ │ │ │ │ ├── FileService.java │ │ │ │ │ └── FileServiceImpl.java │ │ │ │ ├── job/ │ │ │ │ │ ├── JobLogService.java │ │ │ │ │ ├── JobLogServiceImpl.java │ │ │ │ │ ├── JobService.java │ │ │ │ │ └── JobServiceImpl.java │ │ │ │ └── logger/ │ │ │ │ ├── ApiAccessLogService.java │ │ │ │ ├── ApiAccessLogServiceImpl.java │ │ │ │ ├── ApiErrorLogService.java │ │ │ │ └── ApiErrorLogServiceImpl.java │ │ │ └── websocket/ │ │ │ ├── DemoWebSocketMessageListener.java │ │ │ └── message/ │ │ │ ├── DemoReceiveMessage.java │ │ │ └── DemoSendMessage.java │ │ └── resources/ │ │ └── codegen/ │ │ ├── java/ │ │ │ ├── controller/ │ │ │ │ ├── controller.vm │ │ │ │ └── vo/ │ │ │ │ ├── listReqVO.vm │ │ │ │ ├── pageReqVO.vm │ │ │ │ ├── respVO.vm │ │ │ │ └── saveReqVO.vm │ │ │ ├── dal/ │ │ │ │ ├── do.vm │ │ │ │ ├── do_sub.vm │ │ │ │ ├── mapper.vm │ │ │ │ ├── mapper.xml.vm │ │ │ │ └── mapper_sub.vm │ │ │ ├── enums/ │ │ │ │ └── errorcode.vm │ │ │ ├── service/ │ │ │ │ ├── service.vm │ │ │ │ └── serviceImpl.vm │ │ │ └── test/ │ │ │ └── serviceTest.vm │ │ ├── sql/ │ │ │ ├── h2.vm │ │ │ └── sql.vm │ │ ├── vue/ │ │ │ ├── api/ │ │ │ │ └── api.js.vm │ │ │ └── views/ │ │ │ ├── components/ │ │ │ │ ├── form_sub_erp.vue.vm │ │ │ │ ├── form_sub_inner.vue.vm │ │ │ │ ├── form_sub_normal.vue.vm │ │ │ │ ├── list_sub_erp.vue.vm │ │ │ │ └── list_sub_inner.vue.vm │ │ │ ├── form.vue.vm │ │ │ └── index.vue.vm │ │ ├── vue3/ │ │ │ ├── api/ │ │ │ │ └── api.ts.vm │ │ │ └── views/ │ │ │ ├── components/ │ │ │ │ ├── form_sub_erp.vue.vm │ │ │ │ ├── form_sub_inner.vue.vm │ │ │ │ ├── form_sub_normal.vue.vm │ │ │ │ ├── list_sub_erp.vue.vm │ │ │ │ └── list_sub_inner.vue.vm │ │ │ ├── form.vue.vm │ │ │ └── index.vue.vm │ │ ├── vue3_schema/ │ │ │ ├── api/ │ │ │ │ └── api.ts.vm │ │ │ └── views/ │ │ │ ├── data.ts.vm │ │ │ ├── form.vue.vm │ │ │ └── index.vue.vm │ │ └── vue3_vben/ │ │ ├── api/ │ │ │ └── api.ts.vm │ │ └── views/ │ │ ├── data.ts.vm │ │ ├── form.vue.vm │ │ └── index.vue.vm │ ├── yshop-module-mall/ │ │ ├── pom.xml │ │ ├── yshop-module-order-api/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── order/ │ │ │ └── enums/ │ │ │ ├── AdminAfterOrderStatusEnum.java │ │ │ ├── AdminOrderStatusEnum.java │ │ │ ├── AfterChangeTypeEnum.java │ │ │ ├── AfterSalesStatusEnum.java │ │ │ ├── AfterStatusEnum.java │ │ │ ├── AfterTypeEnum.java │ │ │ ├── AppFromEnum.java │ │ │ ├── ErrorCodeConstants.java │ │ │ ├── OrderLogEnum.java │ │ │ ├── OrderStatusEnum.java │ │ │ ├── PayTypeEnum.java │ │ │ ├── ShippingTempEnum.java │ │ │ └── UpdateOrderEnum.java │ │ ├── yshop-module-order-biz/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── co/ │ │ │ │ └── yixiang/ │ │ │ │ └── yshop/ │ │ │ │ └── module/ │ │ │ │ └── order/ │ │ │ │ ├── controller/ │ │ │ │ │ ├── admin/ │ │ │ │ │ │ └── storeorder/ │ │ │ │ │ │ ├── StoreOrderController.java │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ ├── ShoperOrderTimeDataVo.java │ │ │ │ │ │ ├── StoreOrderBaseVO.java │ │ │ │ │ │ ├── StoreOrderCreateReqVO.java │ │ │ │ │ │ ├── StoreOrderExcelVO.java │ │ │ │ │ │ ├── StoreOrderExportReqVO.java │ │ │ │ │ │ ├── StoreOrderPageReqVO.java │ │ │ │ │ │ ├── StoreOrderRefundVO.java │ │ │ │ │ │ ├── StoreOrderRespVO.java │ │ │ │ │ │ └── StoreOrderUpdateReqVO.java │ │ │ │ │ └── app/ │ │ │ │ │ └── order/ │ │ │ │ │ ├── AppOrderController.java │ │ │ │ │ ├── param/ │ │ │ │ │ │ ├── AppComputeOrderParam.java │ │ │ │ │ │ ├── AppConfirmOrderParam.java │ │ │ │ │ │ ├── AppDoOrderParam.java │ │ │ │ │ │ ├── AppExpressParam.java │ │ │ │ │ │ ├── AppHandleOrderParam.java │ │ │ │ │ │ ├── AppOrderParam.java │ │ │ │ │ │ ├── AppPayParam.java │ │ │ │ │ │ ├── AppProductReplyParam.java │ │ │ │ │ │ └── AppRefundParam.java │ │ │ │ │ └── vo/ │ │ │ │ │ ├── AppComputeVo.java │ │ │ │ │ ├── AppConfirmOrderVo.java │ │ │ │ │ └── AppStoreOrderQueryVo.java │ │ │ │ ├── convert/ │ │ │ │ │ ├── storeorder/ │ │ │ │ │ │ └── StoreOrderConvert.java │ │ │ │ │ ├── storeordercartinfo/ │ │ │ │ │ │ └── StoreOrderCartInfoConvert.java │ │ │ │ │ └── storeorderstatus/ │ │ │ │ │ └── StoreOrderStatusConvert.java │ │ │ │ ├── dal/ │ │ │ │ │ ├── dataobject/ │ │ │ │ │ │ ├── ordernumber/ │ │ │ │ │ │ │ └── OrderNumberDO.java │ │ │ │ │ │ ├── storeorder/ │ │ │ │ │ │ │ └── StoreOrderDO.java │ │ │ │ │ │ ├── storeordercartinfo/ │ │ │ │ │ │ │ └── StoreOrderCartInfoDO.java │ │ │ │ │ │ └── storeorderstatus/ │ │ │ │ │ │ └── StoreOrderStatusDO.java │ │ │ │ │ ├── mysql/ │ │ │ │ │ │ ├── ordernumber/ │ │ │ │ │ │ │ └── OrderNumberMapper.java │ │ │ │ │ │ ├── storeorder/ │ │ │ │ │ │ │ └── StoreOrderMapper.java │ │ │ │ │ │ ├── storeordercartinfo/ │ │ │ │ │ │ │ └── StoreOrderCartInfoMapper.java │ │ │ │ │ │ └── storeorderstatus/ │ │ │ │ │ │ └── StoreOrderStatusMapper.java │ │ │ │ │ └── redis/ │ │ │ │ │ ├── RedisKeyConstants.java │ │ │ │ │ ├── ofterorder/ │ │ │ │ │ │ └── AfterOrderRedisDAO.java │ │ │ │ │ └── order/ │ │ │ │ │ ├── AsyncCountRedisDAO.java │ │ │ │ │ ├── AsyncOrderRedisDAO.java │ │ │ │ │ ├── OrderRedisDAO.java │ │ │ │ │ └── PrintMechinRedisDAO.java │ │ │ │ ├── handle/ │ │ │ │ │ ├── OrderAutoConfirmListener.java │ │ │ │ │ ├── OrderUnPayListener.java │ │ │ │ │ └── RedisDelayHandle.java │ │ │ │ ├── mq/ │ │ │ │ │ └── consumer/ │ │ │ │ │ └── PayNoticeConsumer.java │ │ │ │ └── service/ │ │ │ │ ├── storeorder/ │ │ │ │ │ ├── AppStoreOrderService.java │ │ │ │ │ ├── AppStoreOrderServiceImpl.java │ │ │ │ │ ├── AsynStoreOrderServiceImpl.java │ │ │ │ │ ├── AsyncStoreOrderService.java │ │ │ │ │ ├── StoreOrderService.java │ │ │ │ │ ├── StoreOrderServiceImpl.java │ │ │ │ │ └── dto/ │ │ │ │ │ ├── CacheDto.java │ │ │ │ │ ├── ChartDataDto.java │ │ │ │ │ ├── CountDto.java │ │ │ │ │ ├── OrderCountDto.java │ │ │ │ │ ├── OrderExtendDto.java │ │ │ │ │ ├── OrderTimeDataDto.java │ │ │ │ │ ├── OtherDto.java │ │ │ │ │ ├── PriceGroupDto.java │ │ │ │ │ ├── ProductAttrDto.java │ │ │ │ │ ├── ProductDto.java │ │ │ │ │ ├── StatusDto.java │ │ │ │ │ ├── StoreOrderCartInfoDto.java │ │ │ │ │ ├── TemplateDto.java │ │ │ │ │ ├── YxExpressDto.java │ │ │ │ │ ├── YxOrderNowOrderStatusDto.java │ │ │ │ │ ├── YxStoreOrderCartInfoDto.java │ │ │ │ │ └── YxStoreOrderStatusDto.java │ │ │ │ ├── storeordercartinfo/ │ │ │ │ │ ├── StoreOrderCartInfoService.java │ │ │ │ │ └── StoreOrderCartInfoServiceImpl.java │ │ │ │ └── storeorderstatus/ │ │ │ │ ├── StoreOrderStatusService.java │ │ │ │ └── StoreOrderStatusServiceImpl.java │ │ │ └── resources/ │ │ │ └── mapper/ │ │ │ ├── express/ │ │ │ │ └── ExpressMapper.xml │ │ │ ├── storeorder/ │ │ │ │ └── StoreOrderMapper.xml │ │ │ ├── storeordercartinfo/ │ │ │ │ └── StoreOrderCartInfoMapper.xml │ │ │ └── storeorderstatus/ │ │ │ └── StoreOrderStatusMapper.xml │ │ ├── yshop-module-product-api/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── product/ │ │ │ ├── api/ │ │ │ │ ├── package-info.java │ │ │ │ ├── product/ │ │ │ │ │ └── ProductApi.java │ │ │ │ └── property/ │ │ │ │ ├── ProductPropertyValueApi.java │ │ │ │ └── dto/ │ │ │ │ └── ProductPropertyValueDetailRespDTO.java │ │ │ └── enums/ │ │ │ ├── ErrorCodeConstants.java │ │ │ ├── ProductConstants.java │ │ │ ├── comment/ │ │ │ │ └── ProductCommentAuditStatusEnum.java │ │ │ ├── delivery/ │ │ │ │ └── DeliveryTypeEnum.java │ │ │ ├── group/ │ │ │ │ └── ProductGroupStyleEnum.java │ │ │ ├── product/ │ │ │ │ ├── DefaultEnum.java │ │ │ │ ├── ProductEnum.java │ │ │ │ ├── ProductTypeEnum.java │ │ │ │ ├── RelationCateEnum.java │ │ │ │ ├── RelationEnum.java │ │ │ │ ├── ScoreEnum.java │ │ │ │ └── SpecTypeEnum.java │ │ │ └── spu/ │ │ │ ├── ProductSpuSpecTypeEnum.java │ │ │ └── ProductSpuStatusEnum.java │ │ ├── yshop-module-product-biz/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── co/ │ │ │ │ └── yixiang/ │ │ │ │ └── yshop/ │ │ │ │ └── module/ │ │ │ │ └── product/ │ │ │ │ ├── api/ │ │ │ │ │ ├── package-info.java │ │ │ │ │ └── product/ │ │ │ │ │ └── ProductApiImpl.java │ │ │ │ ├── controller/ │ │ │ │ │ ├── admin/ │ │ │ │ │ │ ├── category/ │ │ │ │ │ │ │ ├── ProductCategoryController.java │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ ├── ProductCategoryBaseVO.java │ │ │ │ │ │ │ ├── ProductCategoryCreateReqVO.java │ │ │ │ │ │ │ ├── ProductCategoryListReqVO.java │ │ │ │ │ │ │ ├── ProductCategoryRespVO.java │ │ │ │ │ │ │ └── ProductCategoryUpdateReqVO.java │ │ │ │ │ │ ├── storeproduct/ │ │ │ │ │ │ │ ├── StoreProductController.java │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ ├── StoreProductBaseVO.java │ │ │ │ │ │ │ ├── StoreProductCreateReqVO.java │ │ │ │ │ │ │ ├── StoreProductExcelVO.java │ │ │ │ │ │ │ ├── StoreProductExportReqVO.java │ │ │ │ │ │ │ ├── StoreProductPageReqVO.java │ │ │ │ │ │ │ ├── StoreProductRespVO.java │ │ │ │ │ │ │ └── StoreProductUpdateReqVO.java │ │ │ │ │ │ ├── storeproductreply/ │ │ │ │ │ │ │ ├── StoreProductReplyController.java │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ ├── StoreProductReplyBaseVO.java │ │ │ │ │ │ │ ├── StoreProductReplyPageReqVO.java │ │ │ │ │ │ │ ├── StoreProductReplyRespVO.java │ │ │ │ │ │ │ └── StoreProductReplyUpdateReqVO.java │ │ │ │ │ │ └── storeproductrule/ │ │ │ │ │ │ ├── StoreProductRuleController.java │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ ├── StoreProductRuleBaseVO.java │ │ │ │ │ │ ├── StoreProductRuleCreateReqVO.java │ │ │ │ │ │ ├── StoreProductRuleExcelVO.java │ │ │ │ │ │ ├── StoreProductRuleExportReqVO.java │ │ │ │ │ │ ├── StoreProductRulePageReqVO.java │ │ │ │ │ │ ├── StoreProductRuleRespVO.java │ │ │ │ │ │ └── StoreProductRuleUpdateReqVO.java │ │ │ │ │ └── app/ │ │ │ │ │ ├── cart/ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ └── AppStoreCartQueryVo.java │ │ │ │ │ ├── category/ │ │ │ │ │ │ ├── AppCategoryController.java │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ └── AppCategoryRespVO.java │ │ │ │ │ └── product/ │ │ │ │ │ ├── AppStoreProductController.java │ │ │ │ │ ├── param/ │ │ │ │ │ │ └── AppStoreProductQueryParam.java │ │ │ │ │ └── vo/ │ │ │ │ │ ├── AppIndexVo.java │ │ │ │ │ ├── AppProductVo.java │ │ │ │ │ ├── AppReplyCountVo.java │ │ │ │ │ ├── AppStoreProductAttrQueryVo.java │ │ │ │ │ ├── AppStoreProductReplyQueryVo.java │ │ │ │ │ └── AppStoreProductRespVo.java │ │ │ │ ├── convert/ │ │ │ │ │ ├── category/ │ │ │ │ │ │ └── ProductCategoryConvert.java │ │ │ │ │ ├── storeproduct/ │ │ │ │ │ │ └── StoreProductConvert.java │ │ │ │ │ ├── storeproductattr/ │ │ │ │ │ │ └── StoreProductAttrConvert.java │ │ │ │ │ ├── storeproductattrresult/ │ │ │ │ │ │ └── StoreProductAttrResultConvert.java │ │ │ │ │ ├── storeproductattrvalue/ │ │ │ │ │ │ └── StoreProductAttrValueConvert.java │ │ │ │ │ ├── storeproductreply/ │ │ │ │ │ │ └── StoreProductReplyConvert.java │ │ │ │ │ └── storeproductrule/ │ │ │ │ │ └── StoreProductRuleConvert.java │ │ │ │ ├── dal/ │ │ │ │ │ ├── dataobject/ │ │ │ │ │ │ ├── category/ │ │ │ │ │ │ │ └── ProductCategoryDO.java │ │ │ │ │ │ ├── storeproduct/ │ │ │ │ │ │ │ └── StoreProductDO.java │ │ │ │ │ │ ├── storeproductattr/ │ │ │ │ │ │ │ └── StoreProductAttrDO.java │ │ │ │ │ │ ├── storeproductattrresult/ │ │ │ │ │ │ │ └── StoreProductAttrResultDO.java │ │ │ │ │ │ ├── storeproductattrvalue/ │ │ │ │ │ │ │ └── StoreProductAttrValueDO.java │ │ │ │ │ │ ├── storeproductreply/ │ │ │ │ │ │ │ └── StoreProductReplyDO.java │ │ │ │ │ │ └── storeproductrule/ │ │ │ │ │ │ └── StoreProductRuleDO.java │ │ │ │ │ └── mysql/ │ │ │ │ │ ├── category/ │ │ │ │ │ │ └── ProductCategoryMapper.java │ │ │ │ │ ├── storeproduct/ │ │ │ │ │ │ └── StoreProductMapper.java │ │ │ │ │ ├── storeproductattr/ │ │ │ │ │ │ └── StoreProductAttrMapper.java │ │ │ │ │ ├── storeproductattrresult/ │ │ │ │ │ │ └── StoreProductAttrResultMapper.java │ │ │ │ │ ├── storeproductattrvalue/ │ │ │ │ │ │ └── StoreProductAttrValueMapper.java │ │ │ │ │ ├── storeproductreply/ │ │ │ │ │ │ └── StoreProductReplyMapper.java │ │ │ │ │ └── storeproductrule/ │ │ │ │ │ └── StoreProductRuleMapper.java │ │ │ │ ├── framework/ │ │ │ │ │ ├── package-info.java │ │ │ │ │ └── web/ │ │ │ │ │ ├── config/ │ │ │ │ │ │ └── ProductWebConfiguration.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── package-info.java │ │ │ │ └── service/ │ │ │ │ ├── category/ │ │ │ │ │ ├── ProductCategoryService.java │ │ │ │ │ └── ProductCategoryServiceImpl.java │ │ │ │ ├── storeproduct/ │ │ │ │ │ ├── AppStoreProductService.java │ │ │ │ │ ├── AppStoreProductServiceImpl.java │ │ │ │ │ ├── StoreProductService.java │ │ │ │ │ ├── StoreProductServiceImpl.java │ │ │ │ │ └── dto/ │ │ │ │ │ ├── AttrValueDto.java │ │ │ │ │ ├── DetailDto.java │ │ │ │ │ ├── FromatDetailDto.java │ │ │ │ │ ├── ProductDto.java │ │ │ │ │ ├── ProductFormatDto.java │ │ │ │ │ ├── ProductResultDto.java │ │ │ │ │ ├── StoreProductDto.java │ │ │ │ │ ├── YxStoreProductDto.java │ │ │ │ │ ├── YxStoreProductRelationDto.java │ │ │ │ │ ├── YxStoreProductReplyDto.java │ │ │ │ │ ├── YxStoreProductRuleDto.java │ │ │ │ │ └── YxStoreProductSmallDto.java │ │ │ │ ├── storeproductattr/ │ │ │ │ │ ├── AppStoreProductAttrService.java │ │ │ │ │ ├── AppStoreProductAttrServiceImpl.java │ │ │ │ │ ├── StoreProductAttrService.java │ │ │ │ │ └── StoreProductAttrServiceImpl.java │ │ │ │ ├── storeproductattrresult/ │ │ │ │ │ ├── StoreProductAttrResultService.java │ │ │ │ │ └── StoreProductAttrResultServiceImpl.java │ │ │ │ ├── storeproductattrvalue/ │ │ │ │ │ ├── StoreProductAttrValueService.java │ │ │ │ │ └── StoreProductAttrValueServiceImpl.java │ │ │ │ ├── storeproductreply/ │ │ │ │ │ ├── AppStoreProductReplyService.java │ │ │ │ │ ├── AppStoreProductReplyServiceImpl.java │ │ │ │ │ ├── StoreProductReplyService.java │ │ │ │ │ └── StoreProductReplyServiceImpl.java │ │ │ │ └── storeproductrule/ │ │ │ │ ├── StoreProductRuleService.java │ │ │ │ └── StoreProductRuleServiceImpl.java │ │ │ └── resources/ │ │ │ └── mapper/ │ │ │ ├── shippingtemplates/ │ │ │ │ └── ShippingTemplatesMapper.xml │ │ │ ├── shippingtemplatesfree/ │ │ │ │ └── ShippingTemplatesFreeMapper.xml │ │ │ ├── shippingtemplatesregion/ │ │ │ │ └── ShippingTemplatesRegionMapper.xml │ │ │ ├── storeproduct/ │ │ │ │ └── StoreProductMapper.xml │ │ │ ├── storeproductattr/ │ │ │ │ └── StoreProductAttrMapper.xml │ │ │ ├── storeproductattrresult/ │ │ │ │ └── StoreProductAttrResultMapper.xml │ │ │ ├── storeproductattrvalue/ │ │ │ │ └── StoreProductAttrValueMapper.xml │ │ │ ├── storeproductrelation/ │ │ │ │ └── StoreProductRelationMapper.xml │ │ │ ├── storeproductreply/ │ │ │ │ └── StoreProductReplyMapper.xml │ │ │ └── storeproductrule/ │ │ │ └── StoreProductRuleMapper.xml │ │ ├── yshop-module-shop-api/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── shop/ │ │ │ └── enums/ │ │ │ └── ErrorCodeConstants.java │ │ ├── yshop-module-shop-biz/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ ├── java/ │ │ │ │ │ └── co/ │ │ │ │ │ └── yixiang/ │ │ │ │ │ └── yshop/ │ │ │ │ │ └── module/ │ │ │ │ │ └── shop/ │ │ │ │ │ ├── controller/ │ │ │ │ │ │ ├── admin/ │ │ │ │ │ │ │ ├── material/ │ │ │ │ │ │ │ │ ├── MaterialController.java │ │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ │ ├── MaterialBaseVO.java │ │ │ │ │ │ │ │ ├── MaterialCreateReqVO.java │ │ │ │ │ │ │ │ ├── MaterialExcelVO.java │ │ │ │ │ │ │ │ ├── MaterialExportReqVO.java │ │ │ │ │ │ │ │ ├── MaterialPageReqVO.java │ │ │ │ │ │ │ │ ├── MaterialRespVO.java │ │ │ │ │ │ │ │ └── MaterialUpdateReqVO.java │ │ │ │ │ │ │ ├── materialgroup/ │ │ │ │ │ │ │ │ ├── MaterialGroupController.java │ │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ │ ├── MaterialGroupBaseVO.java │ │ │ │ │ │ │ │ ├── MaterialGroupCreateReqVO.java │ │ │ │ │ │ │ │ ├── MaterialGroupExcelVO.java │ │ │ │ │ │ │ │ ├── MaterialGroupExportReqVO.java │ │ │ │ │ │ │ │ ├── MaterialGroupPageReqVO.java │ │ │ │ │ │ │ │ ├── MaterialGroupRespVO.java │ │ │ │ │ │ │ │ └── MaterialGroupUpdateReqVO.java │ │ │ │ │ │ │ ├── service/ │ │ │ │ │ │ │ │ ├── ServiceController.java │ │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ │ ├── ServiceBaseVO.java │ │ │ │ │ │ │ │ ├── ServiceCreateReqVO.java │ │ │ │ │ │ │ │ ├── ServiceExcelVO.java │ │ │ │ │ │ │ │ ├── ServiceExportReqVO.java │ │ │ │ │ │ │ │ ├── ServicePageReqVO.java │ │ │ │ │ │ │ │ ├── ServiceRespVO.java │ │ │ │ │ │ │ │ └── ServiceUpdateReqVO.java │ │ │ │ │ │ │ └── shopads/ │ │ │ │ │ │ │ ├── ShopAdsController.java │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ ├── ShopAdsBaseVO.java │ │ │ │ │ │ │ ├── ShopAdsCreateReqVO.java │ │ │ │ │ │ │ ├── ShopAdsExcelVO.java │ │ │ │ │ │ │ ├── ShopAdsExportReqVO.java │ │ │ │ │ │ │ ├── ShopAdsPageReqVO.java │ │ │ │ │ │ │ ├── ShopAdsRespVO.java │ │ │ │ │ │ │ └── ShopAdsUpdateReqVO.java │ │ │ │ │ │ └── app/ │ │ │ │ │ │ ├── ad/ │ │ │ │ │ │ │ ├── AppAdController.java │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ └── AppShopAdsVO.java │ │ │ │ │ │ └── service/ │ │ │ │ │ │ ├── AppServiceController.java │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ └── AppServiceVO.java │ │ │ │ │ ├── convert/ │ │ │ │ │ │ ├── material/ │ │ │ │ │ │ │ └── MaterialConvert.java │ │ │ │ │ │ ├── materialgroup/ │ │ │ │ │ │ │ └── MaterialGroupConvert.java │ │ │ │ │ │ ├── service/ │ │ │ │ │ │ │ └── ServiceConvert.java │ │ │ │ │ │ └── shopads/ │ │ │ │ │ │ └── ShopAdsConvert.java │ │ │ │ │ ├── dal/ │ │ │ │ │ │ ├── dataobject/ │ │ │ │ │ │ │ ├── material/ │ │ │ │ │ │ │ │ └── MaterialDO.java │ │ │ │ │ │ │ ├── materialgroup/ │ │ │ │ │ │ │ │ └── MaterialGroupDO.java │ │ │ │ │ │ │ ├── service/ │ │ │ │ │ │ │ │ └── ServiceDO.java │ │ │ │ │ │ │ └── shopads/ │ │ │ │ │ │ │ └── ShopAdsDO.java │ │ │ │ │ │ └── mysql/ │ │ │ │ │ │ ├── material/ │ │ │ │ │ │ │ └── MaterialMapper.java │ │ │ │ │ │ ├── materialgroup/ │ │ │ │ │ │ │ └── MaterialGroupMapper.java │ │ │ │ │ │ ├── service/ │ │ │ │ │ │ │ └── ServiceMapper.java │ │ │ │ │ │ └── shopads/ │ │ │ │ │ │ └── ShopAdsMapper.java │ │ │ │ │ ├── framework/ │ │ │ │ │ │ ├── package-info.java │ │ │ │ │ │ └── web/ │ │ │ │ │ │ ├── config/ │ │ │ │ │ │ │ └── ShopWebConfiguration.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ └── service/ │ │ │ │ │ ├── material/ │ │ │ │ │ │ ├── MaterialService.java │ │ │ │ │ │ └── MaterialServiceImpl.java │ │ │ │ │ ├── materialgroup/ │ │ │ │ │ │ ├── MaterialGroupService.java │ │ │ │ │ │ └── MaterialGroupServiceImpl.java │ │ │ │ │ ├── service/ │ │ │ │ │ │ ├── AppServiceService.java │ │ │ │ │ │ ├── AppServiceServiceImpl.java │ │ │ │ │ │ ├── ServiceService.java │ │ │ │ │ │ └── ServiceServiceImpl.java │ │ │ │ │ └── shopads/ │ │ │ │ │ ├── AppShopAdsService.java │ │ │ │ │ ├── AppShopAdsServiceImpl.java │ │ │ │ │ ├── ShopAdsService.java │ │ │ │ │ └── ShopAdsServiceImpl.java │ │ │ │ └── resources/ │ │ │ │ └── mapper/ │ │ │ │ ├── material/ │ │ │ │ │ └── MaterialMapper.xml │ │ │ │ └── materialgroup/ │ │ │ │ └── MaterialGroupMapper.xml │ │ │ └── test/ │ │ │ └── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── shop/ │ │ │ └── service/ │ │ │ └── material/ │ │ │ └── MaterialServiceImplTest.java │ │ ├── yshop-module-store-api/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── store/ │ │ │ └── enums/ │ │ │ ├── ErrorCodeConstants.java │ │ │ └── WithdrawalStatusEnum.java │ │ └── yshop-module-store-biz/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── store/ │ │ │ ├── controller/ │ │ │ │ ├── admin/ │ │ │ │ │ └── storeshop/ │ │ │ │ │ ├── StoreShopController.java │ │ │ │ │ └── vo/ │ │ │ │ │ ├── StoreShopBaseVO.java │ │ │ │ │ ├── StoreShopCreateReqVO.java │ │ │ │ │ ├── StoreShopExcelVO.java │ │ │ │ │ ├── StoreShopExportReqVO.java │ │ │ │ │ ├── StoreShopPageReqVO.java │ │ │ │ │ ├── StoreShopRespVO.java │ │ │ │ │ └── StoreShopUpdateReqVO.java │ │ │ │ └── app/ │ │ │ │ └── storeshop/ │ │ │ │ ├── AppStoreController.java │ │ │ │ └── vo/ │ │ │ │ └── AppStoreShopVO.java │ │ │ ├── convert/ │ │ │ │ └── storeshop/ │ │ │ │ └── StoreShopConvert.java │ │ │ ├── dal/ │ │ │ │ ├── dataobject/ │ │ │ │ │ └── storeshop/ │ │ │ │ │ └── StoreShopDO.java │ │ │ │ ├── mysql/ │ │ │ │ │ └── storeshop/ │ │ │ │ │ └── StoreShopMapper.java │ │ │ │ └── redis/ │ │ │ │ ├── PrintTokenRedisDAO.java │ │ │ │ └── RedisKeyConstants.java │ │ │ └── service/ │ │ │ └── storeshop/ │ │ │ ├── AppStoreShopService.java │ │ │ ├── AppStoreShopServiceImpl.java │ │ │ ├── StoreShopService.java │ │ │ └── StoreShopServiceImpl.java │ │ └── resources/ │ │ └── mapper/ │ │ └── storeshop/ │ │ └── StoreShopMapper.xml │ ├── yshop-module-marketing/ │ │ ├── pom.xml │ │ ├── yshop-module-coupon-api/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── coupon/ │ │ │ └── enums/ │ │ │ ├── CouponStatusEnum.java │ │ │ └── ErrorCodeConstants.java │ │ └── yshop-module-coupon-biz/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── coupon/ │ │ │ ├── controller/ │ │ │ │ ├── admin/ │ │ │ │ │ ├── coupon/ │ │ │ │ │ │ ├── CouponController.java │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ ├── CouponBaseVO.java │ │ │ │ │ │ ├── CouponCreateReqVO.java │ │ │ │ │ │ ├── CouponExcelVO.java │ │ │ │ │ │ ├── CouponExportReqVO.java │ │ │ │ │ │ ├── CouponPageReqVO.java │ │ │ │ │ │ ├── CouponRespVO.java │ │ │ │ │ │ └── CouponUpdateReqVO.java │ │ │ │ │ └── couponuser/ │ │ │ │ │ ├── CouponUserController.java │ │ │ │ │ └── vo/ │ │ │ │ │ ├── CouponUserBaseVO.java │ │ │ │ │ ├── CouponUserCreateReqVO.java │ │ │ │ │ ├── CouponUserExcelVO.java │ │ │ │ │ ├── CouponUserExportReqVO.java │ │ │ │ │ ├── CouponUserPageReqVO.java │ │ │ │ │ ├── CouponUserRespVO.java │ │ │ │ │ └── CouponUserUpdateReqVO.java │ │ │ │ └── app/ │ │ │ │ └── coupon/ │ │ │ │ ├── AppCouponController.java │ │ │ │ └── vo/ │ │ │ │ ├── AppCouponVO.java │ │ │ │ ├── AppMyCouponVO.java │ │ │ │ └── AppReceVO.java │ │ │ ├── convert/ │ │ │ │ ├── coupon/ │ │ │ │ │ └── CouponConvert.java │ │ │ │ └── couponuser/ │ │ │ │ └── CouponUserConvert.java │ │ │ ├── dal/ │ │ │ │ ├── dataobject/ │ │ │ │ │ ├── coupon/ │ │ │ │ │ │ └── CouponDO.java │ │ │ │ │ └── couponuser/ │ │ │ │ │ └── CouponUserDO.java │ │ │ │ └── mysql/ │ │ │ │ ├── coupon/ │ │ │ │ │ └── CouponMapper.java │ │ │ │ └── couponuser/ │ │ │ │ └── CouponUserMapper.java │ │ │ └── service/ │ │ │ ├── coupon/ │ │ │ │ ├── AppCouponService.java │ │ │ │ ├── AppCouponServiceImpl.java │ │ │ │ ├── CouponService.java │ │ │ │ └── CouponServiceImpl.java │ │ │ └── couponuser/ │ │ │ ├── AppCouponUserService.java │ │ │ ├── AppCouponUserServiceImpl.java │ │ │ ├── CouponUserService.java │ │ │ └── CouponUserServiceImpl.java │ │ └── resources/ │ │ └── mapper/ │ │ └── coupon/ │ │ └── CouponMapper.xml │ ├── yshop-module-member/ │ │ ├── pom.xml │ │ ├── yshop-module-member-api/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── member/ │ │ │ ├── api/ │ │ │ │ ├── package-info.java │ │ │ │ └── user/ │ │ │ │ ├── MemberUserApi.java │ │ │ │ └── dto/ │ │ │ │ ├── MemberUserRespDTO.java │ │ │ │ └── WechatUserDto.java │ │ │ └── enums/ │ │ │ ├── BillDetailEnum.java │ │ │ ├── BillEnum.java │ │ │ ├── ErrorCodeConstants.java │ │ │ └── LoginTypeEnum.java │ │ └── yshop-module-member-biz/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── member/ │ │ │ ├── api/ │ │ │ │ ├── package-info.java │ │ │ │ └── user/ │ │ │ │ └── MemberUserApiImpl.java │ │ │ ├── controller/ │ │ │ │ ├── admin/ │ │ │ │ │ ├── user/ │ │ │ │ │ │ ├── MemberUserController.java │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ ├── UserBaseVO.java │ │ │ │ │ │ ├── UserCreateReqVO.java │ │ │ │ │ │ ├── UserExportReqVO.java │ │ │ │ │ │ ├── UserPageReqVO.java │ │ │ │ │ │ ├── UserRespVO.java │ │ │ │ │ │ ├── UserUpdateMoneyReqVO.java │ │ │ │ │ │ └── UserUpdateReqVO.java │ │ │ │ │ ├── useraddress/ │ │ │ │ │ │ ├── UserAddressController.java │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ ├── UserAddressBaseVO.java │ │ │ │ │ │ ├── UserAddressCreateReqVO.java │ │ │ │ │ │ ├── UserAddressExportReqVO.java │ │ │ │ │ │ ├── UserAddressPageReqVO.java │ │ │ │ │ │ ├── UserAddressRespVO.java │ │ │ │ │ │ └── UserAddressUpdateReqVO.java │ │ │ │ │ └── userbill/ │ │ │ │ │ ├── UserBillController.java │ │ │ │ │ └── vo/ │ │ │ │ │ ├── UserBillBaseVO.java │ │ │ │ │ ├── UserBillCreateReqVO.java │ │ │ │ │ ├── UserBillExportReqVO.java │ │ │ │ │ ├── UserBillPageReqVO.java │ │ │ │ │ ├── UserBillRespVO.java │ │ │ │ │ └── UserBillUpdateReqVO.java │ │ │ │ └── app/ │ │ │ │ ├── address/ │ │ │ │ │ ├── AppUserAddressController.java │ │ │ │ │ ├── param/ │ │ │ │ │ │ ├── AddressDetailParam.java │ │ │ │ │ │ └── AppAddressParam.java │ │ │ │ │ └── vo/ │ │ │ │ │ ├── AppUserAddressLocationVo.java │ │ │ │ │ ├── AppUserAddressQueryVo.java │ │ │ │ │ └── AreaNodeRespVO.java │ │ │ │ ├── auth/ │ │ │ │ │ ├── AppAuthController.java │ │ │ │ │ └── vo/ │ │ │ │ │ ├── AppAuthCheckCodeReqVO.java │ │ │ │ │ ├── AppAuthLoginReqVO.java │ │ │ │ │ ├── AppAuthLoginRespVO.java │ │ │ │ │ ├── AppAuthResetPasswordReqVO.java │ │ │ │ │ ├── AppAuthSmsLoginReqVO.java │ │ │ │ │ ├── AppAuthSmsSendReqVO.java │ │ │ │ │ ├── AppAuthSocialLoginReqVO.java │ │ │ │ │ ├── AppAuthUpdatePasswordReqVO.java │ │ │ │ │ ├── AppAuthWeixinMiniAppLoginReqVO.java │ │ │ │ │ ├── AppWeixinMiniLoginVO.java │ │ │ │ │ └── AppWxMiniLoginVO.java │ │ │ │ ├── social/ │ │ │ │ │ ├── AppSocialUserController.java │ │ │ │ │ └── vo/ │ │ │ │ │ ├── AppSocialUserBindReqVO.java │ │ │ │ │ └── AppSocialUserUnbindReqVO.java │ │ │ │ ├── user/ │ │ │ │ │ ├── AppUserController.java │ │ │ │ │ └── vo/ │ │ │ │ │ ├── AppUserBillVO.java │ │ │ │ │ ├── AppUserInfoRespVO.java │ │ │ │ │ ├── AppUserNickVO.java │ │ │ │ │ ├── AppUserOrderCountVo.java │ │ │ │ │ ├── AppUserQueryVo.java │ │ │ │ │ ├── AppUserRechargeVO.java │ │ │ │ │ └── AppUserUpdateMobileReqVO.java │ │ │ │ └── weixin/ │ │ │ │ └── AppWxMpController.java │ │ │ ├── convert/ │ │ │ │ ├── auth/ │ │ │ │ │ └── AuthConvert.java │ │ │ │ ├── package-info.java │ │ │ │ ├── social/ │ │ │ │ │ └── SocialUserConvert.java │ │ │ │ ├── user/ │ │ │ │ │ └── UserConvert.java │ │ │ │ ├── useraddress/ │ │ │ │ │ ├── AreaConvert.java │ │ │ │ │ └── UserAddressConvert.java │ │ │ │ └── userbill/ │ │ │ │ └── UserBillConvert.java │ │ │ ├── dal/ │ │ │ │ ├── dataobject/ │ │ │ │ │ ├── user/ │ │ │ │ │ │ └── MemberUserDO.java │ │ │ │ │ ├── useraddress/ │ │ │ │ │ │ └── UserAddressDO.java │ │ │ │ │ └── userbill/ │ │ │ │ │ └── UserBillDO.java │ │ │ │ ├── mysql/ │ │ │ │ │ ├── user/ │ │ │ │ │ │ └── MemberUserMapper.java │ │ │ │ │ ├── useraddress/ │ │ │ │ │ │ └── UserAddressMapper.java │ │ │ │ │ └── userbill/ │ │ │ │ │ └── UserBillMapper.java │ │ │ │ └── redis/ │ │ │ │ ├── RedisKeyConstants.java │ │ │ │ └── order/ │ │ │ │ └── MiniRedisDAO.java │ │ │ ├── framework/ │ │ │ │ ├── package-info.java │ │ │ │ └── web/ │ │ │ │ ├── config/ │ │ │ │ │ └── MemberWebConfiguration.java │ │ │ │ └── package-info.java │ │ │ └── service/ │ │ │ ├── auth/ │ │ │ │ ├── MemberAuthService.java │ │ │ │ └── MemberAuthServiceImpl.java │ │ │ ├── user/ │ │ │ │ ├── MemberUserService.java │ │ │ │ ├── MemberUserServiceImpl.java │ │ │ │ ├── UserService.java │ │ │ │ ├── UserServiceImpl.java │ │ │ │ └── dto/ │ │ │ │ └── WechatUserDto.java │ │ │ ├── useraddress/ │ │ │ │ ├── AppUserAddressService.java │ │ │ │ ├── AppUserAddressServiceImpl.java │ │ │ │ ├── UserAddressService.java │ │ │ │ └── UserAddressServiceImpl.java │ │ │ └── userbill/ │ │ │ ├── UserBillService.java │ │ │ └── UserBillServiceImpl.java │ │ └── test/ │ │ ├── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── member/ │ │ │ └── service/ │ │ │ └── auth/ │ │ │ └── MemberAuthServiceTest.java │ │ └── resources/ │ │ ├── application-unit-test.yaml │ │ ├── logback.xml │ │ └── sql/ │ │ ├── clean.sql │ │ └── create_tables.sql │ ├── yshop-module-message/ │ │ ├── pom.xml │ │ ├── yshop-module-message-api/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── message/ │ │ │ └── enums/ │ │ │ ├── ErrorCodeConstants.java │ │ │ └── WechatTempateEnum.java │ │ └── yshop-module-message-biz/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── message/ │ │ │ ├── controller/ │ │ │ │ └── admin/ │ │ │ │ └── wechattemplate/ │ │ │ │ ├── WechatTemplateController.java │ │ │ │ └── vo/ │ │ │ │ ├── WechatTemplateBaseVO.java │ │ │ │ ├── WechatTemplateCreateReqVO.java │ │ │ │ ├── WechatTemplateExcelVO.java │ │ │ │ ├── WechatTemplateExportReqVO.java │ │ │ │ ├── WechatTemplatePageReqVO.java │ │ │ │ ├── WechatTemplateRespVO.java │ │ │ │ └── WechatTemplateUpdateReqVO.java │ │ │ ├── convert/ │ │ │ │ └── wechattemplate/ │ │ │ │ └── WechatTemplateConvert.java │ │ │ ├── dal/ │ │ │ │ ├── dataobject/ │ │ │ │ │ └── wechattemplate/ │ │ │ │ │ └── WechatTemplateDO.java │ │ │ │ └── mysql/ │ │ │ │ └── wechattemplate/ │ │ │ │ └── WechatTemplateMapper.java │ │ │ ├── mq/ │ │ │ │ ├── consumer/ │ │ │ │ │ └── WeixinNoticeConsumer.java │ │ │ │ ├── message/ │ │ │ │ │ └── WeixinNoticeMessage.java │ │ │ │ └── producer/ │ │ │ │ └── WeixinNoticeProducer.java │ │ │ ├── redismq/ │ │ │ │ ├── DelayedQueueListener.java │ │ │ │ ├── DelayedQueueListenerConfigurer.java │ │ │ │ ├── DelayedQueuePollTask.java │ │ │ │ └── msg/ │ │ │ │ └── OrderMsg.java │ │ │ ├── service/ │ │ │ │ └── wechattemplate/ │ │ │ │ ├── WechatTemplateService.java │ │ │ │ └── WechatTemplateServiceImpl.java │ │ │ └── supply/ │ │ │ ├── WeiXinSubscribeService.java │ │ │ └── WeixinTemplateService.java │ │ └── resources/ │ │ └── mapper/ │ │ └── wechattemplate/ │ │ └── WechatTemplateMapper.xml │ ├── yshop-module-mp/ │ │ ├── pom.xml │ │ ├── yshop-module-mp-api/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── mp/ │ │ │ └── enums/ │ │ │ ├── ErrorCodeConstants.java │ │ │ ├── MpAccountEnum.java │ │ │ └── message/ │ │ │ ├── MpAutoReplyMatchEnum.java │ │ │ ├── MpAutoReplyTypeEnum.java │ │ │ └── MpMessageSendFromEnum.java │ │ └── yshop-module-mp-biz/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── co/ │ │ └── yixiang/ │ │ └── yshop/ │ │ └── module/ │ │ └── mp/ │ │ ├── controller/ │ │ │ └── admin/ │ │ │ ├── account/ │ │ │ │ ├── MaAccountController.java │ │ │ │ ├── MpAccountController.java │ │ │ │ └── vo/ │ │ │ │ ├── MpAccountBaseVO.java │ │ │ │ ├── MpAccountCreateReqVO.java │ │ │ │ ├── MpAccountPageReqVO.java │ │ │ │ ├── MpAccountRespVO.java │ │ │ │ ├── MpAccountSimpleRespVO.java │ │ │ │ └── MpAccountUpdateReqVO.java │ │ │ ├── material/ │ │ │ │ ├── MpMaterialController.java │ │ │ │ └── vo/ │ │ │ │ ├── MpMaterialPageReqVO.java │ │ │ │ ├── MpMaterialRespVO.java │ │ │ │ ├── MpMaterialUploadNewsImageReqVO.java │ │ │ │ ├── MpMaterialUploadPermanentReqVO.java │ │ │ │ ├── MpMaterialUploadRespVO.java │ │ │ │ └── MpMaterialUploadTemporaryReqVO.java │ │ │ ├── menu/ │ │ │ │ ├── MpMenuController.java │ │ │ │ └── vo/ │ │ │ │ ├── MpMenuBaseVO.java │ │ │ │ ├── MpMenuRespVO.java │ │ │ │ └── MpMenuSaveReqVO.java │ │ │ ├── message/ │ │ │ │ ├── MpAutoReplyController.java │ │ │ │ ├── MpMessageController.java │ │ │ │ └── vo/ │ │ │ │ ├── autoreply/ │ │ │ │ │ ├── MpAutoReplyBaseVO.java │ │ │ │ │ ├── MpAutoReplyCreateReqVO.java │ │ │ │ │ ├── MpAutoReplyPageReqVO.java │ │ │ │ │ ├── MpAutoReplyRespVO.java │ │ │ │ │ └── MpAutoReplyUpdateReqVO.java │ │ │ │ └── message/ │ │ │ │ ├── MpMessagePageReqVO.java │ │ │ │ ├── MpMessageRespVO.java │ │ │ │ └── MpMessageSendReqVO.java │ │ │ ├── news/ │ │ │ │ ├── MpDraftController.java │ │ │ │ ├── MpFreePublishController.java │ │ │ │ └── vo/ │ │ │ │ ├── MpDraftPageReqVO.java │ │ │ │ └── MpFreePublishPageReqVO.java │ │ │ ├── open/ │ │ │ │ ├── MpOpenController.java │ │ │ │ └── vo/ │ │ │ │ ├── MpOpenCheckSignatureReqVO.java │ │ │ │ └── MpOpenHandleMessageReqVO.java │ │ │ ├── statistics/ │ │ │ │ ├── MpStatisticsController.java │ │ │ │ └── vo/ │ │ │ │ ├── MpStatisticsGetReqVO.java │ │ │ │ ├── MpStatisticsInterfaceSummaryRespVO.java │ │ │ │ ├── MpStatisticsUpstreamMessageRespVO.java │ │ │ │ ├── MpStatisticsUserCumulateRespVO.java │ │ │ │ └── MpStatisticsUserSummaryRespVO.java │ │ │ ├── tag/ │ │ │ │ ├── MpTagController.java │ │ │ │ └── vo/ │ │ │ │ ├── MpTagBaseVO.java │ │ │ │ ├── MpTagCreateReqVO.java │ │ │ │ ├── MpTagPageReqVO.java │ │ │ │ ├── MpTagRespVO.java │ │ │ │ ├── MpTagSimpleRespVO.java │ │ │ │ └── MpTagUpdateReqVO.java │ │ │ └── user/ │ │ │ ├── MpUserController.java │ │ │ └── vo/ │ │ │ ├── MpUserPageReqVO.java │ │ │ ├── MpUserRespVO.java │ │ │ └── MpUserUpdateReqVO.java │ │ ├── convert/ │ │ │ ├── account/ │ │ │ │ └── MpAccountConvert.java │ │ │ ├── material/ │ │ │ │ └── MpMaterialConvert.java │ │ │ ├── menu/ │ │ │ │ └── MpMenuConvert.java │ │ │ ├── message/ │ │ │ │ ├── MpAutoReplyConvert.java │ │ │ │ └── MpMessageConvert.java │ │ │ ├── statistics/ │ │ │ │ └── MpStatisticsConvert.java │ │ │ ├── tag/ │ │ │ │ └── MpTagConvert.java │ │ │ └── user/ │ │ │ └── MpUserConvert.java │ │ ├── dal/ │ │ │ ├── dataobject/ │ │ │ │ ├── account/ │ │ │ │ │ └── MpAccountDO.java │ │ │ │ ├── material/ │ │ │ │ │ └── MpMaterialDO.java │ │ │ │ ├── menu/ │ │ │ │ │ └── MpMenuDO.java │ │ │ │ ├── message/ │ │ │ │ │ ├── MpAutoReplyDO.java │ │ │ │ │ └── MpMessageDO.java │ │ │ │ ├── tag/ │ │ │ │ │ └── MpTagDO.java │ │ │ │ └── user/ │ │ │ │ └── MpUserDO.java │ │ │ └── mysql/ │ │ │ ├── account/ │ │ │ │ └── MpAccountMapper.java │ │ │ ├── material/ │ │ │ │ └── MpMaterialMapper.java │ │ │ ├── menu/ │ │ │ │ └── MpMenuMapper.java │ │ │ ├── message/ │ │ │ │ ├── MpAutoReplyMapper.java │ │ │ │ └── MpMessageMapper.java │ │ │ ├── tag/ │ │ │ │ └── MpTagMapper.java │ │ │ └── user/ │ │ │ └── MpUserMapper.java │ │ ├── framework/ │ │ │ ├── mp/ │ │ │ │ ├── config/ │ │ │ │ │ └── MpConfiguration.java │ │ │ │ └── core/ │ │ │ │ ├── DefaultMpServiceFactory.java │ │ │ │ ├── MpServiceFactory.java │ │ │ │ ├── context/ │ │ │ │ │ └── MpContextHolder.java │ │ │ │ └── util/ │ │ │ │ └── MpUtils.java │ │ │ └── web/ │ │ │ └── config/ │ │ │ └── MpWebConfiguration.java │ │ └── service/ │ │ ├── account/ │ │ │ ├── MpAccountService.java │ │ │ └── MpAccountServiceImpl.java │ │ ├── handler/ │ │ │ ├── menu/ │ │ │ │ └── MenuHandler.java │ │ │ ├── message/ │ │ │ │ ├── MessageAutoReplyHandler.java │ │ │ │ └── MessageReceiveHandler.java │ │ │ ├── other/ │ │ │ │ ├── KfSessionHandler.java │ │ │ │ ├── NullHandler.java │ │ │ │ ├── ScanHandler.java │ │ │ │ ├── StoreCheckNotifyHandler.java │ │ │ │ └── package-info.java │ │ │ └── user/ │ │ │ ├── LocationHandler.java │ │ │ ├── SubscribeHandler.java │ │ │ └── UnsubscribeHandler.java │ │ ├── material/ │ │ │ ├── MpMaterialService.java │ │ │ └── MpMaterialServiceImpl.java │ │ ├── menu/ │ │ │ ├── MpMenuService.java │ │ │ └── MpMenuServiceImpl.java │ │ ├── message/ │ │ │ ├── MpAutoReplyService.java │ │ │ ├── MpAutoReplyServiceImpl.java │ │ │ ├── MpMessageService.java │ │ │ ├── MpMessageServiceImpl.java │ │ │ └── bo/ │ │ │ └── MpMessageSendOutReqBO.java │ │ ├── statistics/ │ │ │ ├── MpStatisticsService.java │ │ │ └── MpStatisticsServiceImpl.java │ │ ├── tag/ │ │ │ ├── MpTagService.java │ │ │ └── MpTagServiceImpl.java │ │ └── user/ │ │ ├── MpUserService.java │ │ └── MpUserServiceImpl.java │ ├── yshop-module-pay/ │ │ ├── pom.xml │ │ ├── yshop-module-pay-api/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── pay/ │ │ │ ├── config/ │ │ │ │ ├── MerchantPayServiceConfigurer.java │ │ │ │ ├── PayAutoConfiguration.java │ │ │ │ ├── handlers/ │ │ │ │ │ ├── AliPayMessageHandler.java │ │ │ │ │ └── WxPayMessageHandler.java │ │ │ │ └── interceptor/ │ │ │ │ ├── AliPayMessageInterceptor.java │ │ │ │ └── WxPayMessageInterceptor.java │ │ │ ├── enums/ │ │ │ │ └── ErrorCodeConstants.java │ │ │ └── mq/ │ │ │ ├── message/ │ │ │ │ └── PayNoticeMessage.java │ │ │ └── producer/ │ │ │ └── PayNoticeProducer.java │ │ └── yshop-module-pay-biz/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── pay/ │ │ │ ├── controller/ │ │ │ │ └── admin/ │ │ │ │ └── merchantdetails/ │ │ │ │ ├── MerchantDetailsController.java │ │ │ │ └── vo/ │ │ │ │ ├── MerchantDetailsBaseVO.java │ │ │ │ ├── MerchantDetailsCreateReqVO.java │ │ │ │ ├── MerchantDetailsExcelVO.java │ │ │ │ ├── MerchantDetailsExportReqVO.java │ │ │ │ ├── MerchantDetailsPageReqVO.java │ │ │ │ ├── MerchantDetailsRespVO.java │ │ │ │ └── MerchantDetailsUpdateReqVO.java │ │ │ ├── convert/ │ │ │ │ └── merchantdetails/ │ │ │ │ └── MerchantDetailsConvert.java │ │ │ ├── dal/ │ │ │ │ ├── dataobject/ │ │ │ │ │ └── merchantdetails/ │ │ │ │ │ └── MerchantDetailsDO.java │ │ │ │ └── mysql/ │ │ │ │ └── merchantdetails/ │ │ │ │ └── MerchantDetailsMapper.java │ │ │ ├── http/ │ │ │ │ └── HttpRequestNoticeNewParams.java │ │ │ └── service/ │ │ │ └── merchantdetails/ │ │ │ ├── MerchantDetailsService.java │ │ │ └── MerchantDetailsServiceImpl.java │ │ └── resources/ │ │ └── mapper/ │ │ └── merchantdetails/ │ │ └── MerchantDetailsMapper.xml │ ├── yshop-module-score/ │ │ ├── pom.xml │ │ ├── yshop-module-score-api/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── score/ │ │ │ └── enums/ │ │ │ ├── ErrorCodeConstants.java │ │ │ └── OrderStatusEnum.java │ │ └── yshop-module-score-biz/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── score/ │ │ │ ├── controller/ │ │ │ │ ├── admin/ │ │ │ │ │ ├── scoreorder/ │ │ │ │ │ │ ├── ScoreOrderController.java │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ ├── ScoreOrderBaseVO.java │ │ │ │ │ │ ├── ScoreOrderCreateReqVO.java │ │ │ │ │ │ ├── ScoreOrderExcelVO.java │ │ │ │ │ │ ├── ScoreOrderExportReqVO.java │ │ │ │ │ │ ├── ScoreOrderPageReqVO.java │ │ │ │ │ │ ├── ScoreOrderRespVO.java │ │ │ │ │ │ └── ScoreOrderUpdateReqVO.java │ │ │ │ │ └── scoreproduct/ │ │ │ │ │ ├── ScoreProductController.java │ │ │ │ │ └── vo/ │ │ │ │ │ ├── ScoreProductBaseVO.java │ │ │ │ │ ├── ScoreProductCreateReqVO.java │ │ │ │ │ ├── ScoreProductExcelVO.java │ │ │ │ │ ├── ScoreProductExportReqVO.java │ │ │ │ │ ├── ScoreProductPageReqVO.java │ │ │ │ │ ├── ScoreProductRespVO.java │ │ │ │ │ └── ScoreProductUpdateReqVO.java │ │ │ │ └── app/ │ │ │ │ ├── order/ │ │ │ │ │ ├── AppScoreOrderController.java │ │ │ │ │ ├── param/ │ │ │ │ │ │ └── AppScoreOrderParam.java │ │ │ │ │ └── vo/ │ │ │ │ │ └── AppScoreOrderVO.java │ │ │ │ └── product/ │ │ │ │ ├── AppScoreProductController.java │ │ │ │ ├── param/ │ │ │ │ │ └── AppScoreProductQueryParam.java │ │ │ │ └── vo/ │ │ │ │ └── AppScoreProductVO.java │ │ │ ├── convert/ │ │ │ │ ├── scoreorder/ │ │ │ │ │ └── ScoreOrderConvert.java │ │ │ │ └── scoreproduct/ │ │ │ │ └── ScoreProductConvert.java │ │ │ ├── dal/ │ │ │ │ ├── dataobject/ │ │ │ │ │ ├── scoreorder/ │ │ │ │ │ │ └── ScoreOrderDO.java │ │ │ │ │ └── scoreproduct/ │ │ │ │ │ └── ScoreProductDO.java │ │ │ │ └── mysql/ │ │ │ │ ├── scoreorder/ │ │ │ │ │ └── ScoreOrderMapper.java │ │ │ │ └── scoreproduct/ │ │ │ │ └── ScoreProductMapper.java │ │ │ └── service/ │ │ │ ├── scoreorder/ │ │ │ │ ├── AppScoreOrderService.java │ │ │ │ ├── AppScoreOrderServiceImpl.java │ │ │ │ ├── ScoreOrderService.java │ │ │ │ └── ScoreOrderServiceImpl.java │ │ │ └── scoreproduct/ │ │ │ ├── AppScoreProductService.java │ │ │ ├── AppScoreProductServiceImpl.java │ │ │ ├── ScoreProductService.java │ │ │ └── ScoreProductServiceImpl.java │ │ └── resources/ │ │ └── mapper/ │ │ └── scoreorder/ │ │ └── ScoreOrderMapper.xml │ ├── yshop-module-system/ │ │ ├── pom.xml │ │ ├── yshop-module-system-api/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── system/ │ │ │ ├── api/ │ │ │ │ ├── dept/ │ │ │ │ │ ├── DeptApi.java │ │ │ │ │ ├── PostApi.java │ │ │ │ │ └── dto/ │ │ │ │ │ ├── DeptRespDTO.java │ │ │ │ │ └── PostRespDTO.java │ │ │ │ ├── dict/ │ │ │ │ │ ├── DictDataApi.java │ │ │ │ │ └── dto/ │ │ │ │ │ └── DictDataRespDTO.java │ │ │ │ ├── logger/ │ │ │ │ │ ├── LoginLogApi.java │ │ │ │ │ ├── OperateLogApi.java │ │ │ │ │ └── dto/ │ │ │ │ │ ├── LoginLogCreateReqDTO.java │ │ │ │ │ ├── OperateLogCreateReqDTO.java │ │ │ │ │ ├── OperateLogPageReqDTO.java │ │ │ │ │ └── OperateLogRespDTO.java │ │ │ │ ├── mail/ │ │ │ │ │ ├── MailSendApi.java │ │ │ │ │ └── dto/ │ │ │ │ │ └── MailSendSingleToUserReqDTO.java │ │ │ │ ├── notify/ │ │ │ │ │ ├── NotifyMessageSendApi.java │ │ │ │ │ └── dto/ │ │ │ │ │ ├── NotifySendSingleToUserReqDTO.java │ │ │ │ │ └── NotifyTemplateReqDTO.java │ │ │ │ ├── oauth2/ │ │ │ │ │ ├── OAuth2TokenApi.java │ │ │ │ │ └── dto/ │ │ │ │ │ ├── OAuth2AccessTokenCheckRespDTO.java │ │ │ │ │ ├── OAuth2AccessTokenCreateReqDTO.java │ │ │ │ │ └── OAuth2AccessTokenRespDTO.java │ │ │ │ ├── permission/ │ │ │ │ │ ├── PermissionApi.java │ │ │ │ │ ├── RoleApi.java │ │ │ │ │ └── dto/ │ │ │ │ │ └── DeptDataPermissionRespDTO.java │ │ │ │ ├── sms/ │ │ │ │ │ ├── SmsCodeApi.java │ │ │ │ │ ├── SmsSendApi.java │ │ │ │ │ └── dto/ │ │ │ │ │ ├── code/ │ │ │ │ │ │ ├── SmsCodeSendReqDTO.java │ │ │ │ │ │ ├── SmsCodeUseReqDTO.java │ │ │ │ │ │ └── SmsCodeValidateReqDTO.java │ │ │ │ │ └── send/ │ │ │ │ │ └── SmsSendSingleToUserReqDTO.java │ │ │ │ ├── social/ │ │ │ │ │ ├── SocialClientApi.java │ │ │ │ │ ├── SocialUserApi.java │ │ │ │ │ └── dto/ │ │ │ │ │ ├── SocialUserBindReqDTO.java │ │ │ │ │ ├── SocialUserRespDTO.java │ │ │ │ │ ├── SocialUserUnbindReqDTO.java │ │ │ │ │ ├── SocialWxJsapiSignatureRespDTO.java │ │ │ │ │ └── SocialWxPhoneNumberInfoRespDTO.java │ │ │ │ ├── tenant/ │ │ │ │ │ └── TenantApi.java │ │ │ │ └── user/ │ │ │ │ ├── AdminUserApi.java │ │ │ │ └── dto/ │ │ │ │ └── AdminUserRespDTO.java │ │ │ └── enums/ │ │ │ ├── DictTypeConstants.java │ │ │ ├── ErrorCodeConstants.java │ │ │ ├── LogRecordConstants.java │ │ │ ├── common/ │ │ │ │ └── SexEnum.java │ │ │ ├── logger/ │ │ │ │ ├── LoginLogTypeEnum.java │ │ │ │ └── LoginResultEnum.java │ │ │ ├── mail/ │ │ │ │ └── MailSendStatusEnum.java │ │ │ ├── notice/ │ │ │ │ └── NoticeTypeEnum.java │ │ │ ├── notify/ │ │ │ │ └── NotifyTemplateTypeEnum.java │ │ │ ├── oauth2/ │ │ │ │ ├── OAuth2ClientConstants.java │ │ │ │ └── OAuth2GrantTypeEnum.java │ │ │ ├── permission/ │ │ │ │ ├── DataScopeEnum.java │ │ │ │ ├── MenuTypeEnum.java │ │ │ │ ├── RoleCodeEnum.java │ │ │ │ └── RoleTypeEnum.java │ │ │ ├── sms/ │ │ │ │ ├── SmsReceiveStatusEnum.java │ │ │ │ ├── SmsSceneEnum.java │ │ │ │ ├── SmsSendStatusEnum.java │ │ │ │ └── SmsTemplateTypeEnum.java │ │ │ └── social/ │ │ │ └── SocialTypeEnum.java │ │ └── yshop-module-system-biz/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── co/ │ │ │ │ └── yixiang/ │ │ │ │ └── yshop/ │ │ │ │ └── module/ │ │ │ │ └── system/ │ │ │ │ ├── api/ │ │ │ │ │ ├── dept/ │ │ │ │ │ │ ├── DeptApiImpl.java │ │ │ │ │ │ └── PostApiImpl.java │ │ │ │ │ ├── dict/ │ │ │ │ │ │ └── DictDataApiImpl.java │ │ │ │ │ ├── logger/ │ │ │ │ │ │ ├── LoginLogApiImpl.java │ │ │ │ │ │ └── OperateLogApiImpl.java │ │ │ │ │ ├── mail/ │ │ │ │ │ │ └── MailSendApiImpl.java │ │ │ │ │ ├── notify/ │ │ │ │ │ │ └── NotifyMessageSendApiImpl.java │ │ │ │ │ ├── oauth2/ │ │ │ │ │ │ └── OAuth2TokenApiImpl.java │ │ │ │ │ ├── permission/ │ │ │ │ │ │ ├── PermissionApiImpl.java │ │ │ │ │ │ └── RoleApiImpl.java │ │ │ │ │ ├── sms/ │ │ │ │ │ │ ├── SmsCodeApiImpl.java │ │ │ │ │ │ └── SmsSendApiImpl.java │ │ │ │ │ ├── social/ │ │ │ │ │ │ ├── SocialClientApiImpl.java │ │ │ │ │ │ └── SocialUserApiImpl.java │ │ │ │ │ ├── tenant/ │ │ │ │ │ │ └── TenantApiImpl.java │ │ │ │ │ └── user/ │ │ │ │ │ └── AdminUserApiImpl.java │ │ │ │ ├── controller/ │ │ │ │ │ ├── admin/ │ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ │ ├── AuthController.java │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ ├── AuthLoginReqVO.java │ │ │ │ │ │ │ ├── AuthLoginRespVO.java │ │ │ │ │ │ │ ├── AuthMenuRespVO.java │ │ │ │ │ │ │ ├── AuthPermissionInfoRespVO.java │ │ │ │ │ │ │ ├── AuthSmsLoginReqVO.java │ │ │ │ │ │ │ ├── AuthSmsSendReqVO.java │ │ │ │ │ │ │ └── AuthSocialLoginReqVO.java │ │ │ │ │ │ ├── captcha/ │ │ │ │ │ │ │ └── CaptchaController.java │ │ │ │ │ │ ├── dept/ │ │ │ │ │ │ │ ├── DeptController.java │ │ │ │ │ │ │ ├── PostController.java │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ ├── dept/ │ │ │ │ │ │ │ │ ├── DeptListReqVO.java │ │ │ │ │ │ │ │ ├── DeptRespVO.java │ │ │ │ │ │ │ │ ├── DeptSaveReqVO.java │ │ │ │ │ │ │ │ └── DeptSimpleRespVO.java │ │ │ │ │ │ │ └── post/ │ │ │ │ │ │ │ ├── PostPageReqVO.java │ │ │ │ │ │ │ ├── PostRespVO.java │ │ │ │ │ │ │ ├── PostSaveReqVO.java │ │ │ │ │ │ │ └── PostSimpleRespVO.java │ │ │ │ │ │ ├── dict/ │ │ │ │ │ │ │ ├── DictDataController.java │ │ │ │ │ │ │ ├── DictTypeController.java │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ ├── data/ │ │ │ │ │ │ │ │ ├── DictDataPageReqVO.java │ │ │ │ │ │ │ │ ├── DictDataRespVO.java │ │ │ │ │ │ │ │ ├── DictDataSaveReqVO.java │ │ │ │ │ │ │ │ └── DictDataSimpleRespVO.java │ │ │ │ │ │ │ └── type/ │ │ │ │ │ │ │ ├── DictTypePageReqVO.java │ │ │ │ │ │ │ ├── DictTypeRespVO.java │ │ │ │ │ │ │ ├── DictTypeSaveReqVO.java │ │ │ │ │ │ │ └── DictTypeSimpleRespVO.java │ │ │ │ │ │ ├── ip/ │ │ │ │ │ │ │ ├── AreaController.java │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ └── AreaNodeRespVO.java │ │ │ │ │ │ ├── logger/ │ │ │ │ │ │ │ ├── LoginLogController.java │ │ │ │ │ │ │ ├── OperateLogController.java │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ ├── loginlog/ │ │ │ │ │ │ │ │ ├── LoginLogPageReqVO.java │ │ │ │ │ │ │ │ └── LoginLogRespVO.java │ │ │ │ │ │ │ └── operatelog/ │ │ │ │ │ │ │ ├── OperateLogPageReqVO.java │ │ │ │ │ │ │ └── OperateLogRespVO.java │ │ │ │ │ │ ├── mail/ │ │ │ │ │ │ │ ├── MailAccountController.java │ │ │ │ │ │ │ ├── MailLogController.java │ │ │ │ │ │ │ ├── MailTemplateController.java │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ ├── account/ │ │ │ │ │ │ │ │ ├── MailAccountPageReqVO.java │ │ │ │ │ │ │ │ ├── MailAccountRespVO.java │ │ │ │ │ │ │ │ ├── MailAccountSaveReqVO.java │ │ │ │ │ │ │ │ └── MailAccountSimpleRespVO.java │ │ │ │ │ │ │ ├── log/ │ │ │ │ │ │ │ │ ├── MailLogPageReqVO.java │ │ │ │ │ │ │ │ └── MailLogRespVO.java │ │ │ │ │ │ │ └── template/ │ │ │ │ │ │ │ ├── MailTemplatePageReqVO.java │ │ │ │ │ │ │ ├── MailTemplateRespVO.java │ │ │ │ │ │ │ ├── MailTemplateSaveReqVO.java │ │ │ │ │ │ │ ├── MailTemplateSendReqVO.java │ │ │ │ │ │ │ └── MailTemplateSimpleRespVO.java │ │ │ │ │ │ ├── notice/ │ │ │ │ │ │ │ ├── NoticeController.java │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ ├── NoticePageReqVO.java │ │ │ │ │ │ │ ├── NoticeRespVO.java │ │ │ │ │ │ │ └── NoticeSaveReqVO.java │ │ │ │ │ │ ├── notify/ │ │ │ │ │ │ │ ├── NotifyMessageController.java │ │ │ │ │ │ │ ├── NotifyTemplateController.java │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ ├── message/ │ │ │ │ │ │ │ │ ├── NotifyMessageMyPageReqVO.java │ │ │ │ │ │ │ │ ├── NotifyMessagePageReqVO.java │ │ │ │ │ │ │ │ └── NotifyMessageRespVO.java │ │ │ │ │ │ │ └── template/ │ │ │ │ │ │ │ ├── NotifyTemplatePageReqVO.java │ │ │ │ │ │ │ ├── NotifyTemplateRespVO.java │ │ │ │ │ │ │ ├── NotifyTemplateSaveReqVO.java │ │ │ │ │ │ │ └── NotifyTemplateSendReqVO.java │ │ │ │ │ │ ├── oauth2/ │ │ │ │ │ │ │ ├── OAuth2ClientController.java │ │ │ │ │ │ │ ├── OAuth2OpenController.java │ │ │ │ │ │ │ ├── OAuth2TokenController.java │ │ │ │ │ │ │ ├── OAuth2UserController.java │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ ├── client/ │ │ │ │ │ │ │ │ ├── OAuth2ClientPageReqVO.java │ │ │ │ │ │ │ │ ├── OAuth2ClientRespVO.java │ │ │ │ │ │ │ │ └── OAuth2ClientSaveReqVO.java │ │ │ │ │ │ │ ├── open/ │ │ │ │ │ │ │ │ ├── OAuth2OpenAccessTokenRespVO.java │ │ │ │ │ │ │ │ ├── OAuth2OpenAuthorizeInfoRespVO.java │ │ │ │ │ │ │ │ └── OAuth2OpenCheckTokenRespVO.java │ │ │ │ │ │ │ ├── token/ │ │ │ │ │ │ │ │ ├── OAuth2AccessTokenPageReqVO.java │ │ │ │ │ │ │ │ └── OAuth2AccessTokenRespVO.java │ │ │ │ │ │ │ └── user/ │ │ │ │ │ │ │ ├── OAuth2UserInfoRespVO.java │ │ │ │ │ │ │ └── OAuth2UserUpdateReqVO.java │ │ │ │ │ │ ├── permission/ │ │ │ │ │ │ │ ├── MenuController.java │ │ │ │ │ │ │ ├── PermissionController.java │ │ │ │ │ │ │ ├── RoleController.java │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ ├── menu/ │ │ │ │ │ │ │ │ ├── MenuListReqVO.java │ │ │ │ │ │ │ │ ├── MenuRespVO.java │ │ │ │ │ │ │ │ ├── MenuSaveVO.java │ │ │ │ │ │ │ │ └── MenuSimpleRespVO.java │ │ │ │ │ │ │ ├── permission/ │ │ │ │ │ │ │ │ ├── PermissionAssignRoleDataScopeReqVO.java │ │ │ │ │ │ │ │ ├── PermissionAssignRoleMenuReqVO.java │ │ │ │ │ │ │ │ └── PermissionAssignUserRoleReqVO.java │ │ │ │ │ │ │ └── role/ │ │ │ │ │ │ │ ├── RolePageReqVO.java │ │ │ │ │ │ │ ├── RoleRespVO.java │ │ │ │ │ │ │ ├── RoleSaveReqVO.java │ │ │ │ │ │ │ └── RoleSimpleRespVO.java │ │ │ │ │ │ ├── sms/ │ │ │ │ │ │ │ ├── SmsCallbackController.java │ │ │ │ │ │ │ ├── SmsChannelController.java │ │ │ │ │ │ │ ├── SmsLogController.java │ │ │ │ │ │ │ ├── SmsTemplateController.java │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ ├── channel/ │ │ │ │ │ │ │ │ ├── SmsChannelPageReqVO.java │ │ │ │ │ │ │ │ ├── SmsChannelRespVO.java │ │ │ │ │ │ │ │ ├── SmsChannelSaveReqVO.java │ │ │ │ │ │ │ │ └── SmsChannelSimpleRespVO.java │ │ │ │ │ │ │ ├── log/ │ │ │ │ │ │ │ │ ├── SmsLogPageReqVO.java │ │ │ │ │ │ │ │ └── SmsLogRespVO.java │ │ │ │ │ │ │ └── template/ │ │ │ │ │ │ │ ├── SmsTemplatePageReqVO.java │ │ │ │ │ │ │ ├── SmsTemplateRespVO.java │ │ │ │ │ │ │ ├── SmsTemplateSaveReqVO.java │ │ │ │ │ │ │ └── SmsTemplateSendReqVO.java │ │ │ │ │ │ ├── socail/ │ │ │ │ │ │ │ ├── SocialClientController.java │ │ │ │ │ │ │ ├── SocialUserController.java │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ ├── client/ │ │ │ │ │ │ │ │ ├── SocialClientPageReqVO.java │ │ │ │ │ │ │ │ ├── SocialClientRespVO.java │ │ │ │ │ │ │ │ └── SocialClientSaveReqVO.java │ │ │ │ │ │ │ └── user/ │ │ │ │ │ │ │ ├── SocialUserBindReqVO.java │ │ │ │ │ │ │ ├── SocialUserPageReqVO.java │ │ │ │ │ │ │ ├── SocialUserRespVO.java │ │ │ │ │ │ │ └── SocialUserUnbindReqVO.java │ │ │ │ │ │ ├── tenant/ │ │ │ │ │ │ │ ├── TenantController.java │ │ │ │ │ │ │ ├── TenantPackageController.java │ │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ │ ├── packages/ │ │ │ │ │ │ │ │ ├── TenantPackagePageReqVO.java │ │ │ │ │ │ │ │ ├── TenantPackageRespVO.java │ │ │ │ │ │ │ │ ├── TenantPackageSaveReqVO.java │ │ │ │ │ │ │ │ └── TenantPackageSimpleRespVO.java │ │ │ │ │ │ │ └── tenant/ │ │ │ │ │ │ │ ├── TenantPageReqVO.java │ │ │ │ │ │ │ ├── TenantRespVO.java │ │ │ │ │ │ │ ├── TenantSaveReqVO.java │ │ │ │ │ │ │ └── TenantSimpleRespVO.java │ │ │ │ │ │ └── user/ │ │ │ │ │ │ ├── UserController.java │ │ │ │ │ │ ├── UserProfileController.java │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ ├── profile/ │ │ │ │ │ │ │ ├── UserProfileRespVO.java │ │ │ │ │ │ │ ├── UserProfileUpdatePasswordReqVO.java │ │ │ │ │ │ │ └── UserProfileUpdateReqVO.java │ │ │ │ │ │ └── user/ │ │ │ │ │ │ ├── UserImportExcelVO.java │ │ │ │ │ │ ├── UserImportRespVO.java │ │ │ │ │ │ ├── UserPageReqVO.java │ │ │ │ │ │ ├── UserRespVO.java │ │ │ │ │ │ ├── UserSaveReqVO.java │ │ │ │ │ │ ├── UserSimpleRespVO.java │ │ │ │ │ │ ├── UserUpdatePasswordReqVO.java │ │ │ │ │ │ └── UserUpdateStatusReqVO.java │ │ │ │ │ └── app/ │ │ │ │ │ ├── dict/ │ │ │ │ │ │ ├── AppDictDataController.java │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ └── AppDictDataRespVO.java │ │ │ │ │ ├── ip/ │ │ │ │ │ │ ├── AppAreaController.java │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ └── AppAreaNodeRespVO.java │ │ │ │ │ └── notice/ │ │ │ │ │ └── AppNoticeController.java │ │ │ │ ├── convert/ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ └── AuthConvert.java │ │ │ │ │ ├── oauth2/ │ │ │ │ │ │ └── OAuth2OpenConvert.java │ │ │ │ │ ├── social/ │ │ │ │ │ │ └── SocialUserConvert.java │ │ │ │ │ ├── tenant/ │ │ │ │ │ │ └── TenantConvert.java │ │ │ │ │ └── user/ │ │ │ │ │ └── UserConvert.java │ │ │ │ ├── dal/ │ │ │ │ │ ├── dataobject/ │ │ │ │ │ │ ├── dept/ │ │ │ │ │ │ │ ├── DeptDO.java │ │ │ │ │ │ │ ├── PostDO.java │ │ │ │ │ │ │ └── UserPostDO.java │ │ │ │ │ │ ├── dict/ │ │ │ │ │ │ │ ├── DictDataDO.java │ │ │ │ │ │ │ └── DictTypeDO.java │ │ │ │ │ │ ├── logger/ │ │ │ │ │ │ │ ├── LoginLogDO.java │ │ │ │ │ │ │ └── OperateLogDO.java │ │ │ │ │ │ ├── mail/ │ │ │ │ │ │ │ ├── MailAccountDO.java │ │ │ │ │ │ │ ├── MailLogDO.java │ │ │ │ │ │ │ └── MailTemplateDO.java │ │ │ │ │ │ ├── notice/ │ │ │ │ │ │ │ └── NoticeDO.java │ │ │ │ │ │ ├── notify/ │ │ │ │ │ │ │ ├── NotifyMessageDO.java │ │ │ │ │ │ │ └── NotifyTemplateDO.java │ │ │ │ │ │ ├── oauth2/ │ │ │ │ │ │ │ ├── OAuth2AccessTokenDO.java │ │ │ │ │ │ │ ├── OAuth2ApproveDO.java │ │ │ │ │ │ │ ├── OAuth2ClientDO.java │ │ │ │ │ │ │ ├── OAuth2CodeDO.java │ │ │ │ │ │ │ └── OAuth2RefreshTokenDO.java │ │ │ │ │ │ ├── permission/ │ │ │ │ │ │ │ ├── MenuDO.java │ │ │ │ │ │ │ ├── RoleDO.java │ │ │ │ │ │ │ ├── RoleMenuDO.java │ │ │ │ │ │ │ └── UserRoleDO.java │ │ │ │ │ │ ├── sms/ │ │ │ │ │ │ │ ├── SmsChannelDO.java │ │ │ │ │ │ │ ├── SmsCodeDO.java │ │ │ │ │ │ │ ├── SmsLogDO.java │ │ │ │ │ │ │ └── SmsTemplateDO.java │ │ │ │ │ │ ├── social/ │ │ │ │ │ │ │ ├── SocialClientDO.java │ │ │ │ │ │ │ ├── SocialUserBindDO.java │ │ │ │ │ │ │ └── SocialUserDO.java │ │ │ │ │ │ ├── tenant/ │ │ │ │ │ │ │ ├── TenantDO.java │ │ │ │ │ │ │ └── TenantPackageDO.java │ │ │ │ │ │ └── user/ │ │ │ │ │ │ └── AdminUserDO.java │ │ │ │ │ ├── mysql/ │ │ │ │ │ │ ├── dept/ │ │ │ │ │ │ │ ├── DeptMapper.java │ │ │ │ │ │ │ ├── PostMapper.java │ │ │ │ │ │ │ └── UserPostMapper.java │ │ │ │ │ │ ├── dict/ │ │ │ │ │ │ │ ├── DictDataMapper.java │ │ │ │ │ │ │ └── DictTypeMapper.java │ │ │ │ │ │ ├── logger/ │ │ │ │ │ │ │ ├── LoginLogMapper.java │ │ │ │ │ │ │ └── OperateLogMapper.java │ │ │ │ │ │ ├── mail/ │ │ │ │ │ │ │ ├── MailAccountMapper.java │ │ │ │ │ │ │ ├── MailLogMapper.java │ │ │ │ │ │ │ └── MailTemplateMapper.java │ │ │ │ │ │ ├── notice/ │ │ │ │ │ │ │ └── NoticeMapper.java │ │ │ │ │ │ ├── notify/ │ │ │ │ │ │ │ ├── NotifyMessageMapper.java │ │ │ │ │ │ │ └── NotifyTemplateMapper.java │ │ │ │ │ │ ├── oauth2/ │ │ │ │ │ │ │ ├── OAuth2AccessTokenMapper.java │ │ │ │ │ │ │ ├── OAuth2ApproveMapper.java │ │ │ │ │ │ │ ├── OAuth2ClientMapper.java │ │ │ │ │ │ │ ├── OAuth2CodeMapper.java │ │ │ │ │ │ │ └── OAuth2RefreshTokenMapper.java │ │ │ │ │ │ ├── permission/ │ │ │ │ │ │ │ ├── MenuMapper.java │ │ │ │ │ │ │ ├── RoleMapper.java │ │ │ │ │ │ │ ├── RoleMenuMapper.java │ │ │ │ │ │ │ └── UserRoleMapper.java │ │ │ │ │ │ ├── sms/ │ │ │ │ │ │ │ ├── SmsChannelMapper.java │ │ │ │ │ │ │ ├── SmsCodeMapper.java │ │ │ │ │ │ │ ├── SmsLogMapper.java │ │ │ │ │ │ │ └── SmsTemplateMapper.java │ │ │ │ │ │ ├── social/ │ │ │ │ │ │ │ ├── SocialClientMapper.java │ │ │ │ │ │ │ ├── SocialUserBindMapper.java │ │ │ │ │ │ │ └── SocialUserMapper.java │ │ │ │ │ │ ├── tenant/ │ │ │ │ │ │ │ ├── TenantMapper.java │ │ │ │ │ │ │ └── TenantPackageMapper.java │ │ │ │ │ │ └── user/ │ │ │ │ │ │ └── AdminUserMapper.java │ │ │ │ │ └── redis/ │ │ │ │ │ ├── RedisKeyConstants.java │ │ │ │ │ └── oauth2/ │ │ │ │ │ └── OAuth2AccessTokenRedisDAO.java │ │ │ │ ├── framework/ │ │ │ │ │ ├── captcha/ │ │ │ │ │ │ ├── config/ │ │ │ │ │ │ │ └── YshopCaptchaConfiguration.java │ │ │ │ │ │ └── core/ │ │ │ │ │ │ └── RedisCaptchaServiceImpl.java │ │ │ │ │ ├── datapermission/ │ │ │ │ │ │ └── config/ │ │ │ │ │ │ └── DataPermissionConfiguration.java │ │ │ │ │ ├── operatelog/ │ │ │ │ │ │ └── core/ │ │ │ │ │ │ ├── AdminUserParseFunction.java │ │ │ │ │ │ ├── AreaParseFunction.java │ │ │ │ │ │ ├── BooleanParseFunction.java │ │ │ │ │ │ ├── DeptParseFunction.java │ │ │ │ │ │ ├── PostParseFunction.java │ │ │ │ │ │ └── SexParseFunction.java │ │ │ │ │ ├── sms/ │ │ │ │ │ │ ├── config/ │ │ │ │ │ │ │ ├── SmsCodeProperties.java │ │ │ │ │ │ │ └── SmsConfiguration.java │ │ │ │ │ │ └── core/ │ │ │ │ │ │ ├── client/ │ │ │ │ │ │ │ ├── SmsClient.java │ │ │ │ │ │ │ ├── SmsClientFactory.java │ │ │ │ │ │ │ ├── dto/ │ │ │ │ │ │ │ │ ├── SmsReceiveRespDTO.java │ │ │ │ │ │ │ │ ├── SmsSendRespDTO.java │ │ │ │ │ │ │ │ └── SmsTemplateRespDTO.java │ │ │ │ │ │ │ └── impl/ │ │ │ │ │ │ │ ├── AbstractSmsClient.java │ │ │ │ │ │ │ ├── AliyunSmsClient.java │ │ │ │ │ │ │ ├── DebugDingTalkSmsClient.java │ │ │ │ │ │ │ ├── SmsClientFactoryImpl.java │ │ │ │ │ │ │ └── TencentSmsClient.java │ │ │ │ │ │ ├── enums/ │ │ │ │ │ │ │ ├── SmsChannelEnum.java │ │ │ │ │ │ │ └── SmsTemplateAuditStatusEnum.java │ │ │ │ │ │ └── property/ │ │ │ │ │ │ └── SmsChannelProperties.java │ │ │ │ │ └── web/ │ │ │ │ │ └── config/ │ │ │ │ │ └── SystemWebConfiguration.java │ │ │ │ ├── job/ │ │ │ │ │ └── DemoJob.java │ │ │ │ ├── mq/ │ │ │ │ │ ├── consumer/ │ │ │ │ │ │ ├── mail/ │ │ │ │ │ │ │ └── MailSendConsumer.java │ │ │ │ │ │ └── sms/ │ │ │ │ │ │ └── SmsSendConsumer.java │ │ │ │ │ ├── message/ │ │ │ │ │ │ ├── mail/ │ │ │ │ │ │ │ └── MailSendMessage.java │ │ │ │ │ │ └── sms/ │ │ │ │ │ │ └── SmsSendMessage.java │ │ │ │ │ └── producer/ │ │ │ │ │ ├── mail/ │ │ │ │ │ │ └── MailProducer.java │ │ │ │ │ └── sms/ │ │ │ │ │ └── SmsProducer.java │ │ │ │ ├── service/ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ ├── AdminAuthService.java │ │ │ │ │ │ └── AdminAuthServiceImpl.java │ │ │ │ │ ├── dept/ │ │ │ │ │ │ ├── DeptService.java │ │ │ │ │ │ ├── DeptServiceImpl.java │ │ │ │ │ │ ├── PostService.java │ │ │ │ │ │ └── PostServiceImpl.java │ │ │ │ │ ├── dict/ │ │ │ │ │ │ ├── DictDataService.java │ │ │ │ │ │ ├── DictDataServiceImpl.java │ │ │ │ │ │ ├── DictTypeService.java │ │ │ │ │ │ └── DictTypeServiceImpl.java │ │ │ │ │ ├── logger/ │ │ │ │ │ │ ├── LoginLogService.java │ │ │ │ │ │ ├── LoginLogServiceImpl.java │ │ │ │ │ │ ├── OperateLogService.java │ │ │ │ │ │ └── OperateLogServiceImpl.java │ │ │ │ │ ├── mail/ │ │ │ │ │ │ ├── MailAccountService.java │ │ │ │ │ │ ├── MailAccountServiceImpl.java │ │ │ │ │ │ ├── MailLogService.java │ │ │ │ │ │ ├── MailLogServiceImpl.java │ │ │ │ │ │ ├── MailSendService.java │ │ │ │ │ │ ├── MailSendServiceImpl.java │ │ │ │ │ │ ├── MailTemplateService.java │ │ │ │ │ │ └── MailTemplateServiceImpl.java │ │ │ │ │ ├── member/ │ │ │ │ │ │ ├── MemberService.java │ │ │ │ │ │ └── MemberServiceImpl.java │ │ │ │ │ ├── notice/ │ │ │ │ │ │ ├── NoticeService.java │ │ │ │ │ │ └── NoticeServiceImpl.java │ │ │ │ │ ├── notify/ │ │ │ │ │ │ ├── NotifyMessageService.java │ │ │ │ │ │ ├── NotifyMessageServiceImpl.java │ │ │ │ │ │ ├── NotifySendService.java │ │ │ │ │ │ ├── NotifySendServiceImpl.java │ │ │ │ │ │ ├── NotifyTemplateService.java │ │ │ │ │ │ └── NotifyTemplateServiceImpl.java │ │ │ │ │ ├── oauth2/ │ │ │ │ │ │ ├── OAuth2ApproveService.java │ │ │ │ │ │ ├── OAuth2ApproveServiceImpl.java │ │ │ │ │ │ ├── OAuth2ClientService.java │ │ │ │ │ │ ├── OAuth2ClientServiceImpl.java │ │ │ │ │ │ ├── OAuth2CodeService.java │ │ │ │ │ │ ├── OAuth2CodeServiceImpl.java │ │ │ │ │ │ ├── OAuth2GrantService.java │ │ │ │ │ │ ├── OAuth2GrantServiceImpl.java │ │ │ │ │ │ ├── OAuth2TokenService.java │ │ │ │ │ │ └── OAuth2TokenServiceImpl.java │ │ │ │ │ ├── permission/ │ │ │ │ │ │ ├── MenuService.java │ │ │ │ │ │ ├── MenuServiceImpl.java │ │ │ │ │ │ ├── PermissionService.java │ │ │ │ │ │ ├── PermissionServiceImpl.java │ │ │ │ │ │ ├── RoleService.java │ │ │ │ │ │ └── RoleServiceImpl.java │ │ │ │ │ ├── sms/ │ │ │ │ │ │ ├── SmsChannelService.java │ │ │ │ │ │ ├── SmsChannelServiceImpl.java │ │ │ │ │ │ ├── SmsCodeService.java │ │ │ │ │ │ ├── SmsCodeServiceImpl.java │ │ │ │ │ │ ├── SmsLogService.java │ │ │ │ │ │ ├── SmsLogServiceImpl.java │ │ │ │ │ │ ├── SmsSendService.java │ │ │ │ │ │ ├── SmsSendServiceImpl.java │ │ │ │ │ │ ├── SmsTemplateService.java │ │ │ │ │ │ └── SmsTemplateServiceImpl.java │ │ │ │ │ ├── social/ │ │ │ │ │ │ ├── SocialClientService.java │ │ │ │ │ │ ├── SocialClientServiceImpl.java │ │ │ │ │ │ ├── SocialUserService.java │ │ │ │ │ │ └── SocialUserServiceImpl.java │ │ │ │ │ ├── tenant/ │ │ │ │ │ │ ├── TenantPackageService.java │ │ │ │ │ │ ├── TenantPackageServiceImpl.java │ │ │ │ │ │ ├── TenantService.java │ │ │ │ │ │ ├── TenantServiceImpl.java │ │ │ │ │ │ └── handler/ │ │ │ │ │ │ ├── TenantInfoHandler.java │ │ │ │ │ │ └── TenantMenuHandler.java │ │ │ │ │ └── user/ │ │ │ │ │ ├── AdminUserService.java │ │ │ │ │ └── AdminUserServiceImpl.java │ │ │ │ └── util/ │ │ │ │ └── oauth2/ │ │ │ │ └── OAuth2Utils.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ └── com.xingyuv.captcha.service.CaptchaCacheService │ │ └── test/ │ │ ├── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── module/ │ │ │ └── system/ │ │ │ ├── controller/ │ │ │ │ └── admin/ │ │ │ │ └── oauth2/ │ │ │ │ └── OAuth2OpenControllerTest.java │ │ │ ├── framework/ │ │ │ │ └── sms/ │ │ │ │ └── core/ │ │ │ │ └── client/ │ │ │ │ └── impl/ │ │ │ │ ├── AliyunSmsClientTest.java │ │ │ │ └── TencentSmsClientTest.java │ │ │ └── service/ │ │ │ ├── auth/ │ │ │ │ └── AdminAuthServiceImplTest.java │ │ │ ├── dept/ │ │ │ │ ├── DeptServiceImplTest.java │ │ │ │ └── PostServiceImplTest.java │ │ │ ├── dict/ │ │ │ │ ├── DictDataServiceImplTest.java │ │ │ │ └── DictTypeServiceImplTest.java │ │ │ ├── logger/ │ │ │ │ ├── LoginLogServiceImplTest.java │ │ │ │ └── OperateLogServiceImplTest.java │ │ │ ├── mail/ │ │ │ │ ├── MailAccountServiceImplTest.java │ │ │ │ ├── MailLogServiceImplTest.java │ │ │ │ ├── MailSendServiceImplTest.java │ │ │ │ └── MailTemplateServiceImplTest.java │ │ │ ├── notice/ │ │ │ │ └── NoticeServiceImplTest.java │ │ │ ├── notify/ │ │ │ │ ├── NotifyMessageServiceImplTest.java │ │ │ │ ├── NotifySendServiceImplTest.java │ │ │ │ └── NotifyTemplateServiceImplTest.java │ │ │ ├── oauth2/ │ │ │ │ ├── OAuth2ApproveServiceImplTest.java │ │ │ │ ├── OAuth2ClientServiceImplTest.java │ │ │ │ ├── OAuth2CodeServiceImplTest.java │ │ │ │ ├── OAuth2GrantServiceImplTest.java │ │ │ │ └── OAuth2TokenServiceImplTest.java │ │ │ ├── permission/ │ │ │ │ ├── MenuServiceImplTest.java │ │ │ │ ├── PermissionServiceTest.java │ │ │ │ └── RoleServiceImplTest.java │ │ │ ├── sms/ │ │ │ │ ├── SmsChannelServiceTest.java │ │ │ │ ├── SmsCodeServiceImplTest.java │ │ │ │ ├── SmsLogServiceImplTest.java │ │ │ │ ├── SmsSendServiceImplTest.java │ │ │ │ └── SmsTemplateServiceImplTest.java │ │ │ ├── social/ │ │ │ │ ├── SocialClientServiceImplTest.java │ │ │ │ └── SocialUserServiceImplTest.java │ │ │ ├── tenant/ │ │ │ │ ├── TenantPackageServiceImplTest.java │ │ │ │ └── TenantServiceImplTest.java │ │ │ └── user/ │ │ │ └── AdminUserServiceImplTest.java │ │ └── resources/ │ │ ├── application-unit-test.yaml │ │ ├── logback.xml │ │ └── sql/ │ │ ├── clean.sql │ │ └── create_tables.sql │ └── yshop-server/ │ ├── Dockerfile │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── co/ │ │ │ └── yixiang/ │ │ │ └── yshop/ │ │ │ └── server/ │ │ │ ├── YshopServerApplication.java │ │ │ └── controller/ │ │ │ └── DefaultController.java │ │ └── resources/ │ │ ├── META-INF/ │ │ │ └── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ ├── application-dev.yaml │ │ ├── application-local.yaml │ │ ├── application.yaml │ │ └── logback-spring.xml │ └── test/ │ └── java/ │ └── co/ │ └── yixiang/ │ └── yshop/ │ └── ProjectReactor.java ├── yshop-drink-uniapp-vue3/ │ ├── .gitignore │ ├── .hbuilderx/ │ │ └── launch.json │ ├── App.vue │ ├── api/ │ │ ├── address.js │ │ ├── api.js │ │ ├── auth.js │ │ ├── coupon.js │ │ ├── goods.js │ │ ├── market.js │ │ ├── order.js │ │ ├── score.js │ │ └── user.js │ ├── components/ │ │ ├── blank/ │ │ │ └── blank.vue │ │ ├── card/ │ │ │ └── card.vue │ │ ├── city-select/ │ │ │ └── city-select.vue │ │ ├── container/ │ │ │ └── container.vue │ │ ├── layout/ │ │ │ └── layout.vue │ │ ├── list-cell/ │ │ │ └── list-cell.vue │ │ ├── logo/ │ │ │ └── logo.vue │ │ ├── modal/ │ │ │ └── modal.vue │ │ ├── space/ │ │ │ └── space.vue │ │ ├── upload-file/ │ │ │ └── upload-file.vue │ │ └── verification/ │ │ └── verification.vue │ ├── config/ │ │ └── index.js │ ├── hooks/ │ │ ├── index.js │ │ ├── useGlobalProperties.js │ │ └── usePage.js │ ├── index.html │ ├── jsconfig.json │ ├── main.js │ ├── manifest.json │ ├── package.json │ ├── pages/ │ │ ├── cart/ │ │ │ └── cart.vue │ │ ├── components/ │ │ │ └── pages/ │ │ │ ├── address/ │ │ │ │ ├── add.vue │ │ │ │ └── address.vue │ │ │ ├── balance/ │ │ │ │ └── bill.vue │ │ │ ├── coupons/ │ │ │ │ └── coupons.vue │ │ │ ├── login/ │ │ │ │ ├── login.vue │ │ │ │ └── logout.vue │ │ │ ├── mine/ │ │ │ │ ├── content.vue │ │ │ │ └── userinfo.vue │ │ │ ├── orders/ │ │ │ │ ├── detail.vue │ │ │ │ ├── orders.vue │ │ │ │ └── refund.vue │ │ │ ├── packages/ │ │ │ │ └── index.vue │ │ │ ├── pay/ │ │ │ │ └── pay.vue │ │ │ ├── remark/ │ │ │ │ └── remark.vue │ │ │ ├── scoreproduct/ │ │ │ │ ├── confirm.vue │ │ │ │ ├── detail.vue │ │ │ │ ├── list.vue │ │ │ │ ├── order.vue │ │ │ │ └── orderDetail.vue │ │ │ └── shop/ │ │ │ └── shop.vue │ │ ├── index/ │ │ │ └── index.vue │ │ ├── menu/ │ │ │ └── menu.vue │ │ ├── mine/ │ │ │ └── mine.vue │ │ └── order/ │ │ └── order.vue │ ├── pages.json │ ├── static/ │ │ ├── iconfont/ │ │ │ ├── iconfont.css │ │ │ └── iconfont.scss │ │ └── style/ │ │ ├── app.scss │ │ ├── style.css │ │ ├── style.less │ │ └── yshop.css │ ├── store/ │ │ ├── home.js │ │ ├── page.js │ │ └── store.js │ ├── uni.promisify.adaptor.js │ ├── uni.scss │ ├── uni_modules/ │ │ ├── uni-card/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uni-card/ │ │ │ │ └── uni-card.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uni-icons/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uni-icons/ │ │ │ │ ├── icons.js │ │ │ │ ├── uni-icons.vue │ │ │ │ └── uniicons.css │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uni-scss/ │ │ │ ├── changelog.md │ │ │ ├── index.scss │ │ │ ├── package.json │ │ │ ├── readme.md │ │ │ ├── styles/ │ │ │ │ ├── index.scss │ │ │ │ ├── setting/ │ │ │ │ │ ├── _border.scss │ │ │ │ │ ├── _color.scss │ │ │ │ │ ├── _radius.scss │ │ │ │ │ ├── _space.scss │ │ │ │ │ ├── _styles.scss │ │ │ │ │ ├── _text.scss │ │ │ │ │ └── _variables.scss │ │ │ │ └── tools/ │ │ │ │ └── functions.scss │ │ │ ├── theme.scss │ │ │ └── variables.scss │ │ ├── uv-action-sheet/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-action-sheet/ │ │ │ │ ├── props.js │ │ │ │ └── uv-action-sheet.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-album/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-album/ │ │ │ │ └── uv-album.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-alert/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-alert/ │ │ │ │ ├── props.js │ │ │ │ └── uv-alert.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-avatar/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ ├── uv-avatar/ │ │ │ │ │ ├── props.js │ │ │ │ │ └── uv-avatar.vue │ │ │ │ └── uv-avatar-group/ │ │ │ │ ├── props.js │ │ │ │ └── uv-avatar-group.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-back-top/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-back-top/ │ │ │ │ ├── props.js │ │ │ │ └── uv-back-top.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-badge/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-badge/ │ │ │ │ ├── props.js │ │ │ │ └── uv-badge.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-button/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-button/ │ │ │ │ ├── nvue.scss │ │ │ │ ├── props.js │ │ │ │ ├── uv-button.vue │ │ │ │ └── vue.scss │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-calendar/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-calendar/ │ │ │ │ ├── calendar.js │ │ │ │ ├── header.vue │ │ │ │ ├── month.vue │ │ │ │ ├── props.js │ │ │ │ └── uv-calendar.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-calendars/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-calendars/ │ │ │ │ ├── calendar-body.vue │ │ │ │ ├── calendar-item.vue │ │ │ │ ├── calendar.js │ │ │ │ ├── i18n/ │ │ │ │ │ ├── en.json │ │ │ │ │ ├── index.js │ │ │ │ │ ├── zh-Hans.json │ │ │ │ │ └── zh-Hant.json │ │ │ │ ├── util.js │ │ │ │ ├── uv-calendar-body.vue │ │ │ │ ├── uv-calendar-item.vue │ │ │ │ └── uv-calendars.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-cell/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ ├── uv-cell/ │ │ │ │ │ ├── props.js │ │ │ │ │ └── uv-cell.vue │ │ │ │ └── uv-cell-group/ │ │ │ │ ├── props.js │ │ │ │ └── uv-cell-group.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-checkbox/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ ├── uv-checkbox/ │ │ │ │ │ ├── props.js │ │ │ │ │ └── uv-checkbox.vue │ │ │ │ └── uv-checkbox-group/ │ │ │ │ ├── props.js │ │ │ │ └── uv-checkbox-group.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-code/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-code/ │ │ │ │ ├── props.js │ │ │ │ └── uv-code.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-code-input/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-code-input/ │ │ │ │ ├── props.js │ │ │ │ └── uv-code-input.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-collapse/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ ├── uv-collapse/ │ │ │ │ │ ├── props.js │ │ │ │ │ └── uv-collapse.vue │ │ │ │ └── uv-collapse-item/ │ │ │ │ ├── props.js │ │ │ │ └── uv-collapse-item.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-count-down/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-count-down/ │ │ │ │ ├── props.js │ │ │ │ ├── utils.js │ │ │ │ └── uv-count-down.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-count-to/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-count-to/ │ │ │ │ ├── props.js │ │ │ │ └── uv-count-to.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-datetime-picker/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-datetime-picker/ │ │ │ │ ├── props.js │ │ │ │ └── uv-datetime-picker.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-divider/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-divider/ │ │ │ │ ├── props.js │ │ │ │ └── uv-divider.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-drop-down/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ ├── uv-drop-down/ │ │ │ │ │ └── uv-drop-down.vue │ │ │ │ ├── uv-drop-down-item/ │ │ │ │ │ └── uv-drop-down-item.vue │ │ │ │ └── uv-drop-down-popup/ │ │ │ │ └── uv-drop-down-popup.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-empty/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-empty/ │ │ │ │ ├── props.js │ │ │ │ └── uv-empty.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-form/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ ├── uv-form/ │ │ │ │ │ ├── props.js │ │ │ │ │ ├── uv-form.vue │ │ │ │ │ └── valid.js │ │ │ │ └── uv-form-item/ │ │ │ │ ├── props.js │ │ │ │ └── uv-form-item.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-gap/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-gap/ │ │ │ │ ├── props.js │ │ │ │ └── uv-gap.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-grid/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ ├── uv-grid/ │ │ │ │ │ ├── props.js │ │ │ │ │ └── uv-grid.vue │ │ │ │ └── uv-grid-item/ │ │ │ │ ├── props.js │ │ │ │ └── uv-grid-item.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-icon/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-icon/ │ │ │ │ ├── icons.js │ │ │ │ ├── props.js │ │ │ │ └── uv-icon.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-image/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-image/ │ │ │ │ ├── props.js │ │ │ │ └── uv-image.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-index-list/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ ├── uv-index-anchor/ │ │ │ │ │ ├── props.js │ │ │ │ │ └── uv-index-anchor.vue │ │ │ │ ├── uv-index-item/ │ │ │ │ │ └── uv-index-item.vue │ │ │ │ └── uv-index-list/ │ │ │ │ ├── props.js │ │ │ │ └── uv-index-list.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-input/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-input/ │ │ │ │ ├── props.js │ │ │ │ └── uv-input.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-keyboard/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ ├── uv-car-keyboard/ │ │ │ │ │ ├── props.js │ │ │ │ │ └── uv-car-keyboard.vue │ │ │ │ ├── uv-keyboard/ │ │ │ │ │ ├── props.js │ │ │ │ │ └── uv-keyboard.vue │ │ │ │ ├── uv-keyboard-car/ │ │ │ │ │ ├── props.js │ │ │ │ │ └── uv-keyboard-car.vue │ │ │ │ ├── uv-keyboard-number/ │ │ │ │ │ ├── props.js │ │ │ │ │ └── uv-keyboard-number.vue │ │ │ │ └── uv-number-keyboard/ │ │ │ │ ├── props.js │ │ │ │ └── uv-number-keyboard.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-line/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-line/ │ │ │ │ ├── props.js │ │ │ │ └── uv-line.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-line-progress/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-line-progress/ │ │ │ │ ├── props.js │ │ │ │ └── uv-line-progress.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-link/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-link/ │ │ │ │ ├── props.js │ │ │ │ └── uv-link.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-list/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ ├── uv-list/ │ │ │ │ │ ├── props.js │ │ │ │ │ └── uv-list.vue │ │ │ │ └── uv-list-item/ │ │ │ │ ├── props.js │ │ │ │ └── uv-list-item.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-load-more/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-load-more/ │ │ │ │ ├── props.js │ │ │ │ └── uv-load-more.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-loading-icon/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-loading-icon/ │ │ │ │ ├── props.js │ │ │ │ └── uv-loading-icon.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-loading-page/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-loading-page/ │ │ │ │ ├── props.js │ │ │ │ └── uv-loading-page.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-modal/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-modal/ │ │ │ │ ├── props.js │ │ │ │ └── uv-modal.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-navbar/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-navbar/ │ │ │ │ ├── props.js │ │ │ │ └── uv-navbar.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-no-network/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-no-network/ │ │ │ │ ├── props.js │ │ │ │ └── uv-no-network.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-notice-bar/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ ├── uv-column-notice/ │ │ │ │ │ ├── props.js │ │ │ │ │ └── uv-column-notice.vue │ │ │ │ ├── uv-notice-bar/ │ │ │ │ │ ├── props.js │ │ │ │ │ └── uv-notice-bar.vue │ │ │ │ └── uv-row-notice/ │ │ │ │ ├── props.js │ │ │ │ └── uv-row-notice.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-notify/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-notify/ │ │ │ │ ├── props.js │ │ │ │ └── uv-notify.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-number-box/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-number-box/ │ │ │ │ ├── props.js │ │ │ │ └── uv-number-box.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-overlay/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-overlay/ │ │ │ │ ├── props.js │ │ │ │ └── uv-overlay.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-parse/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-parse/ │ │ │ │ ├── node/ │ │ │ │ │ └── node.vue │ │ │ │ ├── parser.js │ │ │ │ └── uv-parse.vue │ │ │ ├── package.json │ │ │ ├── readme.md │ │ │ └── static/ │ │ │ └── app-plus/ │ │ │ └── uv-parse/ │ │ │ ├── js/ │ │ │ │ └── handler.js │ │ │ └── local.html │ │ ├── uv-pick-color/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-pick-color/ │ │ │ │ ├── colors.js │ │ │ │ ├── props.js │ │ │ │ └── uv-pick-color.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-picker/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-picker/ │ │ │ │ ├── props.js │ │ │ │ └── uv-picker.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-popup/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-popup/ │ │ │ │ ├── keypress.js │ │ │ │ ├── props.js │ │ │ │ └── uv-popup.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-qrcode/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-qrcode/ │ │ │ │ ├── cache.js │ │ │ │ ├── gcanvas/ │ │ │ │ │ ├── bridge/ │ │ │ │ │ │ └── bridge-weex.js │ │ │ │ │ ├── context-2d/ │ │ │ │ │ │ ├── FillStyleLinearGradient.js │ │ │ │ │ │ ├── FillStylePattern.js │ │ │ │ │ │ ├── FillStyleRadialGradient.js │ │ │ │ │ │ └── RenderingContext.js │ │ │ │ │ ├── context-webgl/ │ │ │ │ │ │ ├── ActiveInfo.js │ │ │ │ │ │ ├── Buffer.js │ │ │ │ │ │ ├── Framebuffer.js │ │ │ │ │ │ ├── GLenum.js │ │ │ │ │ │ ├── GLmethod.js │ │ │ │ │ │ ├── GLtype.js │ │ │ │ │ │ ├── Program.js │ │ │ │ │ │ ├── Renderbuffer.js │ │ │ │ │ │ ├── RenderingContext.js │ │ │ │ │ │ ├── Shader.js │ │ │ │ │ │ ├── ShaderPrecisionFormat.js │ │ │ │ │ │ ├── Texture.js │ │ │ │ │ │ ├── UniformLocation.js │ │ │ │ │ │ └── classUtils.js │ │ │ │ │ ├── env/ │ │ │ │ │ │ ├── canvas.js │ │ │ │ │ │ ├── image.js │ │ │ │ │ │ └── tool.js │ │ │ │ │ └── index.js │ │ │ │ ├── props.js │ │ │ │ ├── qrcode.js │ │ │ │ ├── queue.js │ │ │ │ └── uv-qrcode.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-radio/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ ├── uv-radio/ │ │ │ │ │ ├── props.js │ │ │ │ │ └── uv-radio.vue │ │ │ │ └── uv-radio-group/ │ │ │ │ ├── props.js │ │ │ │ └── uv-radio-group.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-rate/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-rate/ │ │ │ │ ├── props.js │ │ │ │ └── uv-rate.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-read-more/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-read-more/ │ │ │ │ ├── props.js │ │ │ │ └── uv-read-more.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-row/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ ├── uv-col/ │ │ │ │ │ ├── props.js │ │ │ │ │ └── uv-col.vue │ │ │ │ └── uv-row/ │ │ │ │ ├── props.js │ │ │ │ └── uv-row.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-safe-bottom/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-safe-bottom/ │ │ │ │ └── uv-safe-bottom.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-scroll-list/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-scroll-list/ │ │ │ │ ├── nvue.js │ │ │ │ ├── other.js │ │ │ │ ├── props.js │ │ │ │ ├── scrollWxs.wxs │ │ │ │ └── uv-scroll-list.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-search/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-search/ │ │ │ │ ├── props.js │ │ │ │ └── uv-search.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-skeleton/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-skeleton/ │ │ │ │ ├── props.js │ │ │ │ └── uv-skeleton.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-slider/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-slider/ │ │ │ │ ├── props.js │ │ │ │ └── uv-slider.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-status-bar/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-status-bar/ │ │ │ │ ├── props.js │ │ │ │ └── uv-status-bar.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-steps/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ ├── uv-steps/ │ │ │ │ │ ├── props.js │ │ │ │ │ └── uv-steps.vue │ │ │ │ └── uv-steps-item/ │ │ │ │ ├── props.js │ │ │ │ └── uv-steps-item.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-sticky/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-sticky/ │ │ │ │ ├── props.js │ │ │ │ └── uv-sticky.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-subsection/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-subsection/ │ │ │ │ ├── props.js │ │ │ │ └── uv-subsection.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-swipe-action/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ ├── uv-swipe-action/ │ │ │ │ │ ├── props.js │ │ │ │ │ └── uv-swipe-action.vue │ │ │ │ └── uv-swipe-action-item/ │ │ │ │ ├── index - backup.wxs │ │ │ │ ├── index.wxs │ │ │ │ ├── nvue - backup.js │ │ │ │ ├── nvue.js │ │ │ │ ├── props.js │ │ │ │ ├── uv-swipe-action-item.vue │ │ │ │ └── wxs.js │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-swiper/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ ├── uv-swiper/ │ │ │ │ │ ├── props.js │ │ │ │ │ └── uv-swiper.vue │ │ │ │ └── uv-swiper-indicator/ │ │ │ │ ├── props.js │ │ │ │ └── uv-swiper-indicator.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-switch/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-switch/ │ │ │ │ ├── props.js │ │ │ │ └── uv-switch.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-tabbar/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ ├── uv-tabbar/ │ │ │ │ │ ├── props.js │ │ │ │ │ └── uv-tabbar.vue │ │ │ │ └── uv-tabbar-item/ │ │ │ │ ├── props.js │ │ │ │ └── uv-tabbar-item.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-tabs/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-tabs/ │ │ │ │ ├── props.js │ │ │ │ └── uv-tabs.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-tags/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-tags/ │ │ │ │ ├── props.js │ │ │ │ └── uv-tags.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-text/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-text/ │ │ │ │ ├── props.js │ │ │ │ ├── uv-text.vue │ │ │ │ └── value.js │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-textarea/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-textarea/ │ │ │ │ ├── props.js │ │ │ │ └── uv-textarea.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-toast/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-toast/ │ │ │ │ └── uv-toast.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-toolbar/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-toolbar/ │ │ │ │ ├── props.js │ │ │ │ └── uv-toolbar.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-tooltip/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-tooltip/ │ │ │ │ ├── props.js │ │ │ │ └── uv-tooltip.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-transition/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-transition/ │ │ │ │ ├── createAnimation.js │ │ │ │ ├── props.js │ │ │ │ └── uv-transition.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-ui/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-ui/ │ │ │ │ └── uv-ui.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-ui-tools/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ └── uv-ui-tools/ │ │ │ │ └── uv-ui-tools.vue │ │ │ ├── index.js │ │ │ ├── index.scss │ │ │ ├── libs/ │ │ │ │ ├── config/ │ │ │ │ │ └── config.js │ │ │ │ ├── css/ │ │ │ │ │ ├── color.scss │ │ │ │ │ ├── common.scss │ │ │ │ │ ├── components.scss │ │ │ │ │ ├── variable.scss │ │ │ │ │ └── vue.scss │ │ │ │ ├── function/ │ │ │ │ │ ├── colorGradient.js │ │ │ │ │ ├── debounce.js │ │ │ │ │ ├── digit.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── platform.js │ │ │ │ │ ├── test.js │ │ │ │ │ └── throttle.js │ │ │ │ ├── luch-request/ │ │ │ │ │ ├── adapters/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── core/ │ │ │ │ │ │ ├── InterceptorManager.js │ │ │ │ │ │ ├── Request.js │ │ │ │ │ │ ├── buildFullPath.js │ │ │ │ │ │ ├── defaults.js │ │ │ │ │ │ ├── dispatchRequest.js │ │ │ │ │ │ ├── mergeConfig.js │ │ │ │ │ │ └── settle.js │ │ │ │ │ ├── helpers/ │ │ │ │ │ │ ├── buildURL.js │ │ │ │ │ │ ├── combineURLs.js │ │ │ │ │ │ └── isAbsoluteURL.js │ │ │ │ │ ├── index.d.ts │ │ │ │ │ ├── index.js │ │ │ │ │ ├── utils/ │ │ │ │ │ │ └── clone.js │ │ │ │ │ └── utils.js │ │ │ │ ├── mixin/ │ │ │ │ │ ├── button.js │ │ │ │ │ ├── mixin.js │ │ │ │ │ ├── mpMixin.js │ │ │ │ │ ├── mpShare.js │ │ │ │ │ ├── openType.js │ │ │ │ │ └── touch.js │ │ │ │ └── util/ │ │ │ │ ├── dayjs.js │ │ │ │ └── route.js │ │ │ ├── package.json │ │ │ ├── readme.md │ │ │ └── theme.scss │ │ ├── uv-upload/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ ├── uv-preview-video/ │ │ │ │ │ └── uv-preview-video.vue │ │ │ │ └── uv-upload/ │ │ │ │ ├── mixin.js │ │ │ │ ├── props.js │ │ │ │ ├── utils.js │ │ │ │ └── uv-upload.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── uv-vtabs/ │ │ │ ├── changelog.md │ │ │ ├── components/ │ │ │ │ ├── uv-vtabs/ │ │ │ │ │ ├── props.js │ │ │ │ │ └── uv-vtabs.vue │ │ │ │ └── uv-vtabs-item/ │ │ │ │ └── uv-vtabs-item.vue │ │ │ ├── package.json │ │ │ └── readme.md │ │ └── uv-waterfall/ │ │ ├── changelog.md │ │ ├── components/ │ │ │ └── uv-waterfall/ │ │ │ ├── props.js │ │ │ └── uv-waterfall.vue │ │ ├── package.json │ │ └── readme.md │ ├── utils/ │ │ ├── cookie.js │ │ ├── index.js │ │ ├── querystring.js │ │ ├── router.js │ │ └── util.js │ └── vue.config.js └── yshop-drink-vue3/ ├── .editorconfig ├── .eslintignore ├── .eslintrc-auto-import.json ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .stylelintignore ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── README.md ├── build/ │ └── vite/ │ ├── index.ts │ └── optimize.ts ├── index.html ├── package.json ├── postcss.config.js ├── prettier.config.js ├── public/ │ ├── UEditor/ │ │ ├── dialogs/ │ │ │ ├── anchor/ │ │ │ │ └── anchor.html │ │ │ ├── attachment/ │ │ │ │ ├── attachment.css │ │ │ │ ├── attachment.html │ │ │ │ └── attachment.js │ │ │ ├── background/ │ │ │ │ ├── background.css │ │ │ │ ├── background.html │ │ │ │ └── background.js │ │ │ ├── charts/ │ │ │ │ ├── chart.config.js │ │ │ │ ├── charts.css │ │ │ │ ├── charts.html │ │ │ │ └── charts.js │ │ │ ├── emotion/ │ │ │ │ ├── emotion.css │ │ │ │ ├── emotion.html │ │ │ │ └── emotion.js │ │ │ ├── gmap/ │ │ │ │ └── gmap.html │ │ │ ├── help/ │ │ │ │ ├── help.css │ │ │ │ ├── help.html │ │ │ │ └── help.js │ │ │ ├── image/ │ │ │ │ ├── image.css │ │ │ │ ├── image.html │ │ │ │ └── image.js │ │ │ ├── insertframe/ │ │ │ │ └── insertframe.html │ │ │ ├── internal.js │ │ │ ├── link/ │ │ │ │ └── link.html │ │ │ ├── map/ │ │ │ │ ├── map.html │ │ │ │ └── show.html │ │ │ ├── music/ │ │ │ │ ├── music.css │ │ │ │ ├── music.html │ │ │ │ └── music.js │ │ │ ├── preview/ │ │ │ │ └── preview.html │ │ │ ├── scrawl/ │ │ │ │ ├── scrawl.css │ │ │ │ ├── scrawl.html │ │ │ │ └── scrawl.js │ │ │ ├── searchreplace/ │ │ │ │ ├── searchreplace.html │ │ │ │ └── searchreplace.js │ │ │ ├── snapscreen/ │ │ │ │ └── snapscreen.html │ │ │ ├── spechars/ │ │ │ │ ├── spechars.html │ │ │ │ └── spechars.js │ │ │ ├── table/ │ │ │ │ ├── edittable.css │ │ │ │ ├── edittable.html │ │ │ │ ├── edittable.js │ │ │ │ ├── edittd.html │ │ │ │ └── edittip.html │ │ │ ├── template/ │ │ │ │ ├── config.js │ │ │ │ ├── template.css │ │ │ │ ├── template.html │ │ │ │ └── template.js │ │ │ ├── video/ │ │ │ │ ├── video.css │ │ │ │ ├── video.html │ │ │ │ └── video.js │ │ │ ├── webapp/ │ │ │ │ └── webapp.html │ │ │ └── wordimage/ │ │ │ ├── tangram.js │ │ │ ├── wordimage.html │ │ │ └── wordimage.js │ │ ├── index.html │ │ ├── lang/ │ │ │ ├── en/ │ │ │ │ └── en.js │ │ │ └── zh-cn/ │ │ │ └── zh-cn.js │ │ ├── themes/ │ │ │ ├── default/ │ │ │ │ ├── css/ │ │ │ │ │ └── ueditor.css │ │ │ │ └── dialogbase.css │ │ │ └── iframe.css │ │ ├── third-party/ │ │ │ ├── SyntaxHighlighter/ │ │ │ │ ├── shCore.js │ │ │ │ └── shCoreDefault.css │ │ │ ├── codemirror/ │ │ │ │ ├── codemirror.css │ │ │ │ └── codemirror.js │ │ │ ├── highcharts/ │ │ │ │ ├── adapters/ │ │ │ │ │ ├── mootools-adapter.js │ │ │ │ │ ├── mootools-adapter.src.js │ │ │ │ │ ├── prototype-adapter.js │ │ │ │ │ ├── prototype-adapter.src.js │ │ │ │ │ ├── standalone-framework.js │ │ │ │ │ └── standalone-framework.src.js │ │ │ │ ├── highcharts-more.js │ │ │ │ ├── highcharts-more.src.js │ │ │ │ ├── highcharts.js │ │ │ │ ├── highcharts.src.js │ │ │ │ ├── modules/ │ │ │ │ │ ├── annotations.js │ │ │ │ │ ├── annotations.src.js │ │ │ │ │ ├── canvas-tools.js │ │ │ │ │ ├── canvas-tools.src.js │ │ │ │ │ ├── data.js │ │ │ │ │ ├── data.src.js │ │ │ │ │ ├── drilldown.js │ │ │ │ │ ├── drilldown.src.js │ │ │ │ │ ├── exporting.js │ │ │ │ │ ├── exporting.src.js │ │ │ │ │ ├── funnel.js │ │ │ │ │ ├── funnel.src.js │ │ │ │ │ ├── heatmap.js │ │ │ │ │ ├── heatmap.src.js │ │ │ │ │ ├── map.js │ │ │ │ │ ├── map.src.js │ │ │ │ │ ├── no-data-to-display.js │ │ │ │ │ └── no-data-to-display.src.js │ │ │ │ └── themes/ │ │ │ │ ├── dark-blue.js │ │ │ │ ├── dark-green.js │ │ │ │ ├── gray.js │ │ │ │ ├── grid.js │ │ │ │ └── skies.js │ │ │ ├── jquery-1.10.2.js │ │ │ ├── video-js/ │ │ │ │ ├── video-js.css │ │ │ │ ├── video.dev.js │ │ │ │ └── video.js │ │ │ ├── webuploader/ │ │ │ │ ├── webuploader.css │ │ │ │ ├── webuploader.custom.js │ │ │ │ ├── webuploader.flashonly.js │ │ │ │ ├── webuploader.html5only.js │ │ │ │ ├── webuploader.js │ │ │ │ └── webuploader.withoutimage.js │ │ │ └── zeroclipboard/ │ │ │ └── ZeroClipboard.js │ │ ├── ueditor.all.js │ │ ├── ueditor.config.js │ │ └── ueditor.parse.js │ └── UEditor22/ │ ├── dialogs/ │ │ ├── anchor/ │ │ │ └── anchor.html │ │ ├── attachment/ │ │ │ ├── attachment.css │ │ │ ├── attachment.html │ │ │ └── attachment.js │ │ ├── background/ │ │ │ ├── background.css │ │ │ ├── background.html │ │ │ └── background.js │ │ ├── charts/ │ │ │ ├── chart.config.js │ │ │ ├── charts.css │ │ │ ├── charts.html │ │ │ └── charts.js │ │ ├── emotion/ │ │ │ ├── emotion.css │ │ │ ├── emotion.html │ │ │ └── emotion.js │ │ ├── gmap/ │ │ │ └── gmap.html │ │ ├── help/ │ │ │ ├── help.css │ │ │ ├── help.html │ │ │ └── help.js │ │ ├── image/ │ │ │ ├── image.css │ │ │ ├── image.html │ │ │ └── image.js │ │ ├── insertframe/ │ │ │ └── insertframe.html │ │ ├── internal.js │ │ ├── link/ │ │ │ └── link.html │ │ ├── map/ │ │ │ ├── map.html │ │ │ └── show.html │ │ ├── music/ │ │ │ ├── music.css │ │ │ ├── music.html │ │ │ └── music.js │ │ ├── preview/ │ │ │ └── preview.html │ │ ├── scrawl/ │ │ │ ├── scrawl.css │ │ │ ├── scrawl.html │ │ │ └── scrawl.js │ │ ├── searchreplace/ │ │ │ ├── searchreplace.html │ │ │ └── searchreplace.js │ │ ├── snapscreen/ │ │ │ └── snapscreen.html │ │ ├── spechars/ │ │ │ ├── spechars.html │ │ │ └── spechars.js │ │ ├── table/ │ │ │ ├── edittable.css │ │ │ ├── edittable.html │ │ │ ├── edittable.js │ │ │ ├── edittd.html │ │ │ └── edittip.html │ │ ├── template/ │ │ │ ├── config.js │ │ │ ├── template.css │ │ │ ├── template.html │ │ │ └── template.js │ │ ├── video/ │ │ │ ├── video.css │ │ │ ├── video.html │ │ │ └── video.js │ │ ├── webapp/ │ │ │ └── webapp.html │ │ └── wordimage/ │ │ ├── fClipboard_ueditor.swf │ │ ├── imageUploader.swf │ │ ├── tangram.js │ │ ├── wordimage.html │ │ └── wordimage.js │ ├── index.html │ ├── lang/ │ │ ├── en/ │ │ │ └── en.js │ │ └── zh-cn/ │ │ └── zh-cn.js │ ├── php/ │ │ ├── Uploader.class.php │ │ ├── action_crawler.php │ │ ├── action_list.php │ │ ├── action_upload.php │ │ ├── config.json │ │ └── controller.php │ ├── themes/ │ │ ├── default/ │ │ │ ├── css/ │ │ │ │ └── ueditor.css │ │ │ └── dialogbase.css │ │ └── iframe.css │ ├── third-party/ │ │ ├── SyntaxHighlighter/ │ │ │ ├── shCore.js │ │ │ └── shCoreDefault.css │ │ ├── codemirror/ │ │ │ ├── codemirror.css │ │ │ └── codemirror.js │ │ ├── highcharts/ │ │ │ ├── adapters/ │ │ │ │ ├── mootools-adapter.js │ │ │ │ ├── mootools-adapter.src.js │ │ │ │ ├── prototype-adapter.js │ │ │ │ ├── prototype-adapter.src.js │ │ │ │ ├── standalone-framework.js │ │ │ │ └── standalone-framework.src.js │ │ │ ├── highcharts-more.js │ │ │ ├── highcharts-more.src.js │ │ │ ├── highcharts.js │ │ │ ├── highcharts.src.js │ │ │ ├── modules/ │ │ │ │ ├── annotations.js │ │ │ │ ├── annotations.src.js │ │ │ │ ├── canvas-tools.js │ │ │ │ ├── canvas-tools.src.js │ │ │ │ ├── data.js │ │ │ │ ├── data.src.js │ │ │ │ ├── drilldown.js │ │ │ │ ├── drilldown.src.js │ │ │ │ ├── exporting.js │ │ │ │ ├── exporting.src.js │ │ │ │ ├── funnel.js │ │ │ │ ├── funnel.src.js │ │ │ │ ├── heatmap.js │ │ │ │ ├── heatmap.src.js │ │ │ │ ├── map.js │ │ │ │ ├── map.src.js │ │ │ │ ├── no-data-to-display.js │ │ │ │ └── no-data-to-display.src.js │ │ │ └── themes/ │ │ │ ├── dark-blue.js │ │ │ ├── dark-green.js │ │ │ ├── gray.js │ │ │ ├── grid.js │ │ │ └── skies.js │ │ ├── jquery-1.10.2.js │ │ ├── video-js/ │ │ │ ├── video-js.css │ │ │ ├── video-js.swf │ │ │ ├── video.dev.js │ │ │ └── video.js │ │ ├── webuploader/ │ │ │ ├── Uploader.swf │ │ │ ├── webuploader.css │ │ │ ├── webuploader.custom.js │ │ │ ├── webuploader.flashonly.js │ │ │ ├── webuploader.html5only.js │ │ │ ├── webuploader.js │ │ │ └── webuploader.withoutimage.js │ │ └── zeroclipboard/ │ │ ├── ZeroClipboard.js │ │ └── ZeroClipboard.swf │ ├── ueditor.all.js │ ├── ueditor.config.js │ └── ueditor.parse.js ├── src/ │ ├── App.vue │ ├── api/ │ │ ├── express/ │ │ │ └── index.ts │ │ ├── infra/ │ │ │ ├── apiAccessLog/ │ │ │ │ └── index.ts │ │ │ ├── apiErrorLog/ │ │ │ │ └── index.ts │ │ │ ├── codegen/ │ │ │ │ └── index.ts │ │ │ ├── config/ │ │ │ │ └── index.ts │ │ │ ├── dataSourceConfig/ │ │ │ │ └── index.ts │ │ │ ├── demo/ │ │ │ │ ├── demo01/ │ │ │ │ │ └── index.ts │ │ │ │ ├── demo02/ │ │ │ │ │ └── index.ts │ │ │ │ └── demo03/ │ │ │ │ ├── erp/ │ │ │ │ │ └── index.ts │ │ │ │ ├── inner/ │ │ │ │ │ └── index.ts │ │ │ │ └── normal/ │ │ │ │ └── index.ts │ │ │ ├── file/ │ │ │ │ └── index.ts │ │ │ ├── fileConfig/ │ │ │ │ └── index.ts │ │ │ ├── job/ │ │ │ │ └── index.ts │ │ │ ├── jobLog/ │ │ │ │ └── index.ts │ │ │ └── redis/ │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── login/ │ │ │ ├── index.ts │ │ │ ├── oauth2/ │ │ │ │ └── index.ts │ │ │ └── types.ts │ │ ├── mall/ │ │ │ ├── coupon/ │ │ │ │ ├── index.ts │ │ │ │ └── user/ │ │ │ │ └── index.ts │ │ │ ├── order/ │ │ │ │ └── storeOrder/ │ │ │ │ └── index.ts │ │ │ ├── product/ │ │ │ │ ├── brand.ts │ │ │ │ ├── category.ts │ │ │ │ ├── product.ts │ │ │ │ ├── storeProductRelation/ │ │ │ │ │ └── index.ts │ │ │ │ └── storeProductReply/ │ │ │ │ └── index.ts │ │ │ ├── shop/ │ │ │ │ ├── ads/ │ │ │ │ │ └── index.ts │ │ │ │ ├── materialGroup/ │ │ │ │ │ └── index.ts │ │ │ │ ├── recharge/ │ │ │ │ │ └── index.ts │ │ │ │ ├── service/ │ │ │ │ │ └── index.ts │ │ │ │ └── storeProductRule/ │ │ │ │ └── index.ts │ │ │ └── store/ │ │ │ └── shop/ │ │ │ └── index.ts │ │ ├── member/ │ │ │ ├── user/ │ │ │ │ └── index.ts │ │ │ ├── userAddress/ │ │ │ │ └── index.ts │ │ │ └── userBill/ │ │ │ └── index.ts │ │ ├── message/ │ │ │ └── wechatTemplate/ │ │ │ └── index.ts │ │ ├── mp/ │ │ │ ├── account/ │ │ │ │ └── index.ts │ │ │ ├── account2/ │ │ │ │ └── index.ts │ │ │ ├── autoReply/ │ │ │ │ └── index.ts │ │ │ ├── draft/ │ │ │ │ └── index.ts │ │ │ ├── freePublish/ │ │ │ │ └── index.ts │ │ │ ├── material/ │ │ │ │ └── index.ts │ │ │ ├── menu/ │ │ │ │ └── index.ts │ │ │ ├── message/ │ │ │ │ └── index.ts │ │ │ ├── statistics/ │ │ │ │ └── index.ts │ │ │ ├── tag/ │ │ │ │ └── index.ts │ │ │ └── user/ │ │ │ └── index.ts │ │ ├── pay/ │ │ │ └── merchantDetails/ │ │ │ └── index.ts │ │ ├── score/ │ │ │ ├── order/ │ │ │ │ └── index.ts │ │ │ └── product/ │ │ │ └── index.ts │ │ ├── system/ │ │ │ ├── area/ │ │ │ │ └── index.ts │ │ │ ├── dept/ │ │ │ │ └── index.ts │ │ │ ├── dict/ │ │ │ │ ├── dict.data.ts │ │ │ │ └── dict.type.ts │ │ │ ├── loginLog/ │ │ │ │ └── index.ts │ │ │ ├── mail/ │ │ │ │ ├── account/ │ │ │ │ │ └── index.ts │ │ │ │ ├── log/ │ │ │ │ │ └── index.ts │ │ │ │ └── template/ │ │ │ │ └── index.ts │ │ │ ├── menu/ │ │ │ │ └── index.ts │ │ │ ├── notice/ │ │ │ │ └── index.ts │ │ │ ├── notify/ │ │ │ │ ├── message/ │ │ │ │ │ └── index.ts │ │ │ │ └── template/ │ │ │ │ └── index.ts │ │ │ ├── oauth2/ │ │ │ │ ├── client.ts │ │ │ │ └── token.ts │ │ │ ├── operatelog/ │ │ │ │ └── index.ts │ │ │ ├── permission/ │ │ │ │ └── index.ts │ │ │ ├── post/ │ │ │ │ └── index.ts │ │ │ ├── role/ │ │ │ │ └── index.ts │ │ │ ├── sms/ │ │ │ │ ├── smsChannel/ │ │ │ │ │ └── index.ts │ │ │ │ ├── smsLog/ │ │ │ │ │ └── index.ts │ │ │ │ └── smsTemplate/ │ │ │ │ └── index.ts │ │ │ ├── social/ │ │ │ │ ├── client/ │ │ │ │ │ └── index.ts │ │ │ │ └── user/ │ │ │ │ └── index.ts │ │ │ ├── tenant/ │ │ │ │ └── index.ts │ │ │ ├── tenantPackage/ │ │ │ │ └── index.ts │ │ │ └── user/ │ │ │ ├── index.ts │ │ │ ├── profile.ts │ │ │ └── socialUser.ts │ │ └── tools/ │ │ ├── material.js │ │ └── materialgroup.js │ ├── assets/ │ │ └── map/ │ │ └── json/ │ │ └── china.json │ ├── components/ │ │ ├── AppLinkInput/ │ │ │ ├── AppLinkSelectDialog.vue │ │ │ ├── data.ts │ │ │ └── index.vue │ │ ├── Backtop/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── Backtop.vue │ │ ├── Card/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── CardTitle.vue │ │ ├── ColorInput/ │ │ │ └── index.vue │ │ ├── ConfigGlobal/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── ConfigGlobal.vue │ │ ├── ContentDetailWrap/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── ContentDetailWrap.vue │ │ ├── ContentWrap/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── ContentWrap.vue │ │ ├── CountTo/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── CountTo.vue │ │ ├── Crontab/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── Crontab.vue │ │ ├── Cropper/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ ├── CopperModal.vue │ │ │ ├── Cropper.vue │ │ │ ├── CropperAvatar.vue │ │ │ └── types.ts │ │ ├── Descriptions/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ ├── Descriptions.vue │ │ │ └── DescriptionsItemLabel.vue │ │ ├── Dialog/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── Dialog.vue │ │ ├── DictTag/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── DictTag.vue │ │ ├── DocAlert/ │ │ │ └── index.vue │ │ ├── Draggable/ │ │ │ └── index.vue │ │ ├── Echart/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── Echart.vue │ │ ├── Editor/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── Editor.vue │ │ ├── Error/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── Error.vue │ │ ├── Form/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ ├── Form.vue │ │ │ ├── componentMap.ts │ │ │ ├── components/ │ │ │ │ ├── useRenderCheckbox.tsx │ │ │ │ ├── useRenderRadio.tsx │ │ │ │ └── useRenderSelect.tsx │ │ │ ├── helper.ts │ │ │ └── types.ts │ │ ├── FormCreate/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ ├── components/ │ │ │ │ ├── DictSelect.vue │ │ │ │ └── useApiSelect.tsx │ │ │ ├── config/ │ │ │ │ ├── index.ts │ │ │ │ ├── selectRule.ts │ │ │ │ ├── useDictSelectRule.ts │ │ │ │ ├── useEditorRule.ts │ │ │ │ ├── useSelectRule.ts │ │ │ │ ├── useUploadFileRule.ts │ │ │ │ ├── useUploadImgRule.ts │ │ │ │ └── useUploadImgsRule.ts │ │ │ ├── type/ │ │ │ │ └── index.ts │ │ │ ├── useFormCreateDesigner.ts │ │ │ └── utils/ │ │ │ └── index.ts │ │ ├── Highlight/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── Highlight.vue │ │ ├── IFrame/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── IFrame.vue │ │ ├── Icon/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ ├── Icon.vue │ │ │ ├── IconSelect.vue │ │ │ └── data.ts │ │ ├── ImageViewer/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ ├── ImageViewer.vue │ │ │ └── types.ts │ │ ├── Infotip/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── Infotip.vue │ │ ├── InputPassword/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── InputPassword.vue │ │ ├── InputWithColor/ │ │ │ └── index.vue │ │ ├── MagicCubeEditor/ │ │ │ ├── index.vue │ │ │ └── util.ts │ │ ├── Materials/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ ├── Materials.vue │ │ │ └── editorMaterials.vue │ │ ├── OperateLogV2/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── OperateLogV2.vue │ │ ├── Pagination/ │ │ │ └── index.vue │ │ ├── Qrcode/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── Qrcode.vue │ │ ├── RouterSearch/ │ │ │ └── index.vue │ │ ├── Search/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── Search.vue │ │ ├── ShortcutDateRangePicker/ │ │ │ └── index.vue │ │ ├── SimpleProcessDesigner/ │ │ │ ├── src/ │ │ │ │ ├── addNode.vue │ │ │ │ ├── nodeWrap.vue │ │ │ │ └── util.ts │ │ │ └── theme/ │ │ │ └── workflow.css │ │ ├── Sticky/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── Sticky.vue │ │ ├── SummaryCard/ │ │ │ └── index.vue │ │ ├── Table/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ ├── Table.vue │ │ │ ├── TableSelectForm.vue │ │ │ ├── helper.ts │ │ │ └── types.ts │ │ ├── Tooltip/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── Tooltip.vue │ │ ├── UploadFile/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ ├── UploadFile.vue │ │ │ ├── UploadImg.vue │ │ │ ├── UploadImgs.vue │ │ │ └── useUpload.ts │ │ ├── Verifition/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ ├── Verify/ │ │ │ │ ├── VerifyPoints.vue │ │ │ │ ├── VerifySlide.vue │ │ │ │ └── index.ts │ │ │ ├── Verify.vue │ │ │ └── utils/ │ │ │ ├── ase.ts │ │ │ └── util.ts │ │ ├── VerticalButtonGroup/ │ │ │ └── index.vue │ │ ├── XButton/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ ├── XButton.vue │ │ │ └── XTextButton.vue │ │ └── index.ts │ ├── config/ │ │ └── axios/ │ │ ├── config.ts │ │ ├── errorCode.ts │ │ ├── index.ts │ │ └── service.ts │ ├── directives/ │ │ ├── index.ts │ │ └── permission/ │ │ ├── hasPermi.ts │ │ └── hasRole.ts │ ├── hooks/ │ │ ├── event/ │ │ │ └── useScrollTo.ts │ │ └── web/ │ │ ├── useCache.ts │ │ ├── useConfigGlobal.ts │ │ ├── useCrudSchemas.ts │ │ ├── useDesign.ts │ │ ├── useEmitt.ts │ │ ├── useForm.ts │ │ ├── useGuide.ts │ │ ├── useI18n.ts │ │ ├── useIcon.ts │ │ ├── useLocale.ts │ │ ├── useMessage.ts │ │ ├── useNProgress.ts │ │ ├── useNetwork.ts │ │ ├── useNow.ts │ │ ├── usePageLoading.ts │ │ ├── useTable.ts │ │ ├── useTagsView.ts │ │ ├── useTimeAgo.ts │ │ ├── useTitle.ts │ │ ├── useValidator.ts │ │ └── useWatermark.ts │ ├── layout/ │ │ ├── Layout.vue │ │ └── components/ │ │ ├── AppView.vue │ │ ├── Breadcrumb/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ ├── Breadcrumb.vue │ │ │ └── helper.ts │ │ ├── Collapse/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── Collapse.vue │ │ ├── ContextMenu/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── ContextMenu.vue │ │ ├── Footer/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── Footer.vue │ │ ├── LocaleDropdown/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── LocaleDropdown.vue │ │ ├── Logo/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── Logo.vue │ │ ├── Menu/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ ├── Menu.vue │ │ │ ├── components/ │ │ │ │ ├── useRenderMenuItem.tsx │ │ │ │ └── useRenderMenuTitle.tsx │ │ │ └── helper.ts │ │ ├── Message/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── Message.vue │ │ ├── Screenfull/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── Screenfull.vue │ │ ├── Setting/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ ├── Setting.vue │ │ │ └── components/ │ │ │ ├── ColorRadioPicker.vue │ │ │ ├── InterfaceDisplay.vue │ │ │ └── LayoutRadioPicker.vue │ │ ├── SizeDropdown/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── SizeDropdown.vue │ │ ├── TabMenu/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ ├── TabMenu.vue │ │ │ └── helper.ts │ │ ├── TagsView/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ ├── TagsView.vue │ │ │ └── helper.ts │ │ ├── ThemeSwitch/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ └── ThemeSwitch.vue │ │ ├── ToolHeader.vue │ │ ├── UserInfo/ │ │ │ ├── index.ts │ │ │ └── src/ │ │ │ ├── UserInfo.vue │ │ │ └── components/ │ │ │ ├── LockDialog.vue │ │ │ └── LockPage.vue │ │ └── useRenderLayout.tsx │ ├── locales/ │ │ ├── en.ts │ │ └── zh-CN.ts │ ├── main.ts │ ├── permission.ts │ ├── plugins/ │ │ ├── animate.css/ │ │ │ └── index.ts │ │ ├── echarts/ │ │ │ └── index.ts │ │ ├── elementPlus/ │ │ │ └── index.ts │ │ ├── formCreate/ │ │ │ └── index.ts │ │ ├── svgIcon/ │ │ │ └── index.ts │ │ ├── tongji/ │ │ │ └── index.ts │ │ ├── unocss/ │ │ │ └── index.ts │ │ └── vueI18n/ │ │ ├── helper.ts │ │ └── index.ts │ ├── router/ │ │ ├── index.ts │ │ └── modules/ │ │ └── remaining.ts │ ├── store/ │ │ ├── index.ts │ │ └── modules/ │ │ ├── app.ts │ │ ├── dict.ts │ │ ├── locale.ts │ │ ├── lock.ts │ │ ├── permission.ts │ │ ├── simpleWorkflow.ts │ │ ├── tagsView.ts │ │ └── user.ts │ ├── styles/ │ │ ├── FormCreate/ │ │ │ └── index.scss │ │ ├── global.module.scss │ │ ├── index.scss │ │ ├── theme.scss │ │ ├── var.css │ │ └── variables.scss │ ├── types/ │ │ ├── components.d.ts │ │ ├── configGlobal.d.ts │ │ ├── contextMenu.d.ts │ │ ├── descriptions.d.ts │ │ ├── elementPlus.d.ts │ │ ├── form.d.ts │ │ ├── icon.d.ts │ │ ├── infoTip.d.ts │ │ ├── layout.d.ts │ │ ├── localeDropdown.d.ts │ │ ├── qrcode.d.ts │ │ ├── table.d.ts │ │ └── theme.d.ts │ ├── utils/ │ │ ├── Logger.ts │ │ ├── auth.ts │ │ ├── color.ts │ │ ├── constants.ts │ │ ├── dateUtil.ts │ │ ├── dict.ts │ │ ├── domUtils.ts │ │ ├── download.ts │ │ ├── filt.ts │ │ ├── formCreate.ts │ │ ├── formRules.ts │ │ ├── formatTime.ts │ │ ├── formatter.ts │ │ ├── index.ts │ │ ├── is.ts │ │ ├── jsencrypt.ts │ │ ├── permission.ts │ │ ├── propTypes.ts │ │ ├── routerHelper.ts │ │ ├── tree.ts │ │ └── tsxHelper.ts │ └── views/ │ ├── Error/ │ │ ├── 403.vue │ │ ├── 404.vue │ │ └── 500.vue │ ├── Home/ │ │ ├── Index.vue │ │ ├── Index2.vue │ │ ├── PanelGroupT.vue │ │ ├── echarts-data.ts │ │ └── types.ts │ ├── Login/ │ │ ├── Login.vue │ │ ├── SocialLogin.vue │ │ └── components/ │ │ ├── LoginForm.vue │ │ ├── LoginFormTitle.vue │ │ ├── MobileForm.vue │ │ ├── QrCodeForm.vue │ │ ├── RegisterForm.vue │ │ ├── SSOLogin.vue │ │ ├── index.ts │ │ └── useLogin.ts │ ├── Profile/ │ │ ├── Index.vue │ │ └── components/ │ │ ├── BasicInfo.vue │ │ ├── ProfileUser.vue │ │ ├── ResetPwd.vue │ │ ├── UserAvatar.vue │ │ ├── UserSocial.vue │ │ └── index.ts │ ├── Redirect/ │ │ └── Redirect.vue │ ├── express/ │ │ ├── ExpressForm.vue │ │ ├── ExpressSet.vue │ │ └── index.vue │ ├── infra/ │ │ ├── apiAccessLog/ │ │ │ ├── ApiAccessLogDetail.vue │ │ │ └── index.vue │ │ ├── apiErrorLog/ │ │ │ ├── ApiErrorLogDetail.vue │ │ │ └── index.vue │ │ ├── build/ │ │ │ └── index.vue │ │ ├── codegen/ │ │ │ ├── EditTable.vue │ │ │ ├── ImportTable.vue │ │ │ ├── PreviewCode.vue │ │ │ ├── components/ │ │ │ │ ├── BasicInfoForm.vue │ │ │ │ ├── ColumInfoForm.vue │ │ │ │ ├── GenerateInfoForm.vue │ │ │ │ └── index.ts │ │ │ └── index.vue │ │ ├── config/ │ │ │ ├── ConfigForm.vue │ │ │ └── index.vue │ │ ├── dataSourceConfig/ │ │ │ ├── DataSourceConfigForm.vue │ │ │ └── index.vue │ │ ├── demo/ │ │ │ ├── demo01/ │ │ │ │ ├── Demo01ContactForm.vue │ │ │ │ └── index.vue │ │ │ ├── demo02/ │ │ │ │ ├── Demo02CategoryForm.vue │ │ │ │ └── index.vue │ │ │ └── demo03/ │ │ │ ├── erp/ │ │ │ │ ├── Demo03StudentForm.vue │ │ │ │ ├── components/ │ │ │ │ │ ├── Demo03CourseForm.vue │ │ │ │ │ ├── Demo03CourseList.vue │ │ │ │ │ ├── Demo03GradeForm.vue │ │ │ │ │ └── Demo03GradeList.vue │ │ │ │ └── index.vue │ │ │ ├── inner/ │ │ │ │ ├── Demo03StudentForm.vue │ │ │ │ ├── components/ │ │ │ │ │ ├── Demo03CourseForm.vue │ │ │ │ │ ├── Demo03CourseList.vue │ │ │ │ │ ├── Demo03GradeForm.vue │ │ │ │ │ └── Demo03GradeList.vue │ │ │ │ └── index.vue │ │ │ └── normal/ │ │ │ ├── Demo03StudentForm.vue │ │ │ ├── components/ │ │ │ │ ├── Demo03CourseForm.vue │ │ │ │ └── Demo03GradeForm.vue │ │ │ └── index.vue │ │ ├── druid/ │ │ │ └── index.vue │ │ ├── file/ │ │ │ ├── FileForm.vue │ │ │ └── index.vue │ │ ├── fileConfig/ │ │ │ ├── FileConfigForm.vue │ │ │ └── index.vue │ │ ├── job/ │ │ │ ├── JobDetail.vue │ │ │ ├── JobForm.vue │ │ │ ├── index.vue │ │ │ └── logger/ │ │ │ ├── JobLogDetail.vue │ │ │ └── index.vue │ │ ├── redis/ │ │ │ └── index.vue │ │ ├── server/ │ │ │ └── index.vue │ │ ├── skywalking/ │ │ │ └── index.vue │ │ ├── swagger/ │ │ │ └── index.vue │ │ └── webSocket/ │ │ └── index.vue │ ├── mall/ │ │ ├── coupon/ │ │ │ ├── Form.vue │ │ │ ├── index.vue │ │ │ └── user/ │ │ │ └── OrderRecord.vue │ │ ├── member/ │ │ │ ├── user/ │ │ │ │ ├── UserDetail.vue │ │ │ │ ├── UserForm.vue │ │ │ │ ├── index.vue │ │ │ │ └── yue.vue │ │ │ └── userAddress/ │ │ │ ├── UserAddressForm.vue │ │ │ └── index.vue │ │ ├── order/ │ │ │ └── storeOrder/ │ │ │ ├── OrderDetail.vue │ │ │ ├── OrderRecord.vue │ │ │ ├── OrderSend.vue │ │ │ ├── OrderSendInfo.vue │ │ │ ├── StoreOrderForm.vue │ │ │ ├── StoreOrderRefund.vue │ │ │ ├── StoreOrderRemark.vue │ │ │ ├── index.vue │ │ │ └── work.vue │ │ ├── product/ │ │ │ ├── category/ │ │ │ │ ├── CategoryForm.vue │ │ │ │ └── index.vue │ │ │ ├── storeProduct/ │ │ │ │ ├── CateTree.vue │ │ │ │ ├── StoreProductForm.vue │ │ │ │ └── index.vue │ │ │ ├── storeProductRelation/ │ │ │ │ └── index.vue │ │ │ └── storeProductReply/ │ │ │ └── index.vue │ │ ├── shop/ │ │ │ ├── ads/ │ │ │ │ ├── AdsForm.vue │ │ │ │ └── index.vue │ │ │ ├── recharge/ │ │ │ │ ├── RechargeForm.vue │ │ │ │ └── index.vue │ │ │ ├── service/ │ │ │ │ ├── ServiceForm.vue │ │ │ │ └── index.vue │ │ │ └── storeProductRule/ │ │ │ ├── StoreProductRuleForm.vue │ │ │ └── index.vue │ │ └── store/ │ │ └── shop/ │ │ ├── ShopForm.vue │ │ ├── index.vue │ │ └── map.vue │ ├── message/ │ │ └── wechatTemplate/ │ │ ├── WechatTemplateForm.vue │ │ └── index.vue │ ├── mp/ │ │ ├── account/ │ │ │ ├── AccountForm.vue │ │ │ └── index.vue │ │ ├── account2/ │ │ │ ├── AccountForm.vue │ │ │ └── index.vue │ │ ├── autoReply/ │ │ │ ├── components/ │ │ │ │ ├── ReplyForm.vue │ │ │ │ ├── ReplyTable.vue │ │ │ │ └── types.ts │ │ │ └── index.vue │ │ ├── components/ │ │ │ ├── wx-account-select/ │ │ │ │ ├── index.ts │ │ │ │ └── main.vue │ │ │ ├── wx-location/ │ │ │ │ ├── index.ts │ │ │ │ └── main.vue │ │ │ ├── wx-material-select/ │ │ │ │ ├── index.ts │ │ │ │ ├── main.vue │ │ │ │ └── types.ts │ │ │ ├── wx-msg/ │ │ │ │ ├── card.scss │ │ │ │ ├── comment.scss │ │ │ │ ├── components/ │ │ │ │ │ ├── Msg.vue │ │ │ │ │ ├── MsgEvent.vue │ │ │ │ │ └── MsgList.vue │ │ │ │ ├── index.ts │ │ │ │ ├── main.vue │ │ │ │ └── types.ts │ │ │ ├── wx-music/ │ │ │ │ ├── index.ts │ │ │ │ └── main.vue │ │ │ ├── wx-news/ │ │ │ │ ├── index.ts │ │ │ │ └── main.vue │ │ │ ├── wx-reply/ │ │ │ │ ├── components/ │ │ │ │ │ ├── TabImage.vue │ │ │ │ │ ├── TabMusic.vue │ │ │ │ │ ├── TabNews.vue │ │ │ │ │ ├── TabText.vue │ │ │ │ │ ├── TabVideo.vue │ │ │ │ │ ├── TabVoice.vue │ │ │ │ │ └── types.ts │ │ │ │ ├── index.ts │ │ │ │ └── main.vue │ │ │ ├── wx-video-play/ │ │ │ │ ├── index.ts │ │ │ │ └── main.vue │ │ │ └── wx-voice-play/ │ │ │ ├── index.ts │ │ │ └── main.vue │ │ ├── draft/ │ │ │ ├── components/ │ │ │ │ ├── CoverSelect.vue │ │ │ │ ├── DraftTable.vue │ │ │ │ ├── NewsForm.vue │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── editor-config.ts │ │ │ ├── index.vue │ │ │ └── mock.js │ │ ├── freePublish/ │ │ │ └── index.vue │ │ ├── hooks/ │ │ │ └── useUpload.ts │ │ ├── material/ │ │ │ ├── components/ │ │ │ │ ├── ImageTable.vue │ │ │ │ ├── UploadFile.vue │ │ │ │ ├── UploadVideo.vue │ │ │ │ ├── VideoTable.vue │ │ │ │ ├── VoiceTable.vue │ │ │ │ └── upload.ts │ │ │ └── index.vue │ │ ├── menu/ │ │ │ ├── components/ │ │ │ │ ├── MenuEditor.vue │ │ │ │ ├── MenuPreviewer.vue │ │ │ │ ├── menuOptions.ts │ │ │ │ └── types.ts │ │ │ └── index.vue │ │ ├── message/ │ │ │ ├── MessageTable.vue │ │ │ └── index.vue │ │ ├── statistics/ │ │ │ └── index.vue │ │ ├── tag/ │ │ │ ├── TagForm.vue │ │ │ └── index.vue │ │ └── user/ │ │ ├── UserForm.vue │ │ └── index.vue │ ├── pay/ │ │ └── merchantDetails/ │ │ ├── MerchantDetailsForm.vue │ │ └── index.vue │ ├── score/ │ │ ├── order/ │ │ │ ├── OrderDetail.vue │ │ │ ├── OrderForm.vue │ │ │ └── index.vue │ │ └── product/ │ │ ├── ProductForm.vue │ │ └── index.vue │ └── system/ │ ├── area/ │ │ ├── AreaForm.vue │ │ └── index.vue │ ├── dept/ │ │ ├── DeptForm.vue │ │ └── index.vue │ ├── dict/ │ │ ├── DictTypeForm.vue │ │ ├── data/ │ │ │ ├── DictDataForm.vue │ │ │ └── index.vue │ │ └── index.vue │ ├── loginlog/ │ │ ├── LoginLogDetail.vue │ │ └── index.vue │ ├── mail/ │ │ ├── account/ │ │ │ ├── MailAccountDetail.vue │ │ │ ├── MailAccountForm.vue │ │ │ ├── account.data.ts │ │ │ └── index.vue │ │ ├── log/ │ │ │ ├── MailLogDetail.vue │ │ │ ├── index.vue │ │ │ └── log.data.ts │ │ └── template/ │ │ ├── MailTemplateForm.vue │ │ ├── MailTemplateSendForm.vue │ │ ├── index.vue │ │ └── template.data.ts │ ├── menu/ │ │ ├── MenuForm.vue │ │ └── index.vue │ ├── notice/ │ │ ├── NoticeForm.vue │ │ └── index.vue │ ├── notify/ │ │ ├── message/ │ │ │ ├── NotifyMessageDetail.vue │ │ │ └── index.vue │ │ ├── my/ │ │ │ ├── MyNotifyMessageDetail.vue │ │ │ └── index.vue │ │ └── template/ │ │ ├── NotifyTemplateForm.vue │ │ ├── NotifyTemplateSendForm.vue │ │ └── index.vue │ ├── oauth2/ │ │ ├── client/ │ │ │ ├── ClientForm.vue │ │ │ └── index.vue │ │ └── token/ │ │ └── index.vue │ ├── operatelog/ │ │ ├── OperateLogDetail.vue │ │ └── index.vue │ ├── post/ │ │ ├── PostForm.vue │ │ └── index.vue │ ├── role/ │ │ ├── RoleAssignMenuForm.vue │ │ ├── RoleDataPermissionForm.vue │ │ ├── RoleForm.vue │ │ └── index.vue │ ├── sms/ │ │ ├── channel/ │ │ │ ├── SmsChannelForm.vue │ │ │ └── index.vue │ │ ├── log/ │ │ │ ├── SmsLogDetail.vue │ │ │ └── index.vue │ │ └── template/ │ │ ├── SmsTemplateForm.vue │ │ ├── SmsTemplateSendForm.vue │ │ └── index.vue │ ├── social/ │ │ ├── client/ │ │ │ ├── SocialClientForm.vue │ │ │ └── index.vue │ │ └── user/ │ │ ├── SocialUserDetail.vue │ │ └── index.vue │ ├── tenant/ │ │ ├── TenantForm.vue │ │ └── index.vue │ ├── tenantPackage/ │ │ ├── TenantPackageForm.vue │ │ └── index.vue │ └── user/ │ ├── DeptTree.vue │ ├── UserAssignRoleForm.vue │ ├── UserForm.vue │ ├── UserImportForm.vue │ └── index.vue ├── stylelint.config.js ├── tsconfig.json ├── types/ │ ├── components.d.ts │ ├── custom-types.d.ts │ ├── env.d.ts │ ├── global.d.ts │ └── router.d.ts ├── uno.config.ts └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /.git /.idea *.log *.DS_Store /yshop-drink-boot3/target/ /yshop-drink-vue3/node_modules /yshop-drink-uniapp-vue3/node_moduls ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 yshop 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: README.md ================================================ ## 平台简介 意象点餐(扫码点餐)系统,在线点餐(外卖与自取)小程序模式,支持多门店模式,SaaS多租户模式,基础技术Java17,springboot3、vue3、uniapp(vue3)(支持H5、微信小程序) 采用当前流行技术组合的前后端分离点餐系统: SpringBoot3、Spring Security OAuth2、MybatisPlus、SpringSecurity、jwt、redis、Vue3的前后端分离的系统, 包含外卖与自取、商品管理(多规格sku)、店铺管理、云小票打印、图片素材库、订单管理、积分兑换(积分+金额)、充值、优惠券、充值、多门店、微信公众号、商家中心、提前预约、桌面扫码点餐(单人或者多人协同)、收银台(支持扫码枪与扫码盒子收款)、会员卡、桌台点餐等功能,更适合企业或个人二次开发. 官网地址:[https://www.yixiang.co/](https://www.yixiang.co/) ## 演示地址 | 后台登陆: | https://dc.yixiang.co 账号/密码:admin/admin123 | |---|---| | 门店登陆: | https://dc.yixiang.co 账号/密码:yixiang001/123456789 | | 移动端演示:关注右边公众号点击菜单其他系统体验点餐小程序与点餐H5,其中如果演示使用验证码登陆的点击下验证码默认验证码是9999 | ![输入图片说明](assets/77a93e8c07a913b838a756abadb383b9.png) | ## 视频资料 如果对您有帮助,您可以点右上角 "Star" 支持一下,这样我们才有继续免费下去的动力,谢谢! QQ交流群 (入群前,请在网页右上角点 "Star" ),群里有视频教程与开发文档哦!! 交流QQ群:544263002 ## 项目说明 ``` yshop-drink. Java工程 yshop-drink-vue 后台前端vue3工程 yshop-drink-uniapp-vue3 移动端uniapp(vue3版本)工程,支持微信小程序、h5 ``` ## 本地快速启动 ##### 1、环境要求 ``` jdk17 mysql8 redis6+ node16+ maven3.8+ ``` ##### 2、开发工具 ``` idea vscode hbuilder ``` ##### 3、后端启动 - 3.1 请使用idea打开Java工程,自动会安装依赖 - 3.2 创建数据库且导入工程目录下sql/yixiang-drink.sql 文件 - 3.3 找到项目下的yshop-server 的yml,修改数据库相关信息和redis相关信息,如图: ![输入图片说明](assets/image.png) - 3.4 工程下输入 ``` mvn clean install package '-Dmaven.test.skip=true ``` - 3.5 启动项目,如图 ![输入图片说明](assets/1702544439568.jpg) ##### 4、后台vue启动 - 4.1 vscode 打开vue工程,在目录下输入命令: ``` pnpm install ``` - 4.2 配置api如图 ![输入图片说明](assets/1702544756749.jpg) - 4.3 本地启动: ``` npm run dev ``` ##### 5 移动端uniapp启动 - 5.1 hbuilder导入uniapp项目, - 5.2 配置api ![输入图片说明](assets/WX20231214-171211@2x.png) - 5.3 配置小程序 ![输入图片说明](assets/WX20231214-171416@2x.png) - 5.4 运行小程序 ![输入图片说明](assets/WX20231214-171514@2x.png) - 5.5 运行h5 ![输入图片说明](assets/1702545370856.jpg) - ## 小程序截图 | ![输入图片说明](assets/1000.jpg)| ![输入图片说明](assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20260309235851_552_6.png) | |---|---| | ![输入图片说明](assets/200000.jpg) | ![输入图片说明](assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20260309235857_557_6.png) | | ![输入图片说明](assets/10003.jpg) | ![输入图片说明](assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20260309235856_556_6.png) | ## 后台截图 | ![输入图片说明](assets/3000.png) | |---|---| | ![输入图片说明](assets/3001.png) | | ![输入图片说明](assets/3002.png) | | ![输入图片说明](assets/3003.png) | ![输入图片说明](assets/3004.png) | ![输入图片说明](assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20260310000501_559_6.png) ![输入图片说明](assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20260310001028_564_6.png) ![输入图片说明](assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20260310001057_565_6.png) ## 技术栈 - Spring Boot3 - Spring Security oauth2 - MyBatis - MyBatisPlus - Redis - lombok - hutool - Vue3 - Element UI - uniapp(vue3) ## 特别鸣谢 - ruoyi-vue-pro:https://gitee.com/zhijiantianya/ruoyi-vue-pro - element-plus:https://element-plus.gitee.io/zh-CN/ - vue:https://cn.vuejs.org/ - pay-java-parent:https://gitee.com/egzosn/pay-java-parent - uvui:https://www.uvui.cn/ - uniapp:https://uniapp.dcloud.net.cn/ ## 开源协议 本项目采用比 Apache 2.0 更宽松的 [MIT License](https://gitee.com/guchengwuyue/yshop-drink/blob/master/LICENSE) 开源协议,个人与企业可 100% 免费使用,不用保留类作者、Copyright 信息。 ================================================ FILE: yshop-drink-boot3/.gitignore ================================================ ###################################################################### # Build Tools .gradle /build/ !gradle/wrapper/gradle-wrapper.jar target/ !.mvn/wrapper/maven-wrapper.jar .flattened-pom.xml ###################################################################### # IDE ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr ### NetBeans ### nbproject/private/ build/* nbbuild/ dist/ nbdist/ .nb-gradle/ ###################################################################### # Others *.log *.xml.versionsBackup *.swp !*/build/*.java !*/build/*.html !*/build/*.xml ### JRebel ### rebel.xml application-my.yaml ================================================ FILE: yshop-drink-boot3/README.md ================================================ 意向订餐系统,类似肯德基点餐小程序模式,支持多门店模式,基础技术Java,uniapp(支持H5、微信小程序) 采用当前流行技术组合的前后端分离点餐系统: SpringBoot3+jdk17、Spring Security OAuth2、MybatisPlus、SpringSecurity、jwt、redis、Vue3的前后端分离的系统, 包含店铺管理、积分兑换、云小票打印、图片素材库、订单管理、多规格sku、积分、优惠券、充值、多门店、微信公众号等功能,更适合企业或个人二次开发. ================================================ FILE: yshop-drink-boot3/lombok.config ================================================ config.stopBubbling = true lombok.tostring.callsuper=CALL lombok.equalsandhashcode.callsuper=CALL lombok.accessors.chain=true ================================================ FILE: yshop-drink-boot3/pom.xml ================================================ 4.0.0 co.yixiang.boot yshop ${revision} pom yshop-dependencies yshop-framework yshop-server yshop-module-system yshop-module-infra yshop-module-pay yshop-module-mp yshop-module-mall yshop-module-message yshop-module-score yshop-module-express yshop-module-marketing yshop-module-member ${project.artifactId} yshop项目基础脚手架 https://gitee.com/guchengwuyue/yshop-drink 3.0.0 17 ${java.version} ${java.version} 3.2.2 3.11.0 1.5.0 1.18.30 3.2.2 1.5.5.Final UTF-8 co.yixiang.boot yshop-dependencies ${revision} pom import org.apache.maven.plugins maven-surefire-plugin ${maven-surefire-plugin.version} org.apache.maven.plugins maven-compiler-plugin ${maven-compiler-plugin.version} org.springframework.boot spring-boot-configuration-processor ${spring.boot.version} org.projectlombok lombok ${lombok.version} org.mapstruct mapstruct-processor ${mapstruct.version} false -parameters org.codehaus.mojo flatten-maven-plugin org.codehaus.mojo flatten-maven-plugin ${flatten-maven-plugin.version} resolveCiFriendliesOnly true flatten flatten process-resources clean flatten.clean clean huaweicloud huawei https://mirrors.huaweicloud.com/repository/maven/ aliyunmaven aliyun https://maven.aliyun.com/repository/public ================================================ FILE: yshop-drink-boot3/script/docker/Docker-HOWTO.md ================================================ # Docker Build & Up 目标: 快速部署体验系统,帮助了解系统之间的依赖关系。 依赖:docker compose v2,删除`name: yshop-system`,降低`version`版本为`3.3`以下,支持`docker-compose`。 ## 功能文件列表 ```text . ├── Docker-HOWTO.md ├── docker-compose.yml ├── docker.env <-- 提供docker-compose环境变量配置 ├── yshop-server │ └── Dockerfile └── yshop-ui-admin ├── .dockerignore ├── Dockerfile └── nginx.conf <-- 提供基础配置,gzip压缩、api转发 ``` ## 构建 jar 包 ```shell # 创建maven缓存volume docker volume create --name yshop-maven-repo docker run -it --rm --name yshop-maven \ -v yshop-maven-repo:/root/.m2 \ -v $PWD:/usr/src/mymaven \ -w /usr/src/mymaven \ maven mvn clean install package '-Dmaven.test.skip=true' ``` ## 构建启动服务 ```shell docker compose --env-file docker.env up -d ``` 首次运行会自动构建容器。可以通过`docker compose build [service]`来手动构建所有或某个docker镜像 `--env-file docker.env`为可选参数,只是展示了通过`.env`文件配置容器启动的环境变量,`docker-compose.yml`本身已经提供足够的默认参数来正常运行系统。 ## 服务器的宿主机端口映射 - admin ui: http://localhost:8080 - api server: http://localhost:48080 - mysql: root/123456, port: 3306 - redis: port: 6379 ================================================ FILE: yshop-drink-boot3/script/docker/docker-compose.yml ================================================ version: "3.4" name: yshop-system services: mysql: container_name: yshop-mysql image: mysql:8 restart: unless-stopped tty: true ports: - "3306:3306" environment: MYSQL_DATABASE: ${MYSQL_DATABASE:-yixiang-drink} MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-123456} volumes: - mysql:/var/lib/mysql/ - ./sql/mysql/yixiang-drink.sql:/docker-entrypoint-initdb.d/yixiang-drink.sql:ro redis: container_name: yshop-redis image: redis:6-alpine restart: unless-stopped ports: - "6379:6379" volumes: - redis:/data server: container_name: yshop-server build: context: ./yshop-server/ image: yshop-server restart: unless-stopped ports: - "48080:48080" environment: # https://github.com/polovyivan/docker-pass-configs-to-container SPRING_PROFILES_ACTIVE: local JAVA_OPTS: ${JAVA_OPTS:- -Xms512m -Xmx512m -Djava.security.egd=file:/dev/./urandom } ARGS: --spring.datasource.dynamic.datasource.master.url=${MASTER_DATASOURCE_URL:-jdbc:mysql://yshop-mysql:3306/yixiang-drink?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true} --spring.datasource.dynamic.datasource.master.username=${MASTER_DATASOURCE_USERNAME:-root} --spring.datasource.dynamic.datasource.master.password=${MASTER_DATASOURCE_PASSWORD:-123456} --spring.datasource.dynamic.datasource.slave.url=${SLAVE_DATASOURCE_URL:-jdbc:mysql://yshop-mysql:3306/yixiang-drink?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true} --spring.datasource.dynamic.datasource.slave.username=${SLAVE_DATASOURCE_USERNAME:-root} --spring.datasource.dynamic.datasource.slave.password=${SLAVE_DATASOURCE_PASSWORD:-123456} --spring.data.redis.host=${REDIS_HOST:-yshop-redis} depends_on: - mysql - redis admin: container_name: yshop-admin build: context: ./yshop-ui-admin args: NODE_ENV: ENV=${NODE_ENV:-production} PUBLIC_PATH=${PUBLIC_PATH:-/} VUE_APP_TITLE=${VUE_APP_TITLE:-意象商城管理系统} VUE_APP_BASE_API=${VUE_APP_BASE_API:-/prod-api} VUE_APP_APP_NAME=${VUE_APP_APP_NAME:-/} VUE_APP_TENANT_ENABLE=${VUE_APP_TENANT_ENABLE:-true} VUE_APP_CAPTCHA_ENABLE=${VUE_APP_CAPTCHA_ENABLE:-true} VUE_APP_DOC_ENABLE=${VUE_APP_DOC_ENABLE:-true} VUE_APP_BAIDU_CODE=${VUE_APP_BAIDU_CODE:-fadc1bd5db1a1d6f581df60a1807f8ab} image: yshop-admin restart: unless-stopped ports: - "8080:80" depends_on: - server volumes: mysql: driver: local redis: driver: local ================================================ FILE: yshop-drink-boot3/script/docker/docker.env ================================================ ## mysql MYSQL_DATABASE=yixiang-drink MYSQL_ROOT_PASSWORD=123456 ## server JAVA_OPTS=-Xms512m -Xmx512m -Djava.security.egd=file:/dev/./urandom MASTER_DATASOURCE_URL=jdbc:mysql://yshop-mysql:3306/${MYSQL_DATABASE}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true MASTER_DATASOURCE_USERNAME=root MASTER_DATASOURCE_PASSWORD=${MYSQL_ROOT_PASSWORD} SLAVE_DATASOURCE_URL=${MASTER_DATASOURCE_URL} SLAVE_DATASOURCE_USERNAME=${MASTER_DATASOURCE_USERNAME} SLAVE_DATASOURCE_PASSWORD=${MASTER_DATASOURCE_PASSWORD} REDIS_HOST=yshop-redis ## admin NODE_ENV=production PUBLIC_PATH=/ VUE_APP_TITLE=意象商城管理系统 VUE_APP_BASE_API=/prod-api VUE_APP_APP_NAME=/ VUE_APP_TENANT_ENABLE=true VUE_APP_CAPTCHA_ENABLE=true VUE_APP_DOC_ENABLE=true VUE_APP_BAIDU_CODE=fadc1bd5db1a1d6f581df60a1807f8ab ================================================ FILE: yshop-drink-boot3/script/shell/deploy.sh ================================================ #!/bin/bash set -e DATE=$(date +%Y%m%d%H%M) # 基础路径 BASE_PATH=/work/projects/yshop-server # 编译后 jar 的地址。部署时,Jenkins 会上传 jar 包到该目录下 SOURCE_PATH=$BASE_PATH/build # 服务名称。同时约定部署服务的 jar 包名字也为它。 SERVER_NAME=yshop-server # 环境 PROFILES_ACTIVE=development # 健康检查 URL HEALTH_CHECK_URL=http://127.0.0.1:48080/actuator/health/ # heapError 存放路径 HEAP_ERROR_PATH=$BASE_PATH/heapError # JVM 参数 JAVA_OPS="-Xms512m -Xmx512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$HEAP_ERROR_PATH" # SkyWalking Agent 配置 #export SW_AGENT_NAME=$SERVER_NAME #export SW_AGENT_COLLECTOR_BACKEND_SERVICES=192.168.0.84:11800 #export SW_GRPC_LOG_SERVER_HOST=192.168.0.84 #export SW_AGENT_TRACE_IGNORE_PATH="Redisson/PING,/actuator/**,/admin/**" #export JAVA_AGENT=-javaagent:/work/skywalking/apache-skywalking-apm-bin/agent/skywalking-agent.jar # 备份 function backup() { # 如果不存在,则无需备份 if [ ! -f "$BASE_PATH/$SERVER_NAME.jar" ]; then echo "[backup] $BASE_PATH/$SERVER_NAME.jar 不存在,跳过备份" # 如果存在,则备份到 backup 目录下,使用时间作为后缀 else echo "[backup] 开始备份 $SERVER_NAME ..." cp $BASE_PATH/$SERVER_NAME.jar $BASE_PATH/backup/$SERVER_NAME-$DATE.jar echo "[backup] 备份 $SERVER_NAME 完成" fi } # 最新构建代码 移动到项目环境 function transfer() { echo "[transfer] 开始转移 $SERVER_NAME.jar" # 删除原 jar 包 if [ ! -f "$BASE_PATH/$SERVER_NAME.jar" ]; then echo "[transfer] $BASE_PATH/$SERVER_NAME.jar 不存在,跳过删除" else echo "[transfer] 移除 $BASE_PATH/$SERVER_NAME.jar 完成" rm $BASE_PATH/$SERVER_NAME.jar fi # 复制新 jar 包 echo "[transfer] 从 $SOURCE_PATH 中获取 $SERVER_NAME.jar 并迁移至 $BASE_PATH ...." cp $SOURCE_PATH/$SERVER_NAME.jar $BASE_PATH echo "[transfer] 转移 $SERVER_NAME.jar 完成" } # 停止:优雅关闭之前已经启动的服务 function stop() { echo "[stop] 开始停止 $BASE_PATH/$SERVER_NAME" PID=$(ps -ef | grep $BASE_PATH/$SERVER_NAME | grep -v "grep" | awk '{print $2}') # 如果 Java 服务启动中,则进行关闭 if [ -n "$PID" ]; then # 正常关闭 echo "[stop] $BASE_PATH/$SERVER_NAME 运行中,开始 kill [$PID]" kill -15 $PID # 等待最大 120 秒,直到关闭完成。 for ((i = 0; i < 120; i++)) do sleep 1 PID=$(ps -ef | grep $BASE_PATH/$SERVER_NAME | grep -v "grep" | awk '{print $2}') if [ -n "$PID" ]; then echo -e ".\c" else echo "[stop] 停止 $BASE_PATH/$SERVER_NAME 成功" break fi done # 如果正常关闭失败,那么进行强制 kill -9 进行关闭 if [ -n "$PID" ]; then echo "[stop] $BASE_PATH/$SERVER_NAME 失败,强制 kill -9 $PID" kill -9 $PID fi # 如果 Java 服务未启动,则无需关闭 else echo "[stop] $BASE_PATH/$SERVER_NAME 未启动,无需停止" fi } # 启动:启动后端项目 function start() { # 开启启动前,打印启动参数 echo "[start] 开始启动 $BASE_PATH/$SERVER_NAME" echo "[start] JAVA_OPS: $JAVA_OPS" echo "[start] JAVA_AGENT: $JAVA_AGENT" echo "[start] PROFILES: $PROFILES_ACTIVE" # 开始启动 BUILD_ID=dontKillMe nohup java -server $JAVA_OPS $JAVA_AGENT -jar $BASE_PATH/$SERVER_NAME.jar --spring.profiles.active=$PROFILES_ACTIVE & echo "[start] 启动 $BASE_PATH/$SERVER_NAME 完成" } # 健康检查:自动判断后端项目是否正常启动 function healthCheck() { # 如果配置健康检查,则进行健康检查 if [ -n "$HEALTH_CHECK_URL" ]; then # 健康检查最大 120 秒,直到健康检查通过 echo "[healthCheck] 开始通过 $HEALTH_CHECK_URL 地址,进行健康检查"; for ((i = 0; i < 120; i++)) do # 请求健康检查地址,只获取状态码。 result=`curl -I -m 10 -o /dev/null -s -w %{http_code} $HEALTH_CHECK_URL || echo "000"` # 如果状态码为 200,则说明健康检查通过 if [ "$result" == "200" ]; then echo "[healthCheck] 健康检查通过"; break # 如果状态码非 200,则说明未通过。sleep 1 秒后,继续重试 else echo -e ".\c" sleep 1 fi done # 健康检查未通过,则异常退出 shell 脚本,不继续部署。 if [ ! "$result" == "200" ]; then echo "[healthCheck] 健康检查不通过,可能部署失败。查看日志,自行判断是否启动成功"; tail -n 10 nohup.out exit 1; # 健康检查通过,打印最后 10 行日志,可能部署的人想看下日志。 else tail -n 10 nohup.out fi # 如果未配置健康检查,则 sleep 120 秒,人工看日志是否部署成功。 else echo "[healthCheck] HEALTH_CHECK_URL 未配置,开始 sleep 120 秒"; sleep 120 echo "[healthCheck] sleep 120 秒完成,查看日志,自行判断是否启动成功"; tail -n 50 nohup.out fi } # 部署 function deploy() { cd $BASE_PATH # 备份原 jar backup # 停止 Java 服务 stop # 部署新 jar transfer # 启动 Java 服务 start # 健康检查 healthCheck } deploy ================================================ FILE: yshop-drink-boot3/sql/yixiang-drink-open.sql ================================================ [File too large to display: 15.5 MB] ================================================ FILE: yshop-drink-boot3/yshop-dependencies/pom.xml ================================================ 4.0.0 co.yixiang.boot yshop-dependencies ${revision} pom ${project.artifactId} 基础 bom 文件,管理整个项目的依赖版本 https://gitee.com/guchengwuyue/yshop-drink 3.0.0 1.5.0 3.2.2 2.2.0 4.3.0 1.2.21 3.5.5 3.5.5 4.3.0 1.4.10 2.2.11 3.26.0 8.1.3.62 2.3.0 2.2.7 9.0.0 3.2.1 0.33.0 8.0.1.RELEASE 1.0.13 5.2.0 7.0.1 2.0.3 1.17.2 1.18.30 1.5.5.Final 5.8.25 6.0.0-M10 3.3.3 2.3 1.0.5 1.2.83 33.0.0-jre 5.1.0 2.14.5 3.10.0 0.1.55 2.9.1 2.7.0 3.0.6 3.5.0 4.11.0 2.15.1 8.5.7 4.6.4 2.2.1 3.1.880 2.0.5 1.6.6-beta2 2.12.2 4.6.0 1.0.5 2.14.9 3.3.3 org.springframework.boot spring-boot-dependencies ${spring.boot.version} pom import io.github.mouzt bizlog-sdk ${bizlog-sdk.version} org.springframework.boot spring-boot-starter co.yixiang.boot yshop-spring-boot-starter-biz-tenant ${revision} co.yixiang.boot yshop-spring-boot-starter-biz-data-permission ${revision} co.yixiang.boot yshop-spring-boot-starter-biz-ip ${revision} org.springframework.boot spring-boot-configuration-processor ${spring.boot.version} co.yixiang.boot yshop-spring-boot-starter-web ${revision} co.yixiang.boot yshop-spring-boot-starter-security ${revision} co.yixiang.boot yshop-spring-boot-starter-websocket ${revision} com.github.xiaoymin knife4j-openapi3-jakarta-spring-boot-starter ${knife4j.version} org.springdoc springdoc-openapi-starter-webmvc-api ${springdoc.version} org.springdoc springdoc-openapi-ui ${springdoc.version} co.yixiang.boot yshop-spring-boot-starter-mybatis ${revision} com.alibaba druid-spring-boot-3-starter ${druid.version} com.baomidou mybatis-plus-spring-boot3-starter ${mybatis-plus.version} com.baomidou mybatis-plus-generator ${mybatis-plus-generator.version} com.baomidou dynamic-datasource-spring-boot3-starter ${dynamic-datasource.version} com.github.yulichang mybatis-plus-join-boot-starter ${mybatis-plus-join.version} com.fhs-opensource easy-trans-spring-boot-starter ${easy-trans.version} org.springframework spring-context org.springframework.cloud spring-cloud-commons com.fhs-opensource easy-trans-mybatis-plus-extend ${easy-trans.version} com.fhs-opensource easy-trans-anno ${easy-trans.version} co.yixiang.boot yshop-spring-boot-starter-redis ${revision} org.redisson redisson-spring-boot-starter ${redisson.version} org.springframework.boot spring-boot-starter-actuator com.dameng DmJdbcDriver18 ${dm8.jdbc.version} co.yixiang.boot yshop-spring-boot-starter-job ${revision} co.yixiang.boot yshop-spring-boot-starter-mq ${revision} org.apache.rocketmq rocketmq-spring-boot-starter ${rocketmq-spring.version} co.yixiang.boot yshop-spring-boot-starter-protection ${revision} com.baomidou lock4j-redisson-spring-boot-starter ${lock4j.version} redisson-spring-boot-starter org.redisson co.yixiang.boot yshop-spring-boot-starter-monitor ${revision} org.apache.skywalking apm-toolkit-trace ${skywalking.version} org.apache.skywalking apm-toolkit-logback-1.x ${skywalking.version} org.apache.skywalking apm-toolkit-opentracing ${skywalking.version} io.opentracing opentracing-api ${opentracing.version} io.opentracing opentracing-util ${opentracing.version} io.opentracing opentracing-noop ${opentracing.version} de.codecentric spring-boot-admin-starter-server ${spring-boot-admin.version} de.codecentric spring-boot-admin-server-cloud de.codecentric spring-boot-admin-starter-client ${spring-boot-admin.version} co.yixiang.boot yshop-spring-boot-starter-test ${revision} test org.mockito mockito-inline ${mockito-inline.version} org.springframework.boot spring-boot-starter-test ${spring.boot.version} asm org.ow2.asm org.mockito mockito-core com.github.fppt jedis-mock ${jedis-mock.version} uk.co.jemos.podam podam ${podam.version} org.flowable flowable-spring-boot-starter-process ${flowable.version} org.flowable flowable-spring-boot-starter-actuator ${flowable.version} co.yixiang.boot yshop-common ${revision} co.yixiang.boot yshop-spring-boot-starter-excel ${revision} org.projectlombok lombok ${lombok.version} org.mapstruct mapstruct ${mapstruct.version} org.mapstruct mapstruct-jdk8 ${mapstruct.version} org.mapstruct mapstruct-processor ${mapstruct.version} cn.hutool hutool-all ${hutool-5.version} org.dromara.hutool hutool-extra ${hutool-6.version} com.alibaba easyexcel ${easyexcel.verion} commons-io commons-io ${commons-io.version} org.apache.tika tika-core ${tika-core.version} org.apache.velocity velocity-engine-core ${velocity.version} com.alibaba fastjson ${fastjson.version} com.google.guava guava ${guava.version} com.google.inject guice ${guice.version} com.alibaba transmittable-thread-local ${transmittable-thread-local.version} commons-net commons-net ${commons-net.version} com.jcraft jsch ${jsch.version} com.xingyuv spring-boot-starter-captcha-plus ${captcha-plus.version} org.lionsoul ip2region ${ip2region.version} org.jsoup jsoup ${jsoup.version} com.squareup.okio okio ${okio.version} com.squareup.okhttp3 okhttp ${okhttp3.version} io.minio minio ${minio.version} com.aliyun aliyun-java-sdk-core ${aliyun-java-sdk-core.version} opentracing-api io.opentracing opentracing-util io.opentracing com.aliyun aliyun-java-sdk-dysmsapi ${aliyun-java-sdk-dysmsapi.version} com.tencentcloudapi tencentcloud-sdk-java-sms ${tencentcloud-sdk-java.version} com.xingyuv spring-boot-starter-justauth ${justauth.version} cn.hutool hutool-core com.github.binarywang weixin-java-pay ${weixin-java.version} com.github.binarywang wx-java-mp-spring-boot-starter ${weixin-java.version} com.github.binarywang wx-java-miniapp-spring-boot-starter ${weixin-java.version} org.jeecgframework.jimureport jimureport-spring-boot3-starter ${jimureport.version} com.alibaba druid xerces xercesImpl ${xercesImpl.version} com.egzosn pay-spring-boot-starter ${pay.boot.version} com.egzosn pay-java-ali ${pay.version} com.egzosn pay-java-wx ${pay.version} com.egzosn pay-java-web-support ${pay.version} com.google.zxing core ${zxing.version} org.codehaus.mojo flatten-maven-plugin ${flatten-maven-plugin.version} resolveCiFriendliesOnly true flatten flatten process-resources clean flatten.clean clean ================================================ FILE: yshop-drink-boot3/yshop-framework/pom.xml ================================================ 4.0.0 yshop co.yixiang.boot ${revision} pom yshop-common yshop-spring-boot-starter-mybatis yshop-spring-boot-starter-redis yshop-spring-boot-starter-web yshop-spring-boot-starter-security yshop-spring-boot-starter-websocket yshop-spring-boot-starter-monitor yshop-spring-boot-starter-protection yshop-spring-boot-starter-job yshop-spring-boot-starter-mq yshop-spring-boot-starter-excel yshop-spring-boot-starter-test yshop-spring-boot-starter-biz-tenant yshop-spring-boot-starter-biz-data-permission yshop-spring-boot-starter-biz-ip yshop-framework 该包是技术组件,每个子包,代表一个组件。每个组件包括两部分: 1. core 包:是该组件的核心封装 2. config 包:是该组件基于 Spring 的配置 技术组件,也分成两类: 1. 框架组件:和我们熟悉的 MyBatis、Redis 等等的拓展 2. 业务组件:和业务相关的组件的封装,例如说数据字典、操作日志等等。 如果是业务组件,Maven 名字会包含 biz https://gitee.com/guchengwuyue/yshop-drink ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/pom.xml ================================================ co.yixiang.boot yshop-framework ${revision} 4.0.0 yshop-common jar ${project.artifactId} 定义基础 pojo 类、枚举、工具类等等 https://gitee.com/guchengwuyue/yshop-drink org.springframework spring-core provided org.springframework spring-expression provided org.springframework spring-aop provided org.aspectj aspectjweaver provided org.springframework.boot spring-boot-configuration-processor true org.springframework spring-web provided jakarta.servlet jakarta.servlet-api provided org.springdoc springdoc-openapi-starter-webmvc-api provided org.apache.skywalking apm-toolkit-trace org.projectlombok lombok org.mapstruct mapstruct org.mapstruct mapstruct-jdk8 org.mapstruct mapstruct-processor com.google.guava guava provided com.fasterxml.jackson.core jackson-databind provided com.fasterxml.jackson.core jackson-core provided com.fasterxml.jackson.datatype jackson-datatype-jsr310 provided org.slf4j slf4j-api provided jakarta.validation jakarta.validation-api provided cn.hutool hutool-all com.alibaba transmittable-thread-local com.fhs-opensource easy-trans-anno org.springframework.boot spring-boot-starter-test test ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/constant/ShopConstants.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.framework.common.constant; /** * 商城统一常量 * @author hupeng * @since 2020-02-27 */ public interface ShopConstants { /** * 订单自动取消时间(分钟) */ long ORDER_OUTTIME_UNPAY = 30; /** * 订单自动收货时间(分钟) */ long ORDER_OUTTIME_UNCONFIRM = 60; /** * redis订单未付款key */ String REDIS_ORDER_OUTTIME_UNPAY_QUEUE = "order-unpay-cancel-queue"; /** * redis订单收货key */ String REDIS_ORDER_OUTTIME_UNCONFIRM = "order:unconfirm:"; /** * redis拼团key */ String REDIS_PINK_CANCEL_KEY = "pink:cancel:"; /** * 微信支付service */ String YSHOP_WEIXIN_PAY_SERVICE = "yshop_weixin_pay_service"; /** * 微信支付小程序service */ String YSHOP_WEIXIN_MINI_PAY_SERVICE = "yshop_weixin_mini_pay_service"; /** * 微信支付app service */ String YSHOP_WEIXIN_APP_PAY_SERVICE = "yshop_weixin_app_pay_service"; /** * 微信公众号service */ String YSHOP_WEIXIN_MP_SERVICE = "yshop_weixin_mp_service"; /** * 微信小程序service */ String YSHOP_WEIXIN_MA_SERVICE = "yshop_weixin_ma_service"; /** * 商城默认密码 */ String YSHOP_DEFAULT_PWD = "123456"; /** * 商城默认注册图片 */ String YSHOP_DEFAULT_AVATAR = "https://image.dayouqiantu.cn/5e79f6cfd33b6.png"; /** * 腾讯地图地址解析 */ String QQ_MAP_URL = "https://apis.map.qq.com/ws/geocoder/v1/"; /** * redis首页键 */ String YSHOP_REDIS_INDEX_KEY = "yshop:index_data"; /** * 配置列表缓存 */ String YSHOP_REDIS_CONFIG_DATAS = "yshop:config_datas"; /** * 充值方案 */ String YSHOP_RECHARGE_PRICE_WAYS = "yshop_recharge_price_ways"; /** * 首页banner */ String YSHOP_HOME_BANNER = "yshop_home_banner"; /** * 首页菜单 */ String YSHOP_HOME_MENUS = "yshop_home_menus"; /** * 首页滚动新闻 */ String YSHOP_HOME_ROLL_NEWS = "yshop_home_roll_news"; /** * 热门搜索 */ String YSHOP_HOT_SEARCH = "yshop_hot_search"; /** * 个人中心菜单 */ String YSHOP_MY_MENUES = "yshop_my_menus"; /** * 秒杀时间段 */ String YSHOP_SECKILL_TIME = "yshop_seckill_time"; /** * 签到天数 */ String YSHOP_SIGN_DAY_NUM = "yshop_sign_day_num"; /** * 打印机配置 */ String YSHOP_ORDER_PRINT_COUNT = "order_print_count"; /** * 飞蛾用户信息 */ String YSHOP_FEI_E_USER = "fei_e_user"; /** * 飞蛾用户密钥 */ String YSHOP_FEI_E_UKEY= "fei_e_ukey"; /** * 打印机配置 */ String YSHOP_ORDER_PRINT_COUNT_DETAIL = "order_print_count_detail"; /** * 短信验证码长度 */ int YSHOP_SMS_SIZE = 6; /** * 短信缓存时间 */ long YSHOP_SMS_REDIS_TIME = 600L; //零标识 String YSHOP_ZERO = "0"; //业务标识标识 String YSHOP_ONE = "1"; //目前完成任务数量是3 int TASK_FINISH_COUNT = 3; int YSHOP_ONE_NUM = 1; String YSHOP_ORDER_CACHE_KEY = "yshop:order"; String YSHOP_ORDER_SALE_STATUS_KEY = "yshop:order:sale:status"; long YSHOP_ORDER_CACHE_TIME = 3600L; String WECHAT_MENUS = "wechat_menus"; String YSHOP_EXPRESS_SERVICE = "yshop_express_service"; String YSHOP_REDIS_SYS_CITY_KEY = "yshop:city_list"; String YSHOP_REDIS_CITY_KEY = "yshop:city"; String YSHOP_APP_LOGIN_USER = "app-online-token:"; String YSHOP_WECHAT_PUSH_REMARK = "yshop为您服务!"; String DEFAULT_UNI_H5_URL = "https://h5.yixiang.co"; String YSHOP_MINI_SESSION_KET = "yshop:session_key:"; /**公众号二维码*/ String WECHAT_FOLLOW_IMG="wechat_follow_img"; /**后台api地址*/ String ADMIN_API_URL="admin_api_url"; //快递查询接口Logistic String KDNIAO_LOGISTIC_QUERY="https://api.kdniao.com/Ebusiness/EbusinessOrderHandle.aspx"; //跳转到扫码页面 String PAGE_GOOD_HOME = "pages/components/pages/scan/scan"; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/constant/SystemConfigConstants.java ================================================ package co.yixiang.yshop.framework.common.constant; public class SystemConfigConstants { //地址配置 public final static String API="api"; public final static String API_URL="api_url"; public final static String SITE_URL="site_url"; public final static String UNI_SITE_URL="uni_site_url"; public final static String TENGXUN_MAP_KEY="tengxun_map_key"; public final static String FILE_STORE_MODE="file_store_mode"; //业务相关配置 public final static String IMAGEARR="imageArr"; public final static String INTERGRAL_FULL="integral_full"; public final static String INTERGRAL_MAX="integral_max"; public final static String INTERGRAL_RATIO="integral_ratio"; public final static String ORDER_CANCEL_JOB_TIME="order_cancel_job_time"; public final static String STORE_BROKERAGE_OPEN="store_brokerage_open"; public final static String STORE_BROKERAGE_RATIO="store_brokerage_ratio"; public final static String STORE_BROKERAGE_STATU="store_brokerage_statu"; public final static String STORE_BROKERAGE_TWO="store_brokerage_two"; public final static String STORE_FREE_POSTAGE="store_free_postage"; public final static String STORE_POSTAGE="store_postage"; public final static String STORE_SEFL_MENTION="store_self_mention"; public final static String STORE_USER_MIN_RECHARGE="store_user_min_recharge"; public final static String USER_EXTRACT_MIN_PRICE="user_extract_min_price"; public final static String YSHOP_SHOW_RECHARGE = "yshop_show_recharge"; //微信相关配置 public final static String WECHAT_APPID="wechat_appid"; public final static String WECHAT_APPSECRET="wechat_appsecret"; public final static String WECHAT_AVATAR="wechat_avatar"; public final static String WECHAT_ENCODE="wechat_encode"; public final static String WECHAT_ENCODINGAESKEY="wechat_encodingaeskey"; public final static String WECHAT_ID="wechat_id"; public final static String WECHAT_NAME="wechat_name"; public final static String WECHAT_QRCODE="wechat_qrcode"; public final static String WECHAT_SHARE_IMG="wechat_share_img"; public final static String WECHAT_SHARE_SYNOPSIS="wechat_share_synopsis"; public final static String WECHAT_SHARE_TITLE="wechat_share_title"; public final static String WECHAT_SOURCEID="wechat_sourceid"; public final static String WECHAT_TOKEN="wechat_token"; public final static String WECHAT_MA_TOKEN="wechat_ma_token"; public final static String WECHAT_MA_ENCODINGAESKEY="wechat_ma_encodingaeskey"; public final static String WECHAT_TYPE="wechat_type"; public final static String WXAPP_APPID="wxapp_appId"; public final static String WXAPP_SECRET="wxapp_secret"; public final static String WXPAY_APPID="wxpay_appId"; public final static String WXPAY_KEYPATH="wxpay_keyPath"; public final static String WXPAY_MCHID="wxpay_mchId"; public final static String WXPAY_MCHKEY="wxpay_mchKey"; public final static String WX_NATIVE_APP_APPID="wx_native_app_appId"; public final static String EXP_APPID = "exp_appId"; //播放状态变化事件,detail = {code} public static final String BINDSTATECHANGE = "bindstatechange"; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/core/IntArrayValuable.java ================================================ package co.yixiang.yshop.framework.common.core; /** * 可生成 Int 数组的接口 * * @author yshop */ public interface IntArrayValuable { /** * @return int 数组 */ int[] array(); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/core/KeyValue.java ================================================ package co.yixiang.yshop.framework.common.core; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; /** * Key Value 的键值对 * * @author yshop */ @Data @NoArgsConstructor @AllArgsConstructor public class KeyValue implements Serializable { private K key; private V value; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/enums/CommonStatusEnum.java ================================================ package co.yixiang.yshop.framework.common.enums; import cn.hutool.core.util.ObjUtil; import co.yixiang.yshop.framework.common.core.IntArrayValuable; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.Arrays; /** * 通用状态枚举 * * @author yshop */ @Getter @AllArgsConstructor public enum CommonStatusEnum implements IntArrayValuable { ENABLE(0, "开启"), DISABLE(1, "关闭"); public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CommonStatusEnum::getStatus).toArray(); /** * 状态值 */ private final Integer status; /** * 状态名 */ private final String name; @Override public int[] array() { return ARRAYS; } public static boolean isEnable(Integer status) { return ObjUtil.equal(ENABLE.status, status); } public static boolean isDisable(Integer status) { return ObjUtil.equal(DISABLE.status, status); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/enums/DateIntervalEnum.java ================================================ package co.yixiang.yshop.framework.common.enums; import cn.hutool.core.util.ArrayUtil; import co.yixiang.yshop.framework.common.core.IntArrayValuable; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.Arrays; /** * 时间间隔的枚举 * * @author dhb52 */ @Getter @AllArgsConstructor public enum DateIntervalEnum implements IntArrayValuable { DAY(1, "天"), WEEK(2, "周"), MONTH(3, "月"), QUARTER(4, "季度"), YEAR(5, "年") ; public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(DateIntervalEnum::getInterval).toArray(); /** * 类型 */ private final Integer interval; /** * 名称 */ private final String name; @Override public int[] array() { return ARRAYS; } public static DateIntervalEnum valueOf(Integer interval) { return ArrayUtil.firstMatch(item -> item.getInterval().equals(interval), DateIntervalEnum.values()); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/enums/DocumentEnum.java ================================================ package co.yixiang.yshop.framework.common.enums; import lombok.AllArgsConstructor; import lombok.Getter; /** * 文档地址 * * @author yshop */ @Getter @AllArgsConstructor public enum DocumentEnum { REDIS_INSTALL("https://gitee.com/zhijiantianya/yixiang-drink/issues/I4VCSJ", "Redis 安装文档"), TENANT("https://www.yixiang.co", "SaaS 多租户文档"); private final String url; private final String memo; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/enums/OrderInfoEnum.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.framework.common.enums; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.stream.Stream; /** * @author hupeng * 订单相关枚举 */ @Getter @AllArgsConstructor public enum OrderInfoEnum { STATUS_NE1(-1,"申请退款"), STATUS_NE2(-2,"退款成功"), STATUS_0(0,"默认"), STATUS_1(1,"待收货"), STATUS_2(2,"已收货"), STATUS_3(3,"已完成"), PAY_STATUS_0(0,"未支付"), PAY_STATUS_1(1,"已支付"), REFUND_STATUS_0(0,"正常"), REFUND_STATUS_1(1,"退款中"), REFUND_STATUS_2(2,"已退款"), BARGAIN_STATUS_1(1,"参与中"), BARGAIN_STATUS_2(2,"参与失败"), BARGAIN_STATUS_3(3,"参与成功"), PINK_STATUS_1(1,"进行中"), PINK_STATUS_2(2,"已完成"), PINK_STATUS_3(3,"未完成"), PINK_REFUND_STATUS_0(0,"拼团正常"), PINK_REFUND_STATUS_1(1,"拼团已退款"), CANCEL_STATUS_0(0,"正常"), CANCEL_STATUS_1(1,"已取消"), CONFIRM_STATUS_0(0,"正常"), CONFIRM_STATUS_1(1,"确认"), PAY_CHANNEL_0(0,"公众号/H5支付渠道"), PAY_CHANNEL_1(1,"小程序支付渠道"), DESK_ORDER_STATUS_CONFIRM(0,"确认完成"), DESK_ORDER_STATUS_ING(1,"就餐中"), SHIPPIING_TYPE_1(1,"快递"), SHIPPIING_TYPE_2(2,"门店自提"); private Integer value; private String desc; public static OrderInfoEnum toType(int value) { return Stream.of(OrderInfoEnum.values()) .filter(p -> p.value == value) .findAny() .orElse(null); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/enums/OrderTypeEnum.java ================================================ package co.yixiang.yshop.framework.common.enums; import lombok.AllArgsConstructor; import lombok.Getter; /** * @author hupeng * 类型枚举 */ @Getter @AllArgsConstructor public enum OrderTypeEnum { TYPE_WORK("work","工作台"), TYPE_COMMON("common","普通"); private String value; private String desc; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/enums/PayIdEnum.java ================================================ package co.yixiang.yshop.framework.common.enums; import lombok.AllArgsConstructor; import lombok.Getter; /** * @author hupeng * 支付ID枚举 */ @Getter @AllArgsConstructor public enum PayIdEnum { WX_H5("wx_h5","微信支付H5"), WX_MINIAPP("wx_miniapp","微信支付小程序"), WX_WECHAT("wx_wechat","微信支付公众号"), WX_PC("wx_pc","微信支付pc"), WX_APP("wx_app","微信支付app"), ALI_H5("ali_h5","支付宝H5"), ALI_MINIAPP("ali_miniapp","支付宝小程序"), ALI_WECHAT("ali_wechat","支付宝公众号"), ALI_PC("ali_pc","支付宝pc"), ALI_APP("ali_app","支付宝app"); private String value; private String desc; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/enums/ShopCommonEnum.java ================================================ package co.yixiang.yshop.framework.common.enums; import lombok.AllArgsConstructor; import lombok.Getter; /** * @author hupeng * 商城常用枚举 */ @Getter @AllArgsConstructor public enum ShopCommonEnum { STORE_MODE_1(1,"本地存储"), STORE_MODE_2(2,"云存储"), ENABLE_1(1,"开启"), ENABLE_2(2,"关闭"), EXTRACT_MINUS_1(-1,"提现未通过"), EXTRACT_0(0,"提现审核中"), EXTRACT_1(1,"提现已完成"), IS_FINISH_0(0,"未完成"), IS_FINISH_1(1,"已完成"), IS_FOREVER_0(0,"不是永久"), IS_FOREVER_1(1,"永久"), AGREE_1(1,"同意"), AGREE_2(2,"拒绝"), IS_PERMANENT_0(0,"限制"), IS_PERMANENT_1(1,"不限制"), IS_STATUS_0(0,"否"), IS_STATUS_1(1,"是"), IS_PROMOTER_0(0,"默认"), IS_PROMOTER_1(1,"是客服"), IS_NEW_0(0,"默认"), IS_NEW_1(1,"新品"), IS_SUB_0(0,"不单独分佣"), IS_SUB_1(1,"单独分佣"), GRADE_0(0,"一级推荐人"), GRADE_1(1,"二级推荐人"), REPLY_0(0,"未回复"), REPLY_1(1,"已回复"), ADD_1(1,"增加"), ADD_2(2,"减少"), DELETE_0(0,"未删除"), DELETE_1(1,"已删除"), SHOW_0(0,"不显示"), SHOW_1(1,"显示"), DEFAULT_0(0,"不是默认"), DEFAULT_1(1,"默认"); private Integer value; private String desc; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/enums/TerminalEnum.java ================================================ package co.yixiang.yshop.framework.common.enums; import co.yixiang.yshop.framework.common.core.IntArrayValuable; import lombok.Getter; import lombok.RequiredArgsConstructor; import java.util.Arrays; /** * 终端的枚举 * * @author yshop */ @RequiredArgsConstructor @Getter public enum TerminalEnum implements IntArrayValuable { UNKNOWN(0, "未知"), // 目的:在无法解析到 terminal 时,使用它 WECHAT_MINI_PROGRAM(10, "微信小程序"), WECHAT_WAP(11, "微信公众号"), H5(20, "H5 网页"), APP(31, "手机 App"), ; public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TerminalEnum::getTerminal).toArray(); /** * 终端 */ private final Integer terminal; /** * 终端名 */ private final String name; @Override public int[] array() { return ARRAYS; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/enums/UserTypeEnum.java ================================================ package co.yixiang.yshop.framework.common.enums; import cn.hutool.core.util.ArrayUtil; import co.yixiang.yshop.framework.common.core.IntArrayValuable; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.Arrays; /** * 全局用户类型枚举 */ @AllArgsConstructor @Getter public enum UserTypeEnum implements IntArrayValuable { MEMBER(1, "会员"), // 面向 c 端,普通用户 ADMIN(2, "管理员"); // 面向 b 端,管理后台 public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(UserTypeEnum::getValue).toArray(); /** * 类型 */ private final Integer value; /** * 类型名 */ private final String name; public static UserTypeEnum valueOf(Integer value) { return ArrayUtil.firstMatch(userType -> userType.getValue().equals(value), UserTypeEnum.values()); } @Override public int[] array() { return ARRAYS; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/enums/WebFilterOrderEnum.java ================================================ package co.yixiang.yshop.framework.common.enums; /** * Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期 * * 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 enums 包下 * * @author yshop */ public interface WebFilterOrderEnum { int CORS_FILTER = Integer.MIN_VALUE; int TRACE_FILTER = CORS_FILTER + 1; int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500; // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等 int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面 int API_ACCESS_LOG_FILTER = -103; // 需要保证在 RequestBodyCacheFilter 后面 int XSS_FILTER = -102; // 需要保证在 RequestBodyCacheFilter 后面 // Spring Security Filter 默认为 -100,可见 org.springframework.boot.autoconfigure.security.SecurityProperties 配置属性类 int TENANT_SECURITY_FILTER = -99; // 需要保证在 Spring Security 过滤器后面 int FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面 int DEMO_FILTER = Integer.MAX_VALUE; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/exception/ErrorCode.java ================================================ package co.yixiang.yshop.framework.common.exception; import co.yixiang.yshop.framework.common.exception.enums.GlobalErrorCodeConstants; import co.yixiang.yshop.framework.common.exception.enums.ServiceErrorCodeRange; import lombok.Data; /** * 错误码对象 * * 全局错误码,占用 [0, 999], 参见 {@link GlobalErrorCodeConstants} * 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange} * * TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备 */ @Data public class ErrorCode { /** * 错误码 */ private final Integer code; /** * 错误提示 */ private final String msg; public ErrorCode(Integer code, String message) { this.code = code; this.msg = message; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/exception/ServerException.java ================================================ package co.yixiang.yshop.framework.common.exception; import co.yixiang.yshop.framework.common.exception.enums.GlobalErrorCodeConstants; import lombok.Data; import lombok.EqualsAndHashCode; /** * 服务器异常 Exception */ @Data @EqualsAndHashCode(callSuper = true) public final class ServerException extends RuntimeException { /** * 全局错误码 * * @see GlobalErrorCodeConstants */ private Integer code; /** * 错误提示 */ private String message; /** * 空构造方法,避免反序列化问题 */ public ServerException() { } public ServerException(ErrorCode errorCode) { this.code = errorCode.getCode(); this.message = errorCode.getMsg(); } public ServerException(Integer code, String message) { this.code = code; this.message = message; } public Integer getCode() { return code; } public ServerException setCode(Integer code) { this.code = code; return this; } @Override public String getMessage() { return message; } public ServerException setMessage(String message) { this.message = message; return this; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/exception/ServiceException.java ================================================ package co.yixiang.yshop.framework.common.exception; import co.yixiang.yshop.framework.common.exception.enums.ServiceErrorCodeRange; import lombok.Data; import lombok.EqualsAndHashCode; /** * 业务逻辑异常 Exception */ @Data @EqualsAndHashCode(callSuper = true) public final class ServiceException extends RuntimeException { /** * 业务错误码 * * @see ServiceErrorCodeRange */ private Integer code; /** * 错误提示 */ private String message; /** * 空构造方法,避免反序列化问题 */ public ServiceException() { } public ServiceException(ErrorCode errorCode) { this.code = errorCode.getCode(); this.message = errorCode.getMsg(); } public ServiceException(Integer code, String message) { this.code = code; this.message = message; } public Integer getCode() { return code; } public ServiceException setCode(Integer code) { this.code = code; return this; } @Override public String getMessage() { return message; } public ServiceException setMessage(String message) { this.message = message; return this; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/exception/enums/GlobalErrorCodeConstants.java ================================================ package co.yixiang.yshop.framework.common.exception.enums; import co.yixiang.yshop.framework.common.exception.ErrorCode; /** * 全局错误码枚举 * 0-999 系统异常编码保留 * * 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status * 虽然说,HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的 * 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。 * * @author yshop */ public interface GlobalErrorCodeConstants { ErrorCode SUCCESS = new ErrorCode(0, "成功"); // ========== 客户端错误段 ========== ErrorCode BAD_REQUEST = new ErrorCode(400, "请求参数不正确"); ErrorCode UNAUTHORIZED = new ErrorCode(401, "账号未登录"); ErrorCode FORBIDDEN = new ErrorCode(403, "没有该操作权限"); ErrorCode NOT_FOUND = new ErrorCode(404, "请求未找到"); ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确"); ErrorCode LOCKED = new ErrorCode(423, "请求失败,请稍后重试"); // 并发请求,不允许 ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "请求过于频繁,请稍后重试"); // ========== 服务端错误段 ========== ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常"); ErrorCode NOT_IMPLEMENTED = new ErrorCode(501, "功能未实现/未开启"); ErrorCode ERROR_CONFIGURATION = new ErrorCode(502, "错误的配置项"); // ========== 自定义错误段 ========== ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求 ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作"); ErrorCode UNKNOWN = new ErrorCode(999, "未知错误"); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/exception/enums/ServiceErrorCodeRange.java ================================================ package co.yixiang.yshop.framework.common.exception.enums; /** * 业务异常的错误码区间,解决:解决各模块错误码定义,避免重复,在此只声明不做实际使用 * * 一共 10 位,分成四段 * * 第一段,1 位,类型 * 1 - 业务级别异常 * x - 预留 * 第二段,3 位,系统类型 * 001 - 用户系统 * 002 - 商品系统 * 003 - 订单系统 * 004 - 支付系统 * 005 - 优惠劵系统 * ... - ... * 第三段,3 位,模块 * 不限制规则。 * 一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子: * 001 - OAuth2 模块 * 002 - User 模块 * 003 - MobileCode 模块 * 第四段,3 位,错误码 * 不限制规则。 * 一般建议,每个模块自增。 * * @author yshop */ public class ServiceErrorCodeRange { // 模块 infra 错误码区间 [1-001-000-000 ~ 1-002-000-000) // 模块 system 错误码区间 [1-002-000-000 ~ 1-003-000-000) // 模块 report 错误码区间 [1-003-000-000 ~ 1-004-000-000) // 模块 member 错误码区间 [1-004-000-000 ~ 1-005-000-000) // 模块 mp 错误码区间 [1-006-000-000 ~ 1-007-000-000) // 模块 pay 错误码区间 [1-007-000-000 ~ 1-008-000-000) // 模块 bpm 错误码区间 [1-009-000-000 ~ 1-010-000-000) // 模块 product 错误码区间 [1-008-000-000 ~ 1-009-000-000) // 模块 trade 错误码区间 [1-011-000-000 ~ 1-012-000-000) // 模块 promotion 错误码区间 [1-013-000-000 ~ 1-014-000-000) // 模块 crm 错误码区间 [1-020-000-000 ~ 1-021-000-000) } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/exception/util/ServiceExceptionUtil.java ================================================ package co.yixiang.yshop.framework.common.exception.util; import co.yixiang.yshop.framework.common.exception.ErrorCode; import co.yixiang.yshop.framework.common.exception.ServiceException; import co.yixiang.yshop.framework.common.exception.enums.GlobalErrorCodeConstants; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; /** * {@link ServiceException} 工具类 * * 目的在于,格式化异常信息提示。 * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化 * */ @Slf4j public class ServiceExceptionUtil { // ========== 和 ServiceException 的集成 ========== public static ServiceException exception(ErrorCode errorCode) { return exception0(errorCode.getCode(), errorCode.getMsg()); } public static ServiceException exception(ErrorCode errorCode, Object... params) { return exception0(errorCode.getCode(), errorCode.getMsg(), params); } public static ServiceException exception0(Integer code, String messagePattern, Object... params) { String message = doFormat(code, messagePattern, params); return new ServiceException(code, message); } public static ServiceException invalidParamException(String messagePattern, Object... params) { return exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), messagePattern, params); } // ========== 格式化方法 ========== /** * 将错误编号对应的消息使用 params 进行格式化。 * * @param code 错误编号 * @param messagePattern 消息模版 * @param params 参数 * @return 格式化后的提示 */ @VisibleForTesting public static String doFormat(int code, String messagePattern, Object... params) { StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50); int i = 0; int j; int l; for (l = 0; l < params.length; l++) { j = messagePattern.indexOf("{}", i); if (j == -1) { log.error("[doFormat][参数过多:错误码({})|错误内容({})|参数({})", code, messagePattern, params); if (i == 0) { return messagePattern; } else { sbuf.append(messagePattern.substring(i)); return sbuf.toString(); } } else { sbuf.append(messagePattern, i, j); sbuf.append(params[l]); i = j + 2; } } if (messagePattern.indexOf("{}", i) != -1) { log.error("[doFormat][参数过少:错误码({})|错误内容({})|参数({})", code, messagePattern, params); } sbuf.append(messagePattern.substring(i)); return sbuf.toString(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/params/QueryParam.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.framework.common.params; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; @Data @Schema(description = "用户 APP - 查询参数对象") public abstract class QueryParam implements Serializable{ private static final long serialVersionUID = -3263921252635611410L; @Schema(description = "页码,默认为1", required = true) private Integer page =1; @Schema(description = "页大小,默认为10", required = true) private Integer limit = 10; @Schema(description = "搜索字符串", required = true) private String keyword; // @Schema(description = "当前第几页", required = true) // public void setCurrent(Integer current) { // if (current == null || current <= 0){ // this.page = 1; // }else{ // this.page = current; // } // } // // public void setSize(Integer size) { // if (size == null || size <= 0){ // this.limit = 10; // }else{ // this.limit = size; // } // } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/pojo/CommonResult.java ================================================ package co.yixiang.yshop.framework.common.pojo; import co.yixiang.yshop.framework.common.exception.ErrorCode; import co.yixiang.yshop.framework.common.exception.ServiceException; import co.yixiang.yshop.framework.common.exception.enums.GlobalErrorCodeConstants; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import org.springframework.util.Assert; import java.io.Serializable; import java.util.Objects; /** * 通用返回 * * @param 数据泛型 */ @Data public class CommonResult implements Serializable { /** * 错误码 * * @see ErrorCode#getCode() */ private Integer code; /** * 返回数据 */ private T data; /** * 错误提示,用户可阅读 * * @see ErrorCode#getMsg() () */ private String msg; /** * 将传入的 result 对象,转换成另外一个泛型结果的对象 * * 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。 * * @param result 传入的 result 对象 * @param 返回的泛型 * @return 新的 CommonResult 对象 */ public static CommonResult error(CommonResult result) { return error(result.getCode(), result.getMsg()); } public static CommonResult error(Integer code, String message) { Assert.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code), "code 必须是错误的!"); CommonResult result = new CommonResult<>(); result.code = code; result.msg = message; return result; } public static CommonResult error(ErrorCode errorCode) { return error(errorCode.getCode(), errorCode.getMsg()); } public static CommonResult success(T data) { CommonResult result = new CommonResult<>(); result.code = GlobalErrorCodeConstants.SUCCESS.getCode(); result.data = data; result.msg = ""; return result; } public static boolean isSuccess(Integer code) { return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode()); } @JsonIgnore // 避免 jackson 序列化 public boolean isSuccess() { return isSuccess(code); } @JsonIgnore // 避免 jackson 序列化 public boolean isError() { return !isSuccess(); } // ========= 和 Exception 异常体系集成 ========= /** * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常 */ public void checkError() throws ServiceException { if (isSuccess()) { return; } // 业务异常 throw new ServiceException(code, msg); } /** * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常 * 如果没有,则返回 {@link #data} 数据 */ @JsonIgnore // 避免 jackson 序列化 public T getCheckedData() { checkError(); return data; } public static CommonResult error(ServiceException serviceException) { return error(serviceException.getCode(), serviceException.getMessage()); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/pojo/PageParam.java ================================================ package co.yixiang.yshop.framework.common.pojo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotNull; import java.io.Serializable; @Schema(description="分页参数") @Data public class PageParam implements Serializable { private static final Integer PAGE_NO = 1; private static final Integer PAGE_SIZE = 10; /** * 每页条数 - 不分页 * * 例如说,导出接口,可以设置 {@link #pageSize} 为 -1 不分页,查询所有数据。 */ public static final Integer PAGE_SIZE_NONE = -1; @Schema(description = "页码,从 1 开始", requiredMode = Schema.RequiredMode.REQUIRED,example = "1") @NotNull(message = "页码不能为空") @Min(value = 1, message = "页码最小值为 1") private Integer pageNo = PAGE_NO; @Schema(description = "每页条数,最大值为 100", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") @NotNull(message = "每页条数不能为空") @Min(value = 1, message = "每页条数最小值为 1") @Max(value = 100, message = "每页条数最大值为 100") private Integer pageSize = PAGE_SIZE; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/pojo/PageResult.java ================================================ package co.yixiang.yshop.framework.common.pojo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; import java.util.ArrayList; import java.util.List; @Schema(description = "分页结果") @Data public final class PageResult implements Serializable { @Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED) private List list; @Schema(description = "总量", requiredMode = Schema.RequiredMode.REQUIRED) private Long total; public PageResult() { } public PageResult(List list, Long total) { this.list = list; this.total = total; } public PageResult(Long total) { this.list = new ArrayList<>(); this.total = total; } public static PageResult empty() { return new PageResult<>(0L); } public static PageResult empty(Long total) { return new PageResult<>(total); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/pojo/SortablePageParam.java ================================================ package co.yixiang.yshop.framework.common.pojo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import java.util.List; @Schema(description = "可排序的分页参数") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class SortablePageParam extends PageParam { @Schema(description = "排序字段") private List sortingFields; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/pojo/SortingField.java ================================================ package co.yixiang.yshop.framework.common.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; /** * 排序字段 DTO * * 类名加了 ing 的原因是,避免和 ES SortField 重名。 */ @Data @NoArgsConstructor @AllArgsConstructor public class SortingField implements Serializable { /** * 顺序 - 升序 */ public static final String ORDER_ASC = "asc"; /** * 顺序 - 降序 */ public static final String ORDER_DESC = "desc"; /** * 字段 */ private String field; /** * 顺序 */ private String order; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/serializer/BigDecimalSerializer.java ================================================ package co.yixiang.yshop.framework.common.serializer; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import java.io.IOException; import java.math.BigDecimal; import java.text.DecimalFormat; /** * @author :LionCity * @date :Created in 2020-05-30 14:12 * @description: * @modified By: * @version: */ public class BigDecimalSerializer extends JsonSerializer { @Override public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException { if (value != null && !"".equals(value)) { DecimalFormat df2 =new DecimalFormat("0.00"); gen.writeString(df2.format(value)); } else { gen.writeString(value + ""); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/serializer/DoubleSerializer.java ================================================ package co.yixiang.yshop.framework.common.serializer; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import java.io.IOException; import java.text.DecimalFormat; /** * @author :LionCity * @date :Created in 2020-05-30 14:12 * @description: * @modified By: * @version: */ public class DoubleSerializer extends JsonSerializer { @Override public void serialize(Double value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException { if (value != null && !"".equals(value)) { DecimalFormat df2 =new DecimalFormat("0.00"); gen.writeString(df2.format(value)); } else { gen.writeString(value + ""); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/cache/CacheUtils.java ================================================ package co.yixiang.yshop.framework.common.util.cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import java.time.Duration; import java.util.concurrent.Executors; /** * Cache 工具类 * * @author yshop */ public class CacheUtils { /** * 构建异步刷新的 LoadingCache 对象 * * 注意:如果你的缓存和 ThreadLocal 有关系,要么自己处理 ThreadLocal 的传递,要么使用 {@link #buildCache(Duration, CacheLoader)} 方法 * * 或者简单理解: * 1、和“人”相关的,使用 {@link #buildCache(Duration, CacheLoader)} 方法 * 2、和“全局”、“系统”相关的,使用当前缓存方法 * * @param duration 过期时间 * @param loader CacheLoader 对象 * @return LoadingCache 对象 */ public static LoadingCache buildAsyncReloadingCache(Duration duration, CacheLoader loader) { return CacheBuilder.newBuilder() // 只阻塞当前数据加载线程,其他线程返回旧值 .refreshAfterWrite(duration) // 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程 .build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool())); // TODO yshop:可能要思考下,未来要不要做成可配置 } /** * 构建同步刷新的 LoadingCache 对象 * * @param duration 过期时间 * @param loader CacheLoader 对象 * @return LoadingCache 对象 */ public static LoadingCache buildCache(Duration duration, CacheLoader loader) { return CacheBuilder.newBuilder().refreshAfterWrite(duration).build(loader); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/collection/ArrayUtils.java ================================================ package co.yixiang.yshop.framework.common.util.collection; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.IterUtil; import cn.hutool.core.util.ArrayUtil; import java.util.Collection; import java.util.function.Consumer; import java.util.function.Function; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertList; /** * Array 工具类 * * @author yshop */ public class ArrayUtils { /** * 将 object 和 newElements 合并成一个数组 * * @param object 对象 * @param newElements 数组 * @param 泛型 * @return 结果数组 */ @SafeVarargs public static Consumer[] append(Consumer object, Consumer... newElements) { if (object == null) { return newElements; } Consumer[] result = ArrayUtil.newArray(Consumer.class, 1 + newElements.length); result[0] = object; System.arraycopy(newElements, 0, result, 1, newElements.length); return result; } public static V[] toArray(Collection from, Function mapper) { return toArray(convertList(from, mapper)); } @SuppressWarnings("unchecked") public static T[] toArray(Collection from) { if (CollectionUtil.isEmpty(from)) { return (T[]) (new Object[0]); } return ArrayUtil.toArray(from, (Class) IterUtil.getElementType(from.iterator())); } public static T get(T[] array, int index) { if (null == array || index >= array.length) { return null; } return array[index]; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/collection/CollectionUtils.java ================================================ package co.yixiang.yshop.framework.common.util.collection; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ArrayUtil; import com.google.common.collect.ImmutableMap; import java.util.*; import java.util.function.*; import java.util.stream.Collectors; import java.util.stream.Stream; import static java.util.Arrays.asList; /** * Collection 工具类 * * @author yshop */ public class CollectionUtils { public static boolean containsAny(Object source, Object... targets) { return asList(targets).contains(source); } public static boolean isAnyEmpty(Collection... collections) { return Arrays.stream(collections).anyMatch(CollectionUtil::isEmpty); } public static boolean anyMatch(Collection from, Predicate predicate) { return from.stream().anyMatch(predicate); } public static List filterList(Collection from, Predicate predicate) { if (CollUtil.isEmpty(from)) { return new ArrayList<>(); } return from.stream().filter(predicate).collect(Collectors.toList()); } public static List distinct(Collection from, Function keyMapper) { if (CollUtil.isEmpty(from)) { return new ArrayList<>(); } return distinct(from, keyMapper, (t1, t2) -> t1); } public static List distinct(Collection from, Function keyMapper, BinaryOperator cover) { if (CollUtil.isEmpty(from)) { return new ArrayList<>(); } return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values()); } public static List convertList(T[] from, Function func) { if (ArrayUtil.isEmpty(from)) { return new ArrayList<>(); } return convertList(Arrays.asList(from), func); } public static List convertList(Collection from, Function func) { if (CollUtil.isEmpty(from)) { return new ArrayList<>(); } return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toList()); } public static List convertList(Collection from, Function func, Predicate filter) { if (CollUtil.isEmpty(from)) { return new ArrayList<>(); } return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList()); } public static List convertListByFlatMap(Collection from, Function> func) { if (CollUtil.isEmpty(from)) { return new ArrayList<>(); } return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList()); } public static List convertListByFlatMap(Collection from, Function mapper, Function> func) { if (CollUtil.isEmpty(from)) { return new ArrayList<>(); } return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList()); } public static List mergeValuesFromMap(Map> map) { return map.values() .stream() .flatMap(List::stream) .collect(Collectors.toList()); } public static Set convertSet(Collection from) { return convertSet(from, v -> v); } public static Set convertSet(Collection from, Function func) { if (CollUtil.isEmpty(from)) { return new HashSet<>(); } return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toSet()); } public static Set convertSet(Collection from, Function func, Predicate filter) { if (CollUtil.isEmpty(from)) { return new HashSet<>(); } return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet()); } public static Map convertMapByFilter(Collection from, Predicate filter, Function keyFunc) { if (CollUtil.isEmpty(from)) { return new HashMap<>(); } return from.stream().filter(filter).collect(Collectors.toMap(keyFunc, v -> v)); } public static Set convertSetByFlatMap(Collection from, Function> func) { if (CollUtil.isEmpty(from)) { return new HashSet<>(); } return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet()); } public static Set convertSetByFlatMap(Collection from, Function mapper, Function> func) { if (CollUtil.isEmpty(from)) { return new HashSet<>(); } return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet()); } public static Map convertMap(Collection from, Function keyFunc) { if (CollUtil.isEmpty(from)) { return new HashMap<>(); } return convertMap(from, keyFunc, Function.identity()); } public static Map convertMap(Collection from, Function keyFunc, Supplier> supplier) { if (CollUtil.isEmpty(from)) { return supplier.get(); } return convertMap(from, keyFunc, Function.identity(), supplier); } public static Map convertMap(Collection from, Function keyFunc, Function valueFunc) { if (CollUtil.isEmpty(from)) { return new HashMap<>(); } return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1); } public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction) { if (CollUtil.isEmpty(from)) { return new HashMap<>(); } return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new); } public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, Supplier> supplier) { if (CollUtil.isEmpty(from)) { return supplier.get(); } return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier); } public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction, Supplier> supplier) { if (CollUtil.isEmpty(from)) { return new HashMap<>(); } return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier)); } public static Map> convertMultiMap(Collection from, Function keyFunc) { if (CollUtil.isEmpty(from)) { return new HashMap<>(); } return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList()))); } public static Map> convertMultiMap(Collection from, Function keyFunc, Function valueFunc) { if (CollUtil.isEmpty(from)) { return new HashMap<>(); } return from.stream() .collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList()))); } // 暂时没想好名字,先以 2 结尾噶 public static Map> convertMultiMap2(Collection from, Function keyFunc, Function valueFunc) { if (CollUtil.isEmpty(from)) { return new HashMap<>(); } return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet()))); } public static Map convertImmutableMap(Collection from, Function keyFunc) { if (CollUtil.isEmpty(from)) { return Collections.emptyMap(); } ImmutableMap.Builder builder = ImmutableMap.builder(); from.forEach(item -> builder.put(keyFunc.apply(item), item)); return builder.build(); } /** * 对比老、新两个列表,找出新增、修改、删除的数据 * * @param oldList 老列表 * @param newList 新列表 * @param sameFunc 对比函数,返回 true 表示相同,返回 false 表示不同 * 注意,same 是通过每个元素的“标识”,判断它们是不是同一个数据 * @return [新增列表、修改列表、删除列表] */ public static List> diffList(Collection oldList, Collection newList, BiFunction sameFunc) { List createList = new LinkedList<>(newList); // 默认都认为是新增的,后续会进行移除 List updateList = new ArrayList<>(); List deleteList = new ArrayList<>(); // 通过以 oldList 为主遍历,找出 updateList 和 deleteList for (T oldObj : oldList) { // 1. 寻找是否有匹配的 T foundObj = null; for (Iterator iterator = createList.iterator(); iterator.hasNext(); ) { T newObj = iterator.next(); // 1.1 不匹配,则直接跳过 if (!sameFunc.apply(oldObj, newObj)) { continue; } // 1.2 匹配,则移除,并结束寻找 iterator.remove(); foundObj = newObj; break; } // 2. 匹配添加到 updateList;不匹配则添加到 deleteList 中 if (foundObj != null) { updateList.add(foundObj); } else { deleteList.add(oldObj); } } return asList(createList, updateList, deleteList); } public static boolean containsAny(Collection source, Collection candidates) { return org.springframework.util.CollectionUtils.containsAny(source, candidates); } public static T getFirst(List from) { return !CollectionUtil.isEmpty(from) ? from.get(0) : null; } public static T findFirst(Collection from, Predicate predicate) { return findFirst(from, predicate, Function.identity()); } public static U findFirst(Collection from, Predicate predicate, Function func) { if (CollUtil.isEmpty(from)) { return null; } return from.stream().filter(predicate).findFirst().map(func).orElse(null); } public static > V getMaxValue(Collection from, Function valueFunc) { if (CollUtil.isEmpty(from)) { return null; } assert !from.isEmpty(); // 断言,避免告警 T t = from.stream().max(Comparator.comparing(valueFunc)).get(); return valueFunc.apply(t); } public static > V getMinValue(List from, Function valueFunc) { if (CollUtil.isEmpty(from)) { return null; } assert from.size() > 0; // 断言,避免告警 T t = from.stream().min(Comparator.comparing(valueFunc)).get(); return valueFunc.apply(t); } public static > V getSumValue(List from, Function valueFunc, BinaryOperator accumulator) { return getSumValue(from, valueFunc, accumulator, null); } public static > V getSumValue(Collection from, Function valueFunc, BinaryOperator accumulator, V defaultValue) { if (CollUtil.isEmpty(from)) { return defaultValue; } assert !from.isEmpty(); // 断言,避免告警 return from.stream().map(valueFunc).filter(Objects::nonNull).reduce(accumulator).orElse(defaultValue); } public static void addIfNotNull(Collection coll, T item) { if (item == null) { return; } coll.add(item); } public static Collection singleton(T obj) { return obj == null ? Collections.emptyList() : Collections.singleton(obj); } public static List newArrayList(List> list) { return list.stream().flatMap(Collection::stream).collect(Collectors.toList()); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/collection/MapUtils.java ================================================ package co.yixiang.yshop.framework.common.util.collection; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ObjUtil; import co.yixiang.yshop.framework.common.core.KeyValue; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.function.Consumer; /** * Map 工具类 * * @author yshop */ public class MapUtils { /** * 从哈希表表中,获得 keys 对应的所有 value 数组 * * @param multimap 哈希表 * @param keys keys * @return value 数组 */ public static List getList(Multimap multimap, Collection keys) { List result = new ArrayList<>(); keys.forEach(k -> { Collection values = multimap.get(k); if (CollectionUtil.isEmpty(values)) { return; } result.addAll(values); }); return result; } /** * 从哈希表查找到 key 对应的 value,然后进一步处理 * key 为 null 时, 不处理 * 注意,如果查找到的 value 为 null 时,不进行处理 * * @param map 哈希表 * @param key key * @param consumer 进一步处理的逻辑 */ public static void findAndThen(Map map, K key, Consumer consumer) { if (ObjUtil.isNull(key) || CollUtil.isEmpty(map)) { return; } V value = map.get(key); if (value == null) { return; } consumer.accept(value); } public static Map convertMap(List> keyValues) { Map map = Maps.newLinkedHashMapWithExpectedSize(keyValues.size()); keyValues.forEach(keyValue -> map.put(keyValue.getKey(), keyValue.getValue())); return map; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/collection/SetUtils.java ================================================ package co.yixiang.yshop.framework.common.util.collection; import cn.hutool.core.collection.CollUtil; import java.util.Set; /** * Set 工具类 * * @author yshop */ public class SetUtils { @SafeVarargs public static Set asSet(T... objs) { return CollUtil.newHashSet(objs); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/date/DateUtils.java ================================================ package co.yixiang.yshop.framework.common.util.date; import cn.hutool.core.date.LocalDateTimeUtil; import java.time.*; import java.util.Calendar; import java.util.Date; /** * 时间工具类 * * @author yshop */ public class DateUtils { /** * 时区 - 默认 */ public static final String TIME_ZONE_DEFAULT = "GMT+8"; /** * 秒转换成毫秒 */ public static final long SECOND_MILLIS = 1000; public static final String FORMAT_YEAR_MONTH_DAY = "yyyy-MM-dd"; public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss"; /** * 将 LocalDateTime 转换成 Date * * @param date LocalDateTime * @return LocalDateTime */ public static Date of(LocalDateTime date) { if (date == null) { return null; } // 将此日期时间与时区相结合以创建 ZonedDateTime ZonedDateTime zonedDateTime = date.atZone(ZoneId.systemDefault()); // 本地时间线 LocalDateTime 到即时时间线 Instant 时间戳 Instant instant = zonedDateTime.toInstant(); // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间 return Date.from(instant); } /** * 将 Date 转换成 LocalDateTime * * @param date Date * @return LocalDateTime */ public static LocalDateTime of(Date date) { if (date == null) { return null; } // 转为时间戳 Instant instant = date.toInstant(); // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间 return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); } public static Date addTime(Duration duration) { return new Date(System.currentTimeMillis() + duration.toMillis()); } public static boolean isExpired(LocalDateTime time) { LocalDateTime now = LocalDateTime.now(); return now.isAfter(time); } /** * 创建指定时间 * * @param year 年 * @param mouth 月 * @param day 日 * @return 指定时间 */ public static Date buildTime(int year, int mouth, int day) { return buildTime(year, mouth, day, 0, 0, 0); } /** * 创建指定时间 * * @param year 年 * @param mouth 月 * @param day 日 * @param hour 小时 * @param minute 分钟 * @param second 秒 * @return 指定时间 */ public static Date buildTime(int year, int mouth, int day, int hour, int minute, int second) { Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.YEAR, year); calendar.set(Calendar.MONTH, mouth - 1); calendar.set(Calendar.DAY_OF_MONTH, day); calendar.set(Calendar.HOUR_OF_DAY, hour); calendar.set(Calendar.MINUTE, minute); calendar.set(Calendar.SECOND, second); calendar.set(Calendar.MILLISECOND, 0); // 一般情况下,都是 0 毫秒 return calendar.getTime(); } public static Date max(Date a, Date b) { if (a == null) { return b; } if (b == null) { return a; } return a.compareTo(b) > 0 ? a : b; } public static LocalDateTime max(LocalDateTime a, LocalDateTime b) { if (a == null) { return b; } if (b == null) { return a; } return a.isAfter(b) ? a : b; } /** * 是否今天 * * @param date 日期 * @return 是否 */ public static boolean isToday(LocalDateTime date) { return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now()); } /** * 是否昨天 * * @param date 日期 * @return 是否 */ public static boolean isYesterday(LocalDateTime date) { return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now().minusDays(1)); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/date/LocalDateTimeUtils.java ================================================ package co.yixiang.yshop.framework.common.util.date; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.enums.DateIntervalEnum; import java.time.*; import java.time.format.DateTimeParseException; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAdjusters; import java.util.ArrayList; import java.util.List; /** * 时间工具类,用于 {@link java.time.LocalDateTime} * * @author yshop */ public class LocalDateTimeUtils { /** * 空的 LocalDateTime 对象,主要用于 DB 唯一索引的默认值 */ public static LocalDateTime EMPTY = buildTime(1970, 1, 1); /** * 解析时间 * * 相比 {@link LocalDateTimeUtil#parse(CharSequence)} 方法来说,会尽量去解析,直到成功 * * @param time 时间 * @return 时间字符串 */ public static LocalDateTime parse(String time) { try { return LocalDateTimeUtil.parse(time, DatePattern.NORM_DATE_PATTERN); } catch (DateTimeParseException e) { return LocalDateTimeUtil.parse(time); } } public static LocalDateTime addTime(Duration duration) { return LocalDateTime.now().plus(duration); } public static LocalDateTime minusTime(Duration duration) { return LocalDateTime.now().minus(duration); } public static boolean beforeNow(LocalDateTime date) { return date.isBefore(LocalDateTime.now()); } public static boolean afterNow(LocalDateTime date) { return date.isAfter(LocalDateTime.now()); } /** * 创建指定时间 * * @param year 年 * @param mouth 月 * @param day 日 * @return 指定时间 */ public static LocalDateTime buildTime(int year, int mouth, int day) { return LocalDateTime.of(year, mouth, day, 0, 0, 0); } public static LocalDateTime[] buildBetweenTime(int year1, int mouth1, int day1, int year2, int mouth2, int day2) { return new LocalDateTime[]{buildTime(year1, mouth1, day1), buildTime(year2, mouth2, day2)}; } /** * 判指定断时间,是否在该时间范围内 * * @param startTime 开始时间 * @param endTime 结束时间 * @param time 指定时间 * @return 是否 */ public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime, String time) { if (startTime == null || endTime == null || time == null) { return false; } return LocalDateTimeUtil.isIn(parse(time), startTime, endTime); } /** * 判断当前时间是否在该时间范围内 * * @param startTime 开始时间 * @param endTime 结束时间 * @return 是否 */ public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime) { if (startTime == null || endTime == null) { return false; } return LocalDateTimeUtil.isIn(LocalDateTime.now(), startTime, endTime); } /** * 判断当前时间是否在该时间范围内 * * @param startTime 开始时间 * @param endTime 结束时间 * @return 是否 */ public static boolean isBetween(String startTime, String endTime) { if (startTime == null || endTime == null) { return false; } LocalDate nowDate = LocalDate.now(); return LocalDateTimeUtil.isIn(LocalDateTime.now(), LocalDateTime.of(nowDate, LocalTime.parse(startTime)), LocalDateTime.of(nowDate, LocalTime.parse(endTime))); } /** * 判断时间段是否重叠 * * @param startTime1 开始 time1 * @param endTime1 结束 time1 * @param startTime2 开始 time2 * @param endTime2 结束 time2 * @return 重叠:true 不重叠:false */ public static boolean isOverlap(LocalTime startTime1, LocalTime endTime1, LocalTime startTime2, LocalTime endTime2) { LocalDate nowDate = LocalDate.now(); return LocalDateTimeUtil.isOverlap(LocalDateTime.of(nowDate, startTime1), LocalDateTime.of(nowDate, endTime1), LocalDateTime.of(nowDate, startTime2), LocalDateTime.of(nowDate, endTime2)); } /** * 获取指定日期所在的月份的开始时间 * 例如:2023-09-30 00:00:00,000 * * @param date 日期 * @return 月份的开始时间 */ public static LocalDateTime beginOfMonth(LocalDateTime date) { return date.with(TemporalAdjusters.firstDayOfMonth()).with(LocalTime.MIN); } /** * 获取指定日期所在的月份的最后时间 * 例如:2023-09-30 23:59:59,999 * * @param date 日期 * @return 月份的结束时间 */ public static LocalDateTime endOfMonth(LocalDateTime date) { return date.with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX); } /** * 获得指定日期所在季度 * * @param date 日期 * @return 所在季度 */ public static int getQuarterOfYear(LocalDateTime date) { return (date.getMonthValue() - 1) / 3 + 1; } /** * 获取指定日期到现在过了几天,如果指定日期在当前日期之后,获取结果为负 * * @param dateTime 日期 * @return 相差天数 */ public static Long between(LocalDateTime dateTime) { return LocalDateTimeUtil.between(dateTime, LocalDateTime.now(), ChronoUnit.DAYS); } /** * 获取今天的开始时间 * * @return 今天 */ public static LocalDateTime getToday() { return LocalDateTimeUtil.beginOfDay(LocalDateTime.now()); } /** * 获取昨天的开始时间 * * @return 昨天 */ public static LocalDateTime getYesterday() { return LocalDateTimeUtil.beginOfDay(LocalDateTime.now().minusDays(1)); } /** * 获取本月的开始时间 * * @return 本月 */ public static LocalDateTime getMonth() { return beginOfMonth(LocalDateTime.now()); } /** * 获取本年的开始时间 * * @return 本年 */ public static LocalDateTime getYear() { return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN); } public static List getDateRangeList(LocalDateTime startTime, LocalDateTime endTime, Integer interval) { // 1.1 找到枚举 DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval); Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval); // 1.2 将时间对齐 startTime = LocalDateTimeUtil.beginOfDay(startTime); endTime = LocalDateTimeUtil.endOfDay(endTime); // 2. 循环,生成时间范围 List timeRanges = new ArrayList<>(); switch (intervalEnum) { case DAY: while (startTime.isBefore(endTime)) { timeRanges.add(new LocalDateTime[]{startTime, startTime.plusDays(1).minusNanos(1)}); startTime = startTime.plusDays(1); } break; case WEEK: while (startTime.isBefore(endTime)) { LocalDateTime endOfWeek = startTime.with(DayOfWeek.SUNDAY).plusDays(1).minusNanos(1); timeRanges.add(new LocalDateTime[]{startTime, endOfWeek}); startTime = endOfWeek.plusNanos(1); } break; case MONTH: while (startTime.isBefore(endTime)) { LocalDateTime endOfMonth = startTime.with(TemporalAdjusters.lastDayOfMonth()).plusDays(1).minusNanos(1); timeRanges.add(new LocalDateTime[]{startTime, endOfMonth}); startTime = endOfMonth.plusNanos(1); } break; case QUARTER: while (startTime.isBefore(endTime)) { int quarterOfYear = getQuarterOfYear(startTime); LocalDateTime quarterEnd = quarterOfYear == 4 ? startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1) : startTime.withMonth(quarterOfYear * 3 + 1).withDayOfMonth(1).minusNanos(1); timeRanges.add(new LocalDateTime[]{startTime, quarterEnd}); startTime = quarterEnd.plusNanos(1); } break; case YEAR: while (startTime.isBefore(endTime)) { LocalDateTime endOfYear = startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1); timeRanges.add(new LocalDateTime[]{startTime, endOfYear}); startTime = endOfYear.plusNanos(1); } break; default: throw new IllegalArgumentException("Invalid interval: " + interval); } // 3. 兜底,最后一个时间,需要保持在 endTime 之前 LocalDateTime[] lastTimeRange = CollUtil.getLast(timeRanges); if (lastTimeRange != null) { lastTimeRange[1] = endTime; } return timeRanges; } /** * 格式化时间范围 * * @param startTime 开始时间 * @param endTime 结束时间 * @param interval 时间间隔 * @return 时间范围 */ public static String formatDateRange(LocalDateTime startTime, LocalDateTime endTime, Integer interval) { // 1. 找到枚举 DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval); Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval); // 2. 循环,生成时间范围 switch (intervalEnum) { case DAY: return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN); case WEEK: return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN) + StrUtil.format("(第 {} 周)", LocalDateTimeUtil.weekOfYear(startTime)); case MONTH: return LocalDateTimeUtil.format(startTime, DatePattern.NORM_MONTH_PATTERN); case QUARTER: return StrUtil.format("{}-Q{}", startTime.getYear(), getQuarterOfYear(startTime)); case YEAR: return LocalDateTimeUtil.format(startTime, DatePattern.NORM_YEAR_PATTERN); default: throw new IllegalArgumentException("Invalid interval: " + interval); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/http/HttpUtils.java ================================================ package co.yixiang.yshop.framework.common.util.http; import cn.hutool.core.codec.Base64; import cn.hutool.core.map.TableMap; import cn.hutool.core.net.url.UrlBuilder; import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; import jakarta.servlet.http.HttpServletRequest; import java.net.URI; import java.nio.charset.Charset; import java.util.Map; /** * HTTP 工具类 * * @author yshop */ public class HttpUtils { @SuppressWarnings("unchecked") public static String replaceUrlQuery(String url, String key, String value) { UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset()); // 先移除 TableMap query = (TableMap) ReflectUtil.getFieldValue(builder.getQuery(), "query"); query.remove(key); // 后添加 builder.addQuery(key, value); return builder.build(); } private String append(String base, Map query, boolean fragment) { return append(base, query, null, fragment); } /** * 拼接 URL * * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 append 方法 * * @param base 基础 URL * @param query 查询参数 * @param keys query 的 key,对应的原本的 key 的映射。例如说 query 里有个 key 是 xx,实际它的 key 是 extra_xx,则通过 keys 里添加这个映射 * @param fragment URL 的 fragment,即拼接到 # 中 * @return 拼接后的 URL */ public static String append(String base, Map query, Map keys, boolean fragment) { UriComponentsBuilder template = UriComponentsBuilder.newInstance(); UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(base); URI redirectUri; try { // assume it's encoded to start with (if it came in over the wire) redirectUri = builder.build(true).toUri(); } catch (Exception e) { // ... but allow client registrations to contain hard-coded non-encoded values redirectUri = builder.build().toUri(); builder = UriComponentsBuilder.fromUri(redirectUri); } template.scheme(redirectUri.getScheme()).port(redirectUri.getPort()).host(redirectUri.getHost()) .userInfo(redirectUri.getUserInfo()).path(redirectUri.getPath()); if (fragment) { StringBuilder values = new StringBuilder(); if (redirectUri.getFragment() != null) { String append = redirectUri.getFragment(); values.append(append); } for (String key : query.keySet()) { if (values.length() > 0) { values.append("&"); } String name = key; if (keys != null && keys.containsKey(key)) { name = keys.get(key); } values.append(name).append("={").append(key).append("}"); } if (values.length() > 0) { template.fragment(values.toString()); } UriComponents encoded = template.build().expand(query).encode(); builder.fragment(encoded.getFragment()); } else { for (String key : query.keySet()) { String name = key; if (keys != null && keys.containsKey(key)) { name = keys.get(key); } template.queryParam(name, "{" + key + "}"); } template.fragment(redirectUri.getFragment()); UriComponents encoded = template.build().expand(query).encode(); builder.query(encoded.getQuery()); } return builder.build().toUriString(); } public static String[] obtainBasicAuthorization(HttpServletRequest request) { String clientId; String clientSecret; // 先从 Header 中获取 String authorization = request.getHeader("Authorization"); authorization = StrUtil.subAfter(authorization, "Basic ", true); if (StringUtils.hasText(authorization)) { authorization = Base64.decodeStr(authorization); clientId = StrUtil.subBefore(authorization, ":", false); clientSecret = StrUtil.subAfter(authorization, ":", false); // 再从 Param 中获取 } else { clientId = request.getParameter("client_id"); clientSecret = request.getParameter("client_secret"); } // 如果两者非空,则返回 if (StrUtil.isNotEmpty(clientId) && StrUtil.isNotEmpty(clientSecret)) { return new String[]{clientId, clientSecret}; } return null; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/io/FileUtils.java ================================================ package co.yixiang.yshop.framework.common.util.io; import cn.hutool.core.io.FileTypeUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.file.FileNameUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.digest.DigestUtil; import lombok.SneakyThrows; import java.io.ByteArrayInputStream; import java.io.File; /** * 文件工具类 * * @author yshop */ public class FileUtils { /** * 创建临时文件 * 该文件会在 JVM 退出时,进行删除 * * @param data 文件内容 * @return 文件 */ @SneakyThrows public static File createTempFile(String data) { File file = createTempFile(); // 写入内容 FileUtil.writeUtf8String(data, file); return file; } /** * 创建临时文件 * 该文件会在 JVM 退出时,进行删除 * * @param data 文件内容 * @return 文件 */ @SneakyThrows public static File createTempFile(byte[] data) { File file = createTempFile(); // 写入内容 FileUtil.writeBytes(data, file); return file; } /** * 创建临时文件,无内容 * 该文件会在 JVM 退出时,进行删除 * * @return 文件 */ @SneakyThrows public static File createTempFile() { // 创建文件,通过 UUID 保证唯一 File file = File.createTempFile(IdUtil.simpleUUID(), null); // 标记 JVM 退出时,自动删除 file.deleteOnExit(); return file; } /** * 生成文件路径 * * @param content 文件内容 * @param originalName 原始文件名 * @return path,唯一不可重复 */ public static String generatePath(byte[] content, String originalName) { String sha256Hex = DigestUtil.sha256Hex(content); // 情况一:如果存在 name,则优先使用 name 的后缀 if (StrUtil.isNotBlank(originalName)) { String extName = FileNameUtil.extName(originalName); return StrUtil.isBlank(extName) ? sha256Hex : sha256Hex + "." + extName; } // 情况二:基于 content 计算 return sha256Hex + '.' + FileTypeUtil.getType(new ByteArrayInputStream(content)); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/io/IoUtils.java ================================================ package co.yixiang.yshop.framework.common.util.io; import cn.hutool.core.io.IORuntimeException; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.StrUtil; import java.io.InputStream; /** * IO 工具类,用于 {@link cn.hutool.core.io.IoUtil} 缺失的方法 * * @author yshop */ public class IoUtils { /** * 从流中读取 UTF8 编码的内容 * * @param in 输入流 * @param isClose 是否关闭 * @return 内容 * @throws IORuntimeException IO 异常 */ public static String readUtf8(InputStream in, boolean isClose) throws IORuntimeException { return StrUtil.utf8Str(IoUtil.read(in, isClose)); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/json/JsonUtils.java ================================================ package co.yixiang.yshop.framework.common.util.json; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; /** * JSON 工具类 * * @author yshop */ @Slf4j public class JsonUtils { private static ObjectMapper objectMapper = new ObjectMapper(); static { objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 忽略 null 值 objectMapper.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化 } /** * 初始化 objectMapper 属性 *

* 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean * * @param objectMapper ObjectMapper 对象 */ public static void init(ObjectMapper objectMapper) { JsonUtils.objectMapper = objectMapper; } @SneakyThrows public static String toJsonString(Object object) { return objectMapper.writeValueAsString(object); } @SneakyThrows public static byte[] toJsonByte(Object object) { return objectMapper.writeValueAsBytes(object); } @SneakyThrows public static String toJsonPrettyString(Object object) { return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object); } public static T parseObject(String text, Class clazz) { if (StrUtil.isEmpty(text)) { return null; } try { return objectMapper.readValue(text, clazz); } catch (IOException e) { log.error("json parse err,json:{}", text, e); throw new RuntimeException(e); } } public static T parseObject(String text, String path, Class clazz) { if (StrUtil.isEmpty(text)) { return null; } try { JsonNode treeNode = objectMapper.readTree(text); JsonNode pathNode = treeNode.path(path); return objectMapper.readValue(pathNode.toString(), clazz); } catch (IOException e) { log.error("json parse err,json:{}", text, e); throw new RuntimeException(e); } } public static T parseObject(String text, Type type) { if (StrUtil.isEmpty(text)) { return null; } try { return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type)); } catch (IOException e) { log.error("json parse err,json:{}", text, e); throw new RuntimeException(e); } } /** * 将字符串解析成指定类型的对象 * 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下, * 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。 * * @param text 字符串 * @param clazz 类型 * @return 对象 */ public static T parseObject2(String text, Class clazz) { if (StrUtil.isEmpty(text)) { return null; } return JSONUtil.toBean(text, clazz); } public static T parseObject(byte[] bytes, Class clazz) { if (ArrayUtil.isEmpty(bytes)) { return null; } try { return objectMapper.readValue(bytes, clazz); } catch (IOException e) { log.error("json parse err,json:{}", bytes, e); throw new RuntimeException(e); } } public static T parseObject(String text, TypeReference typeReference) { try { return objectMapper.readValue(text, typeReference); } catch (IOException e) { log.error("json parse err,json:{}", text, e); throw new RuntimeException(e); } } /** * 解析 JSON 字符串成指定类型的对象,如果解析失败,则返回 null * * @param text 字符串 * @param typeReference 类型引用 * @return 指定类型的对象 */ public static T parseObjectQuietly(String text, TypeReference typeReference) { try { return objectMapper.readValue(text, typeReference); } catch (IOException e) { return null; } } public static List parseArray(String text, Class clazz) { if (StrUtil.isEmpty(text)) { return new ArrayList<>(); } try { return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); } catch (IOException e) { log.error("json parse err,json:{}", text, e); throw new RuntimeException(e); } } public static List parseArray(String text, String path, Class clazz) { if (StrUtil.isEmpty(text)) { return null; } try { JsonNode treeNode = objectMapper.readTree(text); JsonNode pathNode = treeNode.path(path); return objectMapper.readValue(pathNode.toString(), objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); } catch (IOException e) { log.error("json parse err,json:{}", text, e); throw new RuntimeException(e); } } public static JsonNode parseTree(String text) { try { return objectMapper.readTree(text); } catch (IOException e) { log.error("json parse err,json:{}", text, e); throw new RuntimeException(e); } } public static JsonNode parseTree(byte[] text) { try { return objectMapper.readTree(text); } catch (IOException e) { log.error("json parse err,json:{}", text, e); throw new RuntimeException(e); } } public static boolean isJson(String text) { return JSONUtil.isTypeJSON(text); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/monitor/TracerUtils.java ================================================ package co.yixiang.yshop.framework.common.util.monitor; import org.apache.skywalking.apm.toolkit.trace.TraceContext; /** * 链路追踪工具类 * * 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 util 包下 * * @author yshop */ public class TracerUtils { /** * 私有化构造方法 */ private TracerUtils() { } /** * 获得链路追踪编号,直接返回 SkyWalking 的 TraceId。 * 如果不存在的话为空字符串!!! * * @return 链路追踪编号 */ public static String getTraceId() { return TraceContext.traceId(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/number/MoneyUtils.java ================================================ package co.yixiang.yshop.framework.common.util.number; import cn.hutool.core.math.Money; import cn.hutool.core.util.NumberUtil; import java.math.BigDecimal; import java.math.RoundingMode; /** * 金额工具类 * * @author yshop */ public class MoneyUtils { /** * 金额的小数位数 */ private static final int PRICE_SCALE = 2; /** * 百分比对应的 BigDecimal 对象 */ public static final BigDecimal PERCENT_100 = BigDecimal.valueOf(100); /** * 计算百分比金额,四舍五入 * * @param price 金额 * @param rate 百分比,例如说 56.77% 则传入 56.77 * @return 百分比金额 */ public static Integer calculateRatePrice(Integer price, Double rate) { return calculateRatePrice(price, rate, 0, RoundingMode.HALF_UP).intValue(); } /** * 计算百分比金额,向下传入 * * @param price 金额 * @param rate 百分比,例如说 56.77% 则传入 56.77 * @return 百分比金额 */ public static Integer calculateRatePriceFloor(Integer price, Double rate) { return calculateRatePrice(price, rate, 0, RoundingMode.FLOOR).intValue(); } /** * 计算百分比金额 * * @param price 金额(单位分) * @param count 数量 * @param percent 折扣(单位分),列如 60.2%,则传入 6020 * @return 商品总价 */ public static Integer calculator(Integer price, Integer count, Integer percent) { price = price * count; if (percent == null) { return price; } return MoneyUtils.calculateRatePriceFloor(price, (double) (percent / 100)); } /** * 计算百分比金额 * * @param price 金额 * @param rate 百分比,例如说 56.77% 则传入 56.77 * @param scale 保留小数位数 * @param roundingMode 舍入模式 */ public static BigDecimal calculateRatePrice(Number price, Number rate, int scale, RoundingMode roundingMode) { return NumberUtil.toBigDecimal(price).multiply(NumberUtil.toBigDecimal(rate)) // 乘以 .divide(BigDecimal.valueOf(100), scale, roundingMode); // 除以 100 } /** * 分转元 * * @param fen 分 * @return 元 */ public static BigDecimal fenToYuan(int fen) { return new Money(0, fen).getAmount(); } /** * 分转元(字符串) * * 例如说 fen 为 1 时,则结果为 0.01 * * @param fen 分 * @return 元 */ public static String fenToYuanStr(int fen) { return new Money(0, fen).toString(); } /** * 金额相乘,默认进行四舍五入 * * 位数:{@link #PRICE_SCALE} * * @param price 金额 * @param count 数量 * @return 金额相乘结果 */ public static BigDecimal priceMultiply(BigDecimal price, BigDecimal count) { if (price == null || count == null) { return null; } return price.multiply(count).setScale(PRICE_SCALE, RoundingMode.HALF_UP); } /** * 金额相乘(百分比),默认进行四舍五入 * * 位数:{@link #PRICE_SCALE} * * @param price 金额 * @param percent 百分比 * @return 金额相乘结果 */ public static BigDecimal priceMultiplyPercent(BigDecimal price, BigDecimal percent) { if (price == null || percent == null) { return null; } return price.multiply(percent).divide(PERCENT_100, PRICE_SCALE, RoundingMode.HALF_UP); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/number/NumberUtils.java ================================================ package co.yixiang.yshop.framework.common.util.number; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.StrUtil; import java.math.BigDecimal; /** * 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能 * * @author yshop */ public class NumberUtils { public static Long parseLong(String str) { return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null; } public static Integer parseInt(String str) { return StrUtil.isNotEmpty(str) ? Integer.valueOf(str) : null; } /** * 通过经纬度获取地球上两点之间的距离 * * 参考 <DistanceUtil> 实现,目前它已经被 hutool 删除 * * @param lat1 经度1 * @param lng1 纬度1 * @param lat2 经度2 * @param lng2 纬度2 * @return 距离,单位:千米 */ public static double getDistance(double lat1, double lng1, double lat2, double lng2) { double radLat1 = lat1 * Math.PI / 180.0; double radLat2 = lat2 * Math.PI / 180.0; double a = radLat1 - radLat2; double b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0; double distance = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2))); distance = distance * 6378.137; distance = Math.round(distance * 10000d) / 10000d; return distance; } /** * 提供精确的乘法运算 * * 和 hutool {@link NumberUtil#mul(BigDecimal...)} 的差别是,如果存在 null,则返回 null * * @param values 多个被乘值 * @return 积 */ public static BigDecimal mul(BigDecimal... values) { for (BigDecimal value : values) { if (value == null) { return null; } } return NumberUtil.mul(values); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/object/BeanUtils.java ================================================ package co.yixiang.yshop.framework.common.util.object; import cn.hutool.core.bean.BeanUtil; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import java.util.List; import java.util.function.Consumer; /** * Bean 工具类 * * 1. 默认使用 {@link cn.hutool.core.bean.BeanUtil} 作为实现类,虽然不同 bean 工具的性能有差别,但是对绝大多数同学的项目,不用在意这点性能 * 2. 针对复杂的对象转换,可以搜参考 AuthConvert 实现,通过 mapstruct + default 配合实现 * * @author yshop */ public class BeanUtils { public static T toBean(Object source, Class targetClass) { return BeanUtil.toBean(source, targetClass); } public static T toBean(Object source, Class targetClass, Consumer peek) { T target = toBean(source, targetClass); if (target != null) { peek.accept(target); } return target; } public static List toBean(List source, Class targetType) { if (source == null) { return null; } return CollectionUtils.convertList(source, s -> toBean(s, targetType)); } public static List toBean(List source, Class targetType, Consumer peek) { List list = toBean(source, targetType); if (list != null) { list.forEach(peek); } return list; } public static PageResult toBean(PageResult source, Class targetType) { return toBean(source, targetType, null); } public static PageResult toBean(PageResult source, Class targetType, Consumer peek) { if (source == null) { return null; } List list = toBean(source.getList(), targetType); if (peek != null) { list.forEach(peek); } return new PageResult<>(list, source.getTotal()); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/object/ObjectUtils.java ================================================ package co.yixiang.yshop.framework.common.util.object; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ReflectUtil; import java.lang.reflect.Field; import java.util.Arrays; import java.util.function.Consumer; /** * Object 工具类 * * @author yshop */ public class ObjectUtils { /** * 复制对象,并忽略 Id 编号 * * @param object 被复制对象 * @param consumer 消费者,可以二次编辑被复制对象 * @return 复制后的对象 */ public static T cloneIgnoreId(T object, Consumer consumer) { T result = ObjectUtil.clone(object); // 忽略 id 编号 Field field = ReflectUtil.getField(object.getClass(), "id"); if (field != null) { ReflectUtil.setFieldValue(result, field, null); } // 二次编辑 if (result != null) { consumer.accept(result); } return result; } public static > T max(T obj1, T obj2) { if (obj1 == null) { return obj2; } if (obj2 == null) { return obj1; } return obj1.compareTo(obj2) > 0 ? obj1 : obj2; } @SafeVarargs public static T defaultIfNull(T... array) { for (T item : array) { if (item != null) { return item; } } return null; } @SafeVarargs public static boolean equalsAny(T obj, T... array) { return Arrays.asList(array).contains(obj); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/object/PageUtils.java ================================================ package co.yixiang.yshop.framework.common.util.object; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.func.Func1; import cn.hutool.core.lang.func.LambdaUtil; import cn.hutool.core.util.ArrayUtil; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.SortablePageParam; import co.yixiang.yshop.framework.common.pojo.SortingField; import org.springframework.util.Assert; import static java.util.Collections.singletonList; /** * {@link co.yixiang.yshop.framework.common.pojo.PageParam} 工具类 * * @author yshop */ public class PageUtils { private static final Object[] ORDER_TYPES = new String[]{SortingField.ORDER_ASC, SortingField.ORDER_DESC}; public static int getStart(PageParam pageParam) { return (pageParam.getPageNo() - 1) * pageParam.getPageSize(); } /** * 构建排序字段(默认倒序) * * @param func 排序字段的 Lambda 表达式 * @param 排序字段所属的类型 * @return 排序字段 */ public static SortingField buildSortingField(Func1 func) { return buildSortingField(func, SortingField.ORDER_DESC); } /** * 构建排序字段 * * @param func 排序字段的 Lambda 表达式 * @param order 排序类型 {@link SortingField#ORDER_ASC} {@link SortingField#ORDER_DESC} * @param 排序字段所属的类型 * @return 排序字段 */ public static SortingField buildSortingField(Func1 func, String order) { Assert.isTrue(ArrayUtil.contains(ORDER_TYPES, order), String.format("字段的排序类型只能是 %s/%s", ORDER_TYPES)); String fieldName = LambdaUtil.getFieldName(func); return new SortingField(fieldName, order); } /** * 构建默认的排序字段 * 如果排序字段为空,则设置排序字段;否则忽略 * * @param sortablePageParam 排序分页查询参数 * @param func 排序字段的 Lambda 表达式 * @param 排序字段所属的类型 */ public static void buildDefaultSortingField(SortablePageParam sortablePageParam, Func1 func) { if (sortablePageParam != null && CollUtil.isEmpty(sortablePageParam.getSortingFields())) { sortablePageParam.setSortingFields(singletonList(buildSortingField(func))); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/servlet/ServletUtils.java ================================================ package co.yixiang.yshop.framework.common.util.servlet; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.servlet.JakartaServletUtil; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import jakarta.servlet.ServletRequest; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.MediaType; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import java.io.IOException; import java.net.URLEncoder; import java.util.Map; /** * 客户端工具类 * * @author yshop */ public class ServletUtils { /** * 返回 JSON 字符串 * * @param response 响应 * @param object 对象,会序列化成 JSON 字符串 */ @SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码 public static void writeJSON(HttpServletResponse response, Object object) { String content = JsonUtils.toJsonString(object); JakartaServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE); } /** * @param request 请求 * @return ua */ public static String getUserAgent(HttpServletRequest request) { String ua = request.getHeader("User-Agent"); return ua != null ? ua : ""; } /** * 获得请求 * * @return HttpServletRequest */ public static HttpServletRequest getRequest() { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if (!(requestAttributes instanceof ServletRequestAttributes)) { return null; } return ((ServletRequestAttributes) requestAttributes).getRequest(); } public static String getUserAgent() { HttpServletRequest request = getRequest(); if (request == null) { return null; } return getUserAgent(request); } public static String getClientIP() { HttpServletRequest request = getRequest(); if (request == null) { return null; } return JakartaServletUtil.getClientIP(request); } public static boolean isJsonRequest(ServletRequest request) { return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE); } public static String getBody(HttpServletRequest request) { // 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取 if (isJsonRequest(request)) { return JakartaServletUtil.getBody(request); } return null; } public static byte[] getBodyBytes(HttpServletRequest request) { // 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取 if (isJsonRequest(request)) { return JakartaServletUtil.getBodyBytes(request); } return null; } public static String getClientIP(HttpServletRequest request) { return JakartaServletUtil.getClientIP(request); } public static Map getParamMap(HttpServletRequest request) { return JakartaServletUtil.getParamMap(request); } public static String getRequstHost(HttpServletRequest request){ String port = ":" + request.getServerPort(); if(request.getServerPort() == 80 || request.getServerPort() == 443){ port = ""; } return request.getScheme()+"://" + request.getServerName() + port; } /** * 返回附件 * * @param response 响应 * @param filename 文件名 * @param content 附件内容 */ public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException { // 设置 header 和 contentType response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8")); response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); // 输出附件 IoUtil.write(response.getOutputStream(), false, content); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/spring/SpringExpressionUtils.java ================================================ package co.yixiang.yshop.framework.common.util.spring; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ArrayUtil; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.expression.EvaluationContext; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import java.lang.reflect.Method; import java.util.Collections; import java.util.List; import java.util.Map; /** * Spring EL 表达式的工具类 * * @author mashu */ public class SpringExpressionUtils { /** * Spring EL 表达式解析器 */ private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); /** * 参数名发现器 */ private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); private SpringExpressionUtils() { } /** * 从切面中,单个解析 EL 表达式的结果 * * @param joinPoint 切面点 * @param expressionString EL 表达式数组 * @return 执行界面 */ public static Object parseExpression(JoinPoint joinPoint, String expressionString) { Map result = parseExpressions(joinPoint, Collections.singletonList(expressionString)); return result.get(expressionString); } /** * 从切面中,批量解析 EL 表达式的结果 * * @param joinPoint 切面点 * @param expressionStrings EL 表达式数组 * @return 结果,key 为表达式,value 为对应值 */ public static Map parseExpressions(JoinPoint joinPoint, List expressionStrings) { // 如果为空,则不进行解析 if (CollUtil.isEmpty(expressionStrings)) { return MapUtil.newHashMap(); } // 第一步,构建解析的上下文 EvaluationContext // 通过 joinPoint 获取被注解方法 MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); // 使用 spring 的 ParameterNameDiscoverer 获取方法形参名数组 String[] paramNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method); // Spring 的表达式上下文对象 EvaluationContext context = new StandardEvaluationContext(); // 给上下文赋值 if (ArrayUtil.isNotEmpty(paramNames)) { Object[] args = joinPoint.getArgs(); for (int i = 0; i < paramNames.length; i++) { context.setVariable(paramNames[i], args[i]); } } // 第二步,逐个参数解析 Map result = MapUtil.newHashMap(expressionStrings.size(), true); expressionStrings.forEach(key -> { Object value = EXPRESSION_PARSER.parseExpression(key).getValue(context); result.put(key, value); }); return result; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/spring/SpringUtils.java ================================================ package co.yixiang.yshop.framework.common.util.spring; import cn.hutool.extra.spring.SpringUtil; import java.util.Objects; /** * Spring 工具类 * * @author yshop */ public class SpringUtils extends SpringUtil { /** * 是否为生产环境 * * @return 是否生产环境 */ public static boolean isProd() { String activeProfile = getActiveProfile(); return Objects.equals("prod", activeProfile); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/string/StrUtils.java ================================================ package co.yixiang.yshop.framework.common.util.string; import cn.hutool.core.text.StrPool; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; import org.springframework.util.CollectionUtils; import java.text.Collator; import java.util.*; import java.util.stream.Collectors; /** * 字符串工具类 * * @author yshop */ public class StrUtils { public static String maxLength(CharSequence str, int maxLength) { return StrUtil.maxLength(str, maxLength - 3); // -3 的原因,是该方法会补充 ... 恰好 } /** * 给定字符串是否以任何一个字符串开始 * 给定字符串和数组为空都返回 false * * @param str 给定字符串 * @param prefixes 需要检测的开始字符串 * @since 3.0.6 */ public static boolean startWithAny(String str, Collection prefixes) { if (StrUtil.isEmpty(str) || ArrayUtil.isEmpty(prefixes)) { return false; } for (CharSequence suffix : prefixes) { if (StrUtil.startWith(str, suffix, false)) { return true; } } return false; } public static List splitToLong(String value, CharSequence separator) { long[] longs = StrUtil.splitToLong(value, separator); return Arrays.stream(longs).boxed().collect(Collectors.toList()); } public static Set splitToLongSet(String value) { return splitToLongSet(value, StrPool.COMMA); } public static Set splitToLongSet(String value, CharSequence separator) { long[] longs = StrUtil.splitToLong(value, separator); return Arrays.stream(longs).boxed().collect(Collectors.toSet()); } public static List splitToInteger(String value, CharSequence separator) { int[] integers = StrUtil.splitToInt(value, separator); return Arrays.stream(integers).boxed().collect(Collectors.toList()); } /** * 移除字符串中,包含指定字符串的行 * * @param content 字符串 * @param sequence 包含的字符串 * @return 移除后的字符串 */ public static String removeLineContains(String content, String sequence) { if (StrUtil.isEmpty(content) || StrUtil.isEmpty(sequence)) { return content; } return Arrays.stream(content.split("\n")) .filter(line -> !line.contains(sequence)) .collect(Collectors.joining("\n")); } /** * 数字排在最前,英文字母其次,汉字则按照拼音进行排序 */ public static List compareTo(List stringList) { if (CollectionUtils.isEmpty(stringList)) { return Collections.emptyList(); } Comparator comparator = (text, texts) -> { Collator collator = Collator.getInstance(java.util.Locale.CHINESE); return collator.getCollationKey(text).compareTo( collator.getCollationKey(texts)); }; Collections.sort(stringList, comparator); return stringList; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/validation/ValidationUtils.java ================================================ package co.yixiang.yshop.framework.common.util.validation; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import org.springframework.util.StringUtils; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; import jakarta.validation.Validation; import jakarta.validation.Validator; import java.util.Set; import java.util.regex.Pattern; /** * 校验工具类 * * @author yshop */ public class ValidationUtils { private static final Pattern PATTERN_MOBILE = Pattern.compile("^(?:(?:\\+|00)86)?1(?:(?:3[\\d])|(?:4[0,1,4-9])|(?:5[0-3,5-9])|(?:6[2,5-7])|(?:7[0-8])|(?:8[\\d])|(?:9[0-3,5-9]))\\d{8}$"); private static final Pattern PATTERN_URL = Pattern.compile("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"); private static final Pattern PATTERN_XML_NCNAME = Pattern.compile("[a-zA-Z_][\\-_.0-9_a-zA-Z$]*"); public static boolean isMobile(String mobile) { return StringUtils.hasText(mobile) && PATTERN_MOBILE.matcher(mobile).matches(); } public static boolean isURL(String url) { return StringUtils.hasText(url) && PATTERN_URL.matcher(url).matches(); } public static boolean isXmlNCName(String str) { return StringUtils.hasText(str) && PATTERN_XML_NCNAME.matcher(str).matches(); } public static void validate(Object object, Class... groups) { Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); Assert.notNull(validator); validate(validator, object, groups); } public static void validate(Validator validator, Object object, Class... groups) { Set> constraintViolations = validator.validate(object, groups); if (CollUtil.isNotEmpty(constraintViolations)) { throw new ConstraintViolationException(constraintViolations); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/util/yshop/LocationUtils.java ================================================ package co.yixiang.yshop.framework.common.util.yshop; import cn.hutool.core.util.NumberUtil; /** * 定位工具 * @author hupeng */ public class LocationUtils { private static double EARTH_RADIUS = 6378.137; private static double rad(double d) { return d * Math.PI / 180.0; } /** * 通过经纬度获取距离(单位:千米) * * @param lat1 * @param lng1 * @param lat2 * @param lng2 * @return */ public static double getDistance(double lat1, double lng1, double lat2, double lng2) { double radLat1 = rad(lat1); double radLat2 = rad(lat2); double a = radLat1 - radLat2; double b = rad(lng1) - rad(lng2); double s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2))); s = s * EARTH_RADIUS; s = Math.round(s * 10000d) / 10000d; //s = s*1000; return NumberUtil.round(s, 2).doubleValue(); //return s; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/validation/InEnum.java ================================================ package co.yixiang.yshop.framework.common.validation; import co.yixiang.yshop.framework.common.core.IntArrayValuable; import jakarta.validation.Constraint; import jakarta.validation.Payload; import java.lang.annotation.*; @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE }) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint( validatedBy = {InEnumValidator.class, InEnumCollectionValidator.class} ) public @interface InEnum { /** * @return 实现 EnumValuable 接口的 */ Class value(); String message() default "必须在指定范围 {value}"; Class[] groups() default {}; Class[] payload() default {}; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/validation/InEnumCollectionValidator.java ================================================ package co.yixiang.yshop.framework.common.validation; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.common.core.IntArrayValuable; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; public class InEnumCollectionValidator implements ConstraintValidator> { private List values; @Override public void initialize(InEnum annotation) { IntArrayValuable[] values = annotation.value().getEnumConstants(); if (values.length == 0) { this.values = Collections.emptyList(); } else { this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toList()); } } @Override public boolean isValid(Collection list, ConstraintValidatorContext context) { // 校验通过 if (CollUtil.containsAll(values, list)) { return true; } // 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值) context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值 context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate() .replaceAll("\\{value}", CollUtil.join(list, ","))).addConstraintViolation(); // 重新添加错误提示语句 return false; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/validation/InEnumValidator.java ================================================ package co.yixiang.yshop.framework.common.validation; import co.yixiang.yshop.framework.common.core.IntArrayValuable; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; public class InEnumValidator implements ConstraintValidator { private List values; @Override public void initialize(InEnum annotation) { IntArrayValuable[] values = annotation.value().getEnumConstants(); if (values.length == 0) { this.values = Collections.emptyList(); } else { this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toList()); } } @Override public boolean isValid(Integer value, ConstraintValidatorContext context) { // 为空时,默认不校验,即认为通过 if (value == null) { return true; } // 校验通过 if (values.contains(value)) { return true; } // 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值) context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值 context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate() .replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句 return false; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/validation/Mobile.java ================================================ package co.yixiang.yshop.framework.common.validation; import jakarta.validation.Constraint; import jakarta.validation.Payload; import java.lang.annotation.*; @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE }) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint( validatedBy = MobileValidator.class ) public @interface Mobile { String message() default "手机号格式不正确"; Class[] groups() default {}; Class[] payload() default {}; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/validation/MobileValidator.java ================================================ package co.yixiang.yshop.framework.common.validation; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.util.validation.ValidationUtils; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; public class MobileValidator implements ConstraintValidator { @Override public void initialize(Mobile annotation) { } @Override public boolean isValid(String value, ConstraintValidatorContext context) { // 如果手机号为空,默认不校验,即校验通过 if (StrUtil.isEmpty(value)) { return true; } // 校验手机 return ValidationUtils.isMobile(value); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/validation/Telephone.java ================================================ package co.yixiang.yshop.framework.common.validation; import jakarta.validation.Constraint; import jakarta.validation.Payload; import java.lang.annotation.*; @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE }) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint( validatedBy = TelephoneValidator.class ) public @interface Telephone { String message() default "电话格式不正确"; Class[] groups() default {}; Class[] payload() default {}; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/validation/TelephoneValidator.java ================================================ package co.yixiang.yshop.framework.common.validation; import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.util.PhoneUtil; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; public class TelephoneValidator implements ConstraintValidator { @Override public void initialize(Telephone annotation) { } @Override public boolean isValid(String value, ConstraintValidatorContext context) { // 如果手机号为空,默认不校验,即校验通过 if (CharSequenceUtil.isEmpty(value)) { return true; } // 校验手机 return PhoneUtil.isTel(value) || PhoneUtil.isPhone(value); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/main/java/co/yixiang/yshop/framework/common/validation/package-info.java ================================================ /** * 使用 Hibernate Validator 实现参数校验 */ package co.yixiang.yshop.framework.common.validation; ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-common/src/test/java/co/yixiang/yshop/framework/common/util/collection/CollectionUtilsTest.java ================================================ package co.yixiang.yshop.framework.common.util.collection; import lombok.AllArgsConstructor; import lombok.Data; import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.function.BiFunction; import static org.junit.jupiter.api.Assertions.assertEquals; /** * {@link CollectionUtils} 的单元测试 */ public class CollectionUtilsTest { @Data @AllArgsConstructor private static class Dog { private Integer id; private String name; private String code; } @Test public void testDiffList() { // 准备参数 Collection oldList = Arrays.asList( new Dog(1, "花花", "hh"), new Dog(2, "旺财", "wc") ); Collection newList = Arrays.asList( new Dog(null, "花花2", "hh"), new Dog(null, "小白", "xb") ); BiFunction sameFunc = (oldObj, newObj) -> { boolean same = oldObj.getCode().equals(newObj.getCode()); // 如果相等的情况下,需要设置下 id,后续好更新 if (same) { newObj.setId(oldObj.getId()); } return same; }; // 调用 List> result = CollectionUtils.diffList(oldList, newList, sameFunc); // 断言 assertEquals(result.size(), 3); // 断言 create assertEquals(result.get(0).size(), 1); assertEquals(result.get(0).get(0), new Dog(null, "小白", "xb")); // 断言 update assertEquals(result.get(1).size(), 1); assertEquals(result.get(1).get(0), new Dog(1, "花花2", "hh")); // 断言 delete assertEquals(result.get(2).size(), 1); assertEquals(result.get(2).get(0), new Dog(2, "旺财", "wc")); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/pom.xml ================================================ yshop-framework co.yixiang.boot ${revision} 4.0.0 yshop-spring-boot-starter-biz-data-permission jar ${project.artifactId} 数据权限 https://gitee.com/guchengwuyue/yshop-drink co.yixiang.boot yshop-common co.yixiang.boot yshop-spring-boot-starter-security true co.yixiang.boot yshop-spring-boot-starter-mybatis co.yixiang.boot yshop-module-system-api ${revision} co.yixiang.boot yshop-spring-boot-starter-test test ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/main/java/co/yixiang/yshop/framework/datapermission/config/YshopDataPermissionAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.datapermission.config; import co.yixiang.yshop.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor; import co.yixiang.yshop.framework.datapermission.core.db.DataPermissionDatabaseInterceptor; import co.yixiang.yshop.framework.datapermission.core.rule.DataPermissionRule; import co.yixiang.yshop.framework.datapermission.core.rule.DataPermissionRuleFactory; import co.yixiang.yshop.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl; import co.yixiang.yshop.framework.mybatis.core.util.MyBatisUtils; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.context.annotation.Bean; import java.util.List; /** * 数据权限的自动配置类 * * @author yshop */ @AutoConfiguration public class YshopDataPermissionAutoConfiguration { @Bean public DataPermissionRuleFactory dataPermissionRuleFactory(List rules) { return new DataPermissionRuleFactoryImpl(rules); } @Bean public DataPermissionDatabaseInterceptor dataPermissionDatabaseInterceptor(MybatisPlusInterceptor interceptor, DataPermissionRuleFactory ruleFactory) { // 创建 DataPermissionDatabaseInterceptor 拦截器 DataPermissionDatabaseInterceptor inner = new DataPermissionDatabaseInterceptor(ruleFactory); // 添加到 interceptor 中 // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定 MyBatisUtils.addInterceptor(interceptor, inner, 0); return inner; } @Bean public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() { return new DataPermissionAnnotationAdvisor(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/main/java/co/yixiang/yshop/framework/datapermission/config/YshopDeptDataPermissionAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.datapermission.config; import co.yixiang.yshop.framework.datapermission.core.rule.dept.DeptDataPermissionRule; import co.yixiang.yshop.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer; import co.yixiang.yshop.framework.security.core.LoginUser; import co.yixiang.yshop.module.system.api.permission.PermissionApi; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.context.annotation.Bean; import java.util.List; /** * 基于部门的数据权限 AutoConfiguration * * @author yshop */ @AutoConfiguration @ConditionalOnClass(LoginUser.class) @ConditionalOnBean(value = {PermissionApi.class, DeptDataPermissionRuleCustomizer.class}) public class YshopDeptDataPermissionAutoConfiguration { @Bean public DeptDataPermissionRule deptDataPermissionRule(PermissionApi permissionApi, List customizers) { // 创建 DeptDataPermissionRule 对象 DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi); // 补全表配置 customizers.forEach(customizer -> customizer.customize(rule)); return rule; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/main/java/co/yixiang/yshop/framework/datapermission/core/annotation/DataPermission.java ================================================ package co.yixiang.yshop.framework.datapermission.core.annotation; import co.yixiang.yshop.framework.datapermission.core.rule.DataPermissionRule; import java.lang.annotation.*; /** * 数据权限注解 * 可声明在类或者方法上,标识使用的数据权限规则 * * @author yshop */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataPermission { /** * 当前类或方法是否开启数据权限 * 即使不添加 @DataPermission 注解,默认是开启状态 * 可通过设置 enable 为 false 禁用 */ boolean enable() default true; /** * 生效的数据权限规则数组,优先级高于 {@link #excludeRules()} */ Class[] includeRules() default {}; /** * 排除的数据权限规则数组,优先级最低 */ Class[] excludeRules() default {}; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/main/java/co/yixiang/yshop/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java ================================================ package co.yixiang.yshop.framework.datapermission.core.aop; import co.yixiang.yshop.framework.datapermission.core.annotation.DataPermission; import lombok.EqualsAndHashCode; import lombok.Getter; import org.aopalliance.aop.Advice; import org.springframework.aop.Pointcut; import org.springframework.aop.support.AbstractPointcutAdvisor; import org.springframework.aop.support.ComposablePointcut; import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; /** * {@link co.yixiang.yshop.framework.datapermission.core.annotation.DataPermission} 注解的 Advisor 实现类 * * @author yshop */ @Getter @EqualsAndHashCode(callSuper = true) public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor { private final Advice advice; private final Pointcut pointcut; public DataPermissionAnnotationAdvisor() { this.advice = new DataPermissionAnnotationInterceptor(); this.pointcut = this.buildPointcut(); } protected Pointcut buildPointcut() { Pointcut classPointcut = new AnnotationMatchingPointcut(DataPermission.class, true); Pointcut methodPointcut = new AnnotationMatchingPointcut(null, DataPermission.class, true); return new ComposablePointcut(classPointcut).union(methodPointcut); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/main/java/co/yixiang/yshop/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java ================================================ package co.yixiang.yshop.framework.datapermission.core.aop; import co.yixiang.yshop.framework.datapermission.core.annotation.DataPermission; import lombok.Getter; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.springframework.core.MethodClassKey; import org.springframework.core.annotation.AnnotationUtils; import java.lang.reflect.Method; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * {@link DataPermission} 注解的拦截器 * 1. 在执行方法前,将 @DataPermission 注解入栈 * 2. 在执行方法后,将 @DataPermission 注解出栈 * * @author yshop */ @DataPermission // 该注解,用于 {@link DATA_PERMISSION_NULL} 的空对象 public class DataPermissionAnnotationInterceptor implements MethodInterceptor { /** * DataPermission 空对象,用于方法无 {@link DataPermission} 注解时,使用 DATA_PERMISSION_NULL 进行占位 */ static final DataPermission DATA_PERMISSION_NULL = DataPermissionAnnotationInterceptor.class.getAnnotation(DataPermission.class); @Getter private final Map dataPermissionCache = new ConcurrentHashMap<>(); @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { // 入栈 DataPermission dataPermission = this.findAnnotation(methodInvocation); if (dataPermission != null) { DataPermissionContextHolder.add(dataPermission); } try { // 执行逻辑 return methodInvocation.proceed(); } finally { // 出栈 if (dataPermission != null) { DataPermissionContextHolder.remove(); } } } private DataPermission findAnnotation(MethodInvocation methodInvocation) { // 1. 从缓存中获取 Method method = methodInvocation.getMethod(); Object targetObject = methodInvocation.getThis(); Class clazz = targetObject != null ? targetObject.getClass() : method.getDeclaringClass(); MethodClassKey methodClassKey = new MethodClassKey(method, clazz); DataPermission dataPermission = dataPermissionCache.get(methodClassKey); if (dataPermission != null) { return dataPermission != DATA_PERMISSION_NULL ? dataPermission : null; } // 2.1 从方法中获取 dataPermission = AnnotationUtils.findAnnotation(method, DataPermission.class); // 2.2 从类上获取 if (dataPermission == null) { dataPermission = AnnotationUtils.findAnnotation(clazz, DataPermission.class); } // 2.3 添加到缓存中 dataPermissionCache.put(methodClassKey, dataPermission != null ? dataPermission : DATA_PERMISSION_NULL); return dataPermission; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/main/java/co/yixiang/yshop/framework/datapermission/core/aop/DataPermissionContextHolder.java ================================================ package co.yixiang.yshop.framework.datapermission.core.aop; import co.yixiang.yshop.framework.datapermission.core.annotation.DataPermission; import com.alibaba.ttl.TransmittableThreadLocal; import java.util.LinkedList; import java.util.List; /** * {@link DataPermission} 注解的 Context 上下文 * * @author yshop */ public class DataPermissionContextHolder { /** * 使用 List 的原因,可能存在方法的嵌套调用 */ private static final ThreadLocal> DATA_PERMISSIONS = TransmittableThreadLocal.withInitial(LinkedList::new); /** * 获得当前的 DataPermission 注解 * * @return DataPermission 注解 */ public static DataPermission get() { return DATA_PERMISSIONS.get().peekLast(); } /** * 入栈 DataPermission 注解 * * @param dataPermission DataPermission 注解 */ public static void add(DataPermission dataPermission) { DATA_PERMISSIONS.get().addLast(dataPermission); } /** * 出栈 DataPermission 注解 * * @return DataPermission 注解 */ public static DataPermission remove() { DataPermission dataPermission = DATA_PERMISSIONS.get().removeLast(); // 无元素时,清空 ThreadLocal if (DATA_PERMISSIONS.get().isEmpty()) { DATA_PERMISSIONS.remove(); } return dataPermission; } /** * 获得所有 DataPermission * * @return DataPermission 队列 */ public static List getAll() { return DATA_PERMISSIONS.get(); } /** * 清空上下文 * * 目前仅仅用于单测 */ public static void clear() { DATA_PERMISSIONS.remove(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/main/java/co/yixiang/yshop/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java ================================================ package co.yixiang.yshop.framework.datapermission.core.db; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.common.util.collection.SetUtils; import co.yixiang.yshop.framework.datapermission.core.rule.DataPermissionRule; import co.yixiang.yshop.framework.datapermission.core.rule.DataPermissionRuleFactory; import co.yixiang.yshop.framework.mybatis.core.util.MyBatisUtils; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import com.baomidou.mybatisplus.core.toolkit.PluginUtils; import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport; import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; import lombok.Getter; import lombok.RequiredArgsConstructor; import net.sf.jsqlparser.expression.*; import net.sf.jsqlparser.expression.operators.conditional.AndExpression; import net.sf.jsqlparser.expression.operators.conditional.OrExpression; import net.sf.jsqlparser.expression.operators.relational.ExistsExpression; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.delete.Delete; import net.sf.jsqlparser.statement.select.*; import net.sf.jsqlparser.statement.update.Update; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import java.sql.Connection; import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** * 数据权限拦截器,通过 {@link DataPermissionRule} 数据权限规则,重写 SQL 的方式来实现 * 主要的 SQL 重写方法,可见 {@link #builderExpression(Expression, List)} 方法 * * 整体的代码实现上,参考 {@link com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor} 实现。 * 所以每次 MyBatis Plus 升级时,需要 Review 下其具体的实现是否有变更! * * @author yshop */ @RequiredArgsConstructor public class DataPermissionDatabaseInterceptor extends JsqlParserSupport implements InnerInterceptor { private final DataPermissionRuleFactory ruleFactory; @Getter private final MappedStatementCache mappedStatementCache = new MappedStatementCache(); @Override // SELECT 场景 public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { // 获得 Mapper 对应的数据权限的规则 List rules = ruleFactory.getDataPermissionRule(ms.getId()); if (mappedStatementCache.noRewritable(ms, rules)) { // 如果无需重写,则跳过 return; } PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql); try { // 初始化上下文 ContextHolder.init(rules); // 处理 SQL mpBs.sql(parserSingle(mpBs.sql(), null)); } finally { // 添加是否需要重写的缓存 addMappedStatementCache(ms); // 清空上下文 ContextHolder.clear(); } } @Override // 只处理 UPDATE / DELETE 场景,不处理 INSERT 场景(因为 INSERT 不需要数据权限) public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) { PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh); MappedStatement ms = mpSh.mappedStatement(); SqlCommandType sct = ms.getSqlCommandType(); if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) { // 获得 Mapper 对应的数据权限的规则 List rules = ruleFactory.getDataPermissionRule(ms.getId()); if (mappedStatementCache.noRewritable(ms, rules)) { // 如果无需重写,则跳过 return; } PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql(); try { // 初始化上下文 ContextHolder.init(rules); // 处理 SQL mpBs.sql(parserMulti(mpBs.sql(), null)); } finally { // 添加是否需要重写的缓存 addMappedStatementCache(ms); // 清空上下文 ContextHolder.clear(); } } } @Override protected void processSelect(Select select, int index, String sql, Object obj) { processSelectBody(select.getSelectBody()); List withItemsList = select.getWithItemsList(); if (!CollectionUtils.isEmpty(withItemsList)) { withItemsList.forEach(this::processSelectBody); } } /** * update 语句处理 */ @Override protected void processUpdate(Update update, int index, String sql, Object obj) { final Table table = update.getTable(); update.setWhere(this.builderExpression(update.getWhere(), table)); } /** * delete 语句处理 */ @Override protected void processDelete(Delete delete, int index, String sql, Object obj) { delete.setWhere(this.builderExpression(delete.getWhere(), delete.getTable())); } // ========== 和 TenantLineInnerInterceptor 一致的逻辑 ========== protected void processSelectBody(SelectBody selectBody) { if (selectBody == null) { return; } if (selectBody instanceof PlainSelect) { processPlainSelect((PlainSelect) selectBody); } else if (selectBody instanceof WithItem) { WithItem withItem = (WithItem) selectBody; processSelectBody(withItem.getSubSelect().getSelectBody()); } else { SetOperationList operationList = (SetOperationList) selectBody; List selectBodyList = operationList.getSelects(); if (CollectionUtils.isNotEmpty(selectBodyList)) { selectBodyList.forEach(this::processSelectBody); } } } /** * 处理 PlainSelect */ protected void processPlainSelect(PlainSelect plainSelect) { //#3087 github List selectItems = plainSelect.getSelectItems(); if (CollectionUtils.isNotEmpty(selectItems)) { selectItems.forEach(this::processSelectItem); } // 处理 where 中的子查询 Expression where = plainSelect.getWhere(); processWhereSubSelect(where); // 处理 fromItem FromItem fromItem = plainSelect.getFromItem(); List list = processFromItem(fromItem); List
mainTables = new ArrayList<>(list); // 处理 join List joins = plainSelect.getJoins(); if (CollectionUtils.isNotEmpty(joins)) { mainTables = processJoins(mainTables, joins); } // 当有 mainTable 时,进行 where 条件追加 if (CollectionUtils.isNotEmpty(mainTables)) { plainSelect.setWhere(builderExpression(where, mainTables)); } } private List
processFromItem(FromItem fromItem) { // 处理括号括起来的表达式 while (fromItem instanceof ParenthesisFromItem) { fromItem = ((ParenthesisFromItem) fromItem).getFromItem(); } List
mainTables = new ArrayList<>(); // 无 join 时的处理逻辑 if (fromItem instanceof Table) { Table fromTable = (Table) fromItem; mainTables.add(fromTable); } else if (fromItem instanceof SubJoin) { // SubJoin 类型则还需要添加上 where 条件 List
tables = processSubJoin((SubJoin) fromItem); mainTables.addAll(tables); } else { // 处理下 fromItem processOtherFromItem(fromItem); } return mainTables; } /** * 处理where条件内的子查询 *

* 支持如下: * 1. in * 2. = * 3. > * 4. < * 5. >= * 6. <= * 7. <> * 8. EXISTS * 9. NOT EXISTS *

* 前提条件: * 1. 子查询必须放在小括号中 * 2. 子查询一般放在比较操作符的右边 * * @param where where 条件 */ protected void processWhereSubSelect(Expression where) { if (where == null) { return; } if (where instanceof FromItem) { processOtherFromItem((FromItem) where); return; } if (where.toString().indexOf("SELECT") > 0) { // 有子查询 if (where instanceof BinaryExpression) { // 比较符号 , and , or , 等等 BinaryExpression expression = (BinaryExpression) where; processWhereSubSelect(expression.getLeftExpression()); processWhereSubSelect(expression.getRightExpression()); } else if (where instanceof InExpression) { // in InExpression expression = (InExpression) where; Expression inExpression = expression.getRightExpression(); if (inExpression instanceof SubSelect) { processSelectBody(((SubSelect) inExpression).getSelectBody()); } } else if (where instanceof ExistsExpression) { // exists ExistsExpression expression = (ExistsExpression) where; processWhereSubSelect(expression.getRightExpression()); } else if (where instanceof NotExpression) { // not exists NotExpression expression = (NotExpression) where; processWhereSubSelect(expression.getExpression()); } else if (where instanceof Parenthesis) { Parenthesis expression = (Parenthesis) where; processWhereSubSelect(expression.getExpression()); } } } protected void processSelectItem(SelectItem selectItem) { if (selectItem instanceof SelectExpressionItem) { SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem; if (selectExpressionItem.getExpression() instanceof SubSelect) { processSelectBody(((SubSelect) selectExpressionItem.getExpression()).getSelectBody()); } else if (selectExpressionItem.getExpression() instanceof Function) { processFunction((Function) selectExpressionItem.getExpression()); } } } /** * 处理函数 *

支持: 1. select fun(args..) 2. select fun1(fun2(args..),args..)

*

fixed gitee pulls/141

* * @param function */ protected void processFunction(Function function) { ExpressionList parameters = function.getParameters(); if (parameters != null) { parameters.getExpressions().forEach(expression -> { if (expression instanceof SubSelect) { processSelectBody(((SubSelect) expression).getSelectBody()); } else if (expression instanceof Function) { processFunction((Function) expression); } }); } } /** * 处理子查询等 */ protected void processOtherFromItem(FromItem fromItem) { // 去除括号 while (fromItem instanceof ParenthesisFromItem) { fromItem = ((ParenthesisFromItem) fromItem).getFromItem(); } if (fromItem instanceof SubSelect) { SubSelect subSelect = (SubSelect) fromItem; if (subSelect.getSelectBody() != null) { processSelectBody(subSelect.getSelectBody()); } } else if (fromItem instanceof ValuesList) { logger.debug("Perform a subQuery, if you do not give us feedback"); } else if (fromItem instanceof LateralSubSelect) { LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem; if (lateralSubSelect.getSubSelect() != null) { SubSelect subSelect = lateralSubSelect.getSubSelect(); if (subSelect.getSelectBody() != null) { processSelectBody(subSelect.getSelectBody()); } } } } /** * 处理 sub join * * @param subJoin subJoin * @return Table subJoin 中的主表 */ private List
processSubJoin(SubJoin subJoin) { List
mainTables = new ArrayList<>(); if (subJoin.getJoinList() != null) { List
list = processFromItem(subJoin.getLeft()); mainTables.addAll(list); mainTables = processJoins(mainTables, subJoin.getJoinList()); } return mainTables; } /** * 处理 joins * * @param mainTables 可以为 null * @param joins join 集合 * @return List
右连接查询的 Table 列表 */ private List
processJoins(List
mainTables, List joins) { // join 表达式中最终的主表 Table mainTable = null; // 当前 join 的左表 Table leftTable = null; if (mainTables == null) { mainTables = new ArrayList<>(); } else if (mainTables.size() == 1) { mainTable = mainTables.get(0); leftTable = mainTable; } //对于 on 表达式写在最后的 join,需要记录下前面多个 on 的表名 Deque> onTableDeque = new LinkedList<>(); for (Join join : joins) { // 处理 on 表达式 FromItem joinItem = join.getRightItem(); // 获取当前 join 的表,subJoint 可以看作是一张表 List
joinTables = null; if (joinItem instanceof Table) { joinTables = new ArrayList<>(); joinTables.add((Table) joinItem); } else if (joinItem instanceof SubJoin) { joinTables = processSubJoin((SubJoin) joinItem); } if (joinTables != null) { // 如果是隐式内连接 if (join.isSimple()) { mainTables.addAll(joinTables); continue; } // 当前表是否忽略 Table joinTable = joinTables.get(0); List
onTables = null; // 如果不要忽略,且是右连接,则记录下当前表 if (join.isRight()) { mainTable = joinTable; if (leftTable != null) { onTables = Collections.singletonList(leftTable); } } else if (join.isLeft()) { onTables = Collections.singletonList(joinTable); } else if (join.isInner()) { if (mainTable == null) { onTables = Collections.singletonList(joinTable); } else { onTables = Arrays.asList(mainTable, joinTable); } mainTable = null; } mainTables = new ArrayList<>(); if (mainTable != null) { mainTables.add(mainTable); } // 获取 join 尾缀的 on 表达式列表 Collection originOnExpressions = join.getOnExpressions(); // 正常 join on 表达式只有一个,立刻处理 if (originOnExpressions.size() == 1 && onTables != null) { List onExpressions = new LinkedList<>(); onExpressions.add(builderExpression(originOnExpressions.iterator().next(), onTables)); join.setOnExpressions(onExpressions); leftTable = joinTable; continue; } // 表名压栈,忽略的表压入 null,以便后续不处理 onTableDeque.push(onTables); // 尾缀多个 on 表达式的时候统一处理 if (originOnExpressions.size() > 1) { Collection onExpressions = new LinkedList<>(); for (Expression originOnExpression : originOnExpressions) { List
currentTableList = onTableDeque.poll(); if (CollectionUtils.isEmpty(currentTableList)) { onExpressions.add(originOnExpression); } else { onExpressions.add(builderExpression(originOnExpression, currentTableList)); } } join.setOnExpressions(onExpressions); } leftTable = joinTable; } else { processOtherFromItem(joinItem); leftTable = null; } } return mainTables; } // ========== 和 TenantLineInnerInterceptor 存在差异的逻辑:关键,实现权限条件的拼接 ========== /** * 处理条件 * * @param currentExpression 当前 where 条件 * @param table 单个表 */ protected Expression builderExpression(Expression currentExpression, Table table) { return this.builderExpression(currentExpression, Collections.singletonList(table)); } /** * 处理条件 * * @param currentExpression 当前 where 条件 * @param tables 多个表 */ protected Expression builderExpression(Expression currentExpression, List
tables) { // 没有表需要处理直接返回 if (CollectionUtils.isEmpty(tables)) { return currentExpression; } // 第一步,获得 Table 对应的数据权限条件 Expression dataPermissionExpression = null; for (Table table : tables) { // 构建每个表的权限 Expression 条件 Expression expression = buildDataPermissionExpression(table); if (expression == null) { continue; } // 合并到 dataPermissionExpression 中 dataPermissionExpression = dataPermissionExpression == null ? expression : new AndExpression(dataPermissionExpression, expression); } // 第二步,合并多个 Expression 条件 if (dataPermissionExpression == null) { return currentExpression; } if (currentExpression == null) { return dataPermissionExpression; } // ① 如果表达式为 Or,则需要 (currentExpression) AND dataPermissionExpression if (currentExpression instanceof OrExpression) { return new AndExpression(new Parenthesis(currentExpression), dataPermissionExpression); } // ② 如果表达式为 And,则直接返回 where AND dataPermissionExpression return new AndExpression(currentExpression, dataPermissionExpression); } /** * 构建指定表的数据权限的 Expression 过滤条件 * * @param table 表 * @return Expression 过滤条件 */ private Expression buildDataPermissionExpression(Table table) { // 生成条件 Expression allExpression = null; for (DataPermissionRule rule : ContextHolder.getRules()) { // 判断表名是否匹配 String tableName = MyBatisUtils.getTableName(table); if (!rule.getTableNames().contains(tableName)) { continue; } // 如果有匹配的规则,说明可重写。 // 为什么不是有 allExpression 非空才重写呢?在生成 column = value 过滤条件时,会因为 value 不存在,导致未重写。 // 这样导致第一次无 value,被标记成无需重写;但是第二次有 value,此时会需要重写。 ContextHolder.setRewrite(true); // 单条规则的条件 Expression oneExpress = rule.getExpression(tableName, table.getAlias()); if (oneExpress == null){ continue; } // 拼接到 allExpression 中 allExpression = allExpression == null ? oneExpress : new AndExpression(allExpression, oneExpress); } return allExpression; } /** * 判断 SQL 是否重写。如果没有重写,则添加到 {@link MappedStatementCache} 中 * * @param ms MappedStatement */ private void addMappedStatementCache(MappedStatement ms) { if (ContextHolder.getRewrite()) { return; } // 无重写,进行添加 mappedStatementCache.addNoRewritable(ms, ContextHolder.getRules()); } /** * SQL 解析上下文,方便透传 {@link DataPermissionRule} 规则 * * @author yshop */ static final class ContextHolder { /** * 该 {@link MappedStatement} 对应的规则 */ private static final ThreadLocal> RULES = ThreadLocal.withInitial(Collections::emptyList); /** * SQL 是否进行重写 */ private static final ThreadLocal REWRITE = ThreadLocal.withInitial(() -> Boolean.FALSE); public static void init(List rules) { RULES.set(rules); REWRITE.set(false); } public static void clear() { RULES.remove(); REWRITE.remove(); } public static boolean getRewrite() { return REWRITE.get(); } public static void setRewrite(boolean rewrite) { REWRITE.set(rewrite); } public static List getRules() { return RULES.get(); } } /** * {@link MappedStatement} 缓存 * 目前主要用于,记录 {@link DataPermissionRule} 是否对指定 {@link MappedStatement} 无效 * 如果无效,则可以避免 SQL 的解析,加快速度 * * @author yshop */ static final class MappedStatementCache { /** * 指定数据权限规则,对指定 MappedStatement 无需重写(不生效)的缓存 * * value:{@link MappedStatement#getId()} 编号 */ @Getter private final Map, Set> noRewritableMappedStatements = new ConcurrentHashMap<>(); /** * 判断是否无需重写 * ps:虽然有点中文式英语,但是容易读懂即可 * * @param ms MappedStatement * @param rules 数据权限规则数组 * @return 是否无需重写 */ public boolean noRewritable(MappedStatement ms, List rules) { // 如果规则为空,说明无需重写 if (CollUtil.isEmpty(rules)) { return true; } // 任一规则不在 noRewritableMap 中,则说明可能需要重写 for (DataPermissionRule rule : rules) { Set mappedStatementIds = noRewritableMappedStatements.get(rule.getClass()); if (!CollUtil.contains(mappedStatementIds, ms.getId())) { return false; } } return true; } /** * 添加无需重写的 MappedStatement * * @param ms MappedStatement * @param rules 数据权限规则数组 */ public void addNoRewritable(MappedStatement ms, List rules) { for (DataPermissionRule rule : rules) { Set mappedStatementIds = noRewritableMappedStatements.get(rule.getClass()); if (CollUtil.isNotEmpty(mappedStatementIds)) { mappedStatementIds.add(ms.getId()); } else { noRewritableMappedStatements.put(rule.getClass(), SetUtils.asSet(ms.getId())); } } } /** * 清空缓存 * 目前主要提供给单元测试 */ public void clear() { noRewritableMappedStatements.clear(); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/main/java/co/yixiang/yshop/framework/datapermission/core/rule/DataPermissionRule.java ================================================ package co.yixiang.yshop.framework.datapermission.core.rule; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.Expression; import java.util.Set; /** * 数据权限规则接口 * 通过实现接口,自定义数据规则。例如说, * * @author yshop */ public interface DataPermissionRule { /** * 返回需要生效的表名数组 * 为什么需要该方法?Data Permission 数组基于 SQL 重写,通过 Where 返回只有权限的数据 * * 如果需要基于实体名获得表名,可调用 {@link TableInfoHelper#getTableInfo(Class)} 获得 * * @return 表名数组 */ Set getTableNames(); /** * 根据表名和别名,生成对应的 WHERE / OR 过滤条件 * * @param tableName 表名 * @param tableAlias 别名,可能为空 * @return 过滤条件 Expression 表达式 */ Expression getExpression(String tableName, Alias tableAlias); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/main/java/co/yixiang/yshop/framework/datapermission/core/rule/DataPermissionRuleFactory.java ================================================ package co.yixiang.yshop.framework.datapermission.core.rule; import java.util.List; /** * {@link DataPermissionRule} 工厂接口 * 作为 {@link DataPermissionRule} 的容器,提供管理能力 * * @author yshop */ public interface DataPermissionRuleFactory { /** * 获得所有数据权限规则数组 * * @return 数据权限规则数组 */ List getDataPermissionRules(); /** * 获得指定 Mapper 的数据权限规则数组 * * @param mappedStatementId 指定 Mapper 的编号 * @return 数据权限规则数组 */ List getDataPermissionRule(String mappedStatementId); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/main/java/co/yixiang/yshop/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java ================================================ package co.yixiang.yshop.framework.datapermission.core.rule; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ArrayUtil; import co.yixiang.yshop.framework.datapermission.core.annotation.DataPermission; import co.yixiang.yshop.framework.datapermission.core.aop.DataPermissionContextHolder; import lombok.RequiredArgsConstructor; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; /** * 默认的 DataPermissionRuleFactoryImpl 实现类 * 支持通过 {@link DataPermissionContextHolder} 过滤数据权限 * * @author yshop */ @RequiredArgsConstructor public class DataPermissionRuleFactoryImpl implements DataPermissionRuleFactory { /** * 数据权限规则数组 */ private final List rules; @Override public List getDataPermissionRules() { return rules; } @Override // mappedStatementId 参数,暂时没有用。以后,可以基于 mappedStatementId + DataPermission 进行缓存 public List getDataPermissionRule(String mappedStatementId) { // 1. 无数据权限 if (CollUtil.isEmpty(rules)) { return Collections.emptyList(); } // 2. 未配置,则默认开启 DataPermission dataPermission = DataPermissionContextHolder.get(); if (dataPermission == null) { return rules; } // 3. 已配置,但禁用 if (!dataPermission.enable()) { return Collections.emptyList(); } // 4. 已配置,只选择部分规则 if (ArrayUtil.isNotEmpty(dataPermission.includeRules())) { return rules.stream().filter(rule -> ArrayUtil.contains(dataPermission.includeRules(), rule.getClass())) .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询 } // 5. 已配置,只排除部分规则 if (ArrayUtil.isNotEmpty(dataPermission.excludeRules())) { return rules.stream().filter(rule -> !ArrayUtil.contains(dataPermission.excludeRules(), rule.getClass())) .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询 } // 6. 已配置,全部规则 return rules; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/main/java/co/yixiang/yshop/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java ================================================ package co.yixiang.yshop.framework.datapermission.core.rule.dept; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.framework.datapermission.core.rule.DataPermissionRule; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.framework.mybatis.core.util.MyBatisUtils; import co.yixiang.yshop.framework.security.core.LoginUser; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import co.yixiang.yshop.module.system.api.permission.PermissionApi; import co.yixiang.yshop.module.system.api.permission.dto.DeptDataPermissionRespDTO; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import net.sf.jsqlparser.expression.*; import net.sf.jsqlparser.expression.operators.conditional.OrExpression; import net.sf.jsqlparser.expression.operators.relational.EqualsTo; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.expression.operators.relational.InExpression; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * 基于部门的 {@link DataPermissionRule} 数据权限规则实现 * * 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。 * * 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改? * 1. 一般情况下,dept_id 不进行修改,则会导致用户看不到之前的数据。【yshop-server 采用该方案】 * 2. 部分情况下,希望该用户还是能看到之前的数据,则有两种方式解决:【需要你改造该 DeptDataPermissionRule 的实现代码】 * 1)编写洗数据的脚本,将 dept_id 修改成新部门的编号;【建议】 * 最终过滤条件是 WHERE dept_id = ? * 2)洗数据的话,可能涉及的数据量较大,也可以采用 user_id 进行过滤的方式,此时需要获取到 dept_id 对应的所有 user_id 用户编号; * 最终过滤条件是 WHERE user_id IN (?, ?, ? ...) * 3)想要保证原 dept_id 和 user_id 都可以看的到,此时使用 dept_id 和 user_id 一起过滤; * 最终过滤条件是 WHERE dept_id = ? OR user_id IN (?, ?, ? ...) * * @author yshop */ @AllArgsConstructor @Slf4j public class DeptDataPermissionRule implements DataPermissionRule { /** * LoginUser 的 Context 缓存 Key */ protected static final String CONTEXT_KEY = DeptDataPermissionRule.class.getSimpleName(); private static final String DEPT_COLUMN_NAME = "dept_id"; private static final String USER_COLUMN_NAME = "user_id"; static final Expression EXPRESSION_NULL = new NullValue(); private final PermissionApi permissionApi; /** * 基于部门的表字段配置 * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 * * key:表名 * value:字段名 */ private final Map deptColumns = new HashMap<>(); /** * 基于用户的表字段配置 * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 * * key:表名 * value:字段名 */ private final Map userColumns = new HashMap<>(); /** * 所有表名,是 {@link #deptColumns} 和 {@link #userColumns} 的合集 */ private final Set TABLE_NAMES = new HashSet<>(); @Override public Set getTableNames() { return TABLE_NAMES; } @Override public Expression getExpression(String tableName, Alias tableAlias) { // 只有有登陆用户的情况下,才进行数据权限的处理 LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); if (loginUser == null) { return null; } // 只有管理员类型的用户,才进行数据权限的处理 if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) { return null; } // 获得数据权限 DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class); // 从上下文中拿不到,则调用逻辑进行获取 if (deptDataPermission == null) { deptDataPermission = permissionApi.getDeptDataPermission(loginUser.getId()); if (deptDataPermission == null) { log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser)); throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限", loginUser.getId(), tableName, tableAlias.getName())); } // 添加到上下文中,避免重复计算 loginUser.setContext(CONTEXT_KEY, deptDataPermission); } // 情况一,如果是 ALL 可查看全部,则无需拼接条件 if (deptDataPermission.getAll()) { return null; } // 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限 if (CollUtil.isEmpty(deptDataPermission.getDeptIds()) && Boolean.FALSE.equals(deptDataPermission.getSelf())) { return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空 } // 情况三,拼接 Dept 和 User 的条件,最后组合 Expression deptExpression = buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds()); Expression userExpression = buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId()); if (deptExpression == null && userExpression == null) { // TODO yshop:获得不到条件的时候,暂时不抛出异常,而是不返回数据 log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]", JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission)); // throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空", // loginUser.getId(), tableName, tableAlias.getName())); return EXPRESSION_NULL; } if (deptExpression == null) { return userExpression; } if (userExpression == null) { return deptExpression; } // 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE (dept_id IN ? OR user_id = ?) return new Parenthesis(new OrExpression(deptExpression, userExpression)); } private Expression buildDeptExpression(String tableName, Alias tableAlias, Set deptIds) { // 如果不存在配置,则无需作为条件 String columnName = deptColumns.get(tableName); if (StrUtil.isEmpty(columnName)) { return null; } // 如果为空,则无条件 if (CollUtil.isEmpty(deptIds)) { return null; } // 拼接条件 return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new ExpressionList(CollectionUtils.convertList(deptIds, LongValue::new))); } private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) { // 如果不查看自己,则无需作为条件 if (Boolean.FALSE.equals(self)) { return null; } String columnName = userColumns.get(tableName); if (StrUtil.isEmpty(columnName)) { return null; } // 拼接条件 return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId)); } // ==================== 添加配置 ==================== public void addDeptColumn(Class entityClass) { addDeptColumn(entityClass, DEPT_COLUMN_NAME); } public void addDeptColumn(Class entityClass, String columnName) { String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); addDeptColumn(tableName, columnName); } public void addDeptColumn(String tableName, String columnName) { deptColumns.put(tableName, columnName); TABLE_NAMES.add(tableName); } public void addUserColumn(Class entityClass) { addUserColumn(entityClass, USER_COLUMN_NAME); } public void addUserColumn(Class entityClass, String columnName) { String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); addUserColumn(tableName, columnName); } public void addUserColumn(String tableName, String columnName) { userColumns.put(tableName, columnName); TABLE_NAMES.add(tableName); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/main/java/co/yixiang/yshop/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java ================================================ package co.yixiang.yshop.framework.datapermission.core.rule.dept; /** * {@link DeptDataPermissionRule} 的自定义配置接口 * * @author yshop */ @FunctionalInterface public interface DeptDataPermissionRuleCustomizer { /** * 自定义该权限规则 * 1. 调用 {@link DeptDataPermissionRule#addDeptColumn(Class, String)} 方法,配置基于 dept_id 的过滤规则 * 2. 调用 {@link DeptDataPermissionRule#addUserColumn(Class, String)} 方法,配置基于 user_id 的过滤规则 * * @param rule 权限规则 */ void customize(DeptDataPermissionRule rule); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/main/java/co/yixiang/yshop/framework/datapermission/core/rule/dept/package-info.java ================================================ /** * 基于部门的数据权限规则 * * @author yshop */ package co.yixiang.yshop.framework.datapermission.core.rule.dept; ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/main/java/co/yixiang/yshop/framework/datapermission/core/util/DataPermissionUtils.java ================================================ package co.yixiang.yshop.framework.datapermission.core.util; import co.yixiang.yshop.framework.datapermission.core.annotation.DataPermission; import co.yixiang.yshop.framework.datapermission.core.aop.DataPermissionContextHolder; import lombok.SneakyThrows; import java.util.concurrent.Callable; /** * 数据权限 Util * * @author yshop */ public class DataPermissionUtils { private static DataPermission DATA_PERMISSION_DISABLE; @DataPermission(enable = false) @SneakyThrows private static DataPermission getDisableDataPermissionDisable() { if (DATA_PERMISSION_DISABLE == null) { DATA_PERMISSION_DISABLE = DataPermissionUtils.class .getDeclaredMethod("getDisableDataPermissionDisable") .getAnnotation(DataPermission.class); } return DATA_PERMISSION_DISABLE; } /** * 忽略数据权限,执行对应的逻辑 * * @param runnable 逻辑 */ public static void executeIgnore(Runnable runnable) { DataPermission dataPermission = getDisableDataPermissionDisable(); DataPermissionContextHolder.add(dataPermission); try { // 执行 runnable runnable.run(); } finally { DataPermissionContextHolder.remove(); } } /** * 忽略数据权限,执行对应的逻辑 * * @param callable 逻辑 * @return 执行结果 */ @SneakyThrows public static T executeIgnore(Callable callable) { DataPermission dataPermission = getDisableDataPermissionDisable(); DataPermissionContextHolder.add(dataPermission); try { // 执行 callable return callable.call(); } finally { DataPermissionContextHolder.remove(); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ co.yixiang.yshop.framework.datapermission.config.YshopDataPermissionAutoConfiguration co.yixiang.yshop.framework.datapermission.config.YshopDeptDataPermissionAutoConfiguration ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/test/java/co/yixiang/yshop/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java ================================================ package co.yixiang.yshop.framework.datapermission.core.aop; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.datapermission.core.annotation.DataPermission; import co.yixiang.yshop.framework.test.core.ut.BaseMockitoUnitTest; import org.aopalliance.intercept.MethodInvocation; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import java.lang.reflect.Method; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.when; /** * {@link DataPermissionAnnotationInterceptor} 的单元测试 * * @author yshop */ public class DataPermissionAnnotationInterceptorTest extends BaseMockitoUnitTest { @InjectMocks private DataPermissionAnnotationInterceptor interceptor; @Mock private MethodInvocation methodInvocation; @BeforeEach public void setUp() { interceptor.getDataPermissionCache().clear(); } @Test // 无 @DataPermission 注解 public void testInvoke_none() throws Throwable { // 参数 mockMethodInvocation(TestNone.class); // 调用 Object result = interceptor.invoke(methodInvocation); // 断言 assertEquals("none", result); assertEquals(1, interceptor.getDataPermissionCache().size()); assertTrue(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); } @Test // 在 Method 上有 @DataPermission 注解 public void testInvoke_method() throws Throwable { // 参数 mockMethodInvocation(TestMethod.class); // 调用 Object result = interceptor.invoke(methodInvocation); // 断言 assertEquals("method", result); assertEquals(1, interceptor.getDataPermissionCache().size()); assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); } @Test // 在 Class 上有 @DataPermission 注解 public void testInvoke_class() throws Throwable { // 参数 mockMethodInvocation(TestClass.class); // 调用 Object result = interceptor.invoke(methodInvocation); // 断言 assertEquals("class", result); assertEquals(1, interceptor.getDataPermissionCache().size()); assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); } private void mockMethodInvocation(Class clazz) throws Throwable { Object targetObject = clazz.newInstance(); Method method = targetObject.getClass().getMethod("echo"); when(methodInvocation.getThis()).thenReturn(targetObject); when(methodInvocation.getMethod()).thenReturn(method); when(methodInvocation.proceed()).then(invocationOnMock -> method.invoke(targetObject)); } static class TestMethod { @DataPermission(enable = false) public String echo() { return "method"; } } @DataPermission(enable = false) static class TestClass { public String echo() { return "class"; } } static class TestNone { public String echo() { return "none"; } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/test/java/co/yixiang/yshop/framework/datapermission/core/aop/DataPermissionContextHolderTest.java ================================================ package co.yixiang.yshop.framework.datapermission.core.aop; import co.yixiang.yshop.framework.datapermission.core.annotation.DataPermission; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; import static org.mockito.Mockito.mock; /** * {@link DataPermissionContextHolder} 的单元测试 * * @author yshop */ class DataPermissionContextHolderTest { @BeforeEach public void setUp() { DataPermissionContextHolder.clear(); } @Test public void testGet() { // mock 方法 DataPermission dataPermission01 = mock(DataPermission.class); DataPermissionContextHolder.add(dataPermission01); DataPermission dataPermission02 = mock(DataPermission.class); DataPermissionContextHolder.add(dataPermission02); // 调用 DataPermission result = DataPermissionContextHolder.get(); // 断言 assertSame(result, dataPermission02); } @Test public void testPush() { // 调用 DataPermission dataPermission01 = mock(DataPermission.class); DataPermissionContextHolder.add(dataPermission01); DataPermission dataPermission02 = mock(DataPermission.class); DataPermissionContextHolder.add(dataPermission02); // 断言 DataPermission first = DataPermissionContextHolder.getAll().get(0); DataPermission second = DataPermissionContextHolder.getAll().get(1); assertSame(dataPermission01, first); assertSame(dataPermission02, second); } @Test public void testRemove() { // mock 方法 DataPermission dataPermission01 = mock(DataPermission.class); DataPermissionContextHolder.add(dataPermission01); DataPermission dataPermission02 = mock(DataPermission.class); DataPermissionContextHolder.add(dataPermission02); // 调用 DataPermission result = DataPermissionContextHolder.remove(); // 断言 assertSame(result, dataPermission02); assertEquals(1, DataPermissionContextHolder.getAll().size()); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/test/java/co/yixiang/yshop/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java ================================================ package co.yixiang.yshop.framework.datapermission.core.db; import co.yixiang.yshop.framework.common.util.collection.SetUtils; import co.yixiang.yshop.framework.datapermission.core.rule.DataPermissionRule; import co.yixiang.yshop.framework.datapermission.core.rule.DataPermissionRuleFactory; import co.yixiang.yshop.framework.mybatis.core.util.MyBatisUtils; import co.yixiang.yshop.framework.test.core.ut.BaseMockitoUnitTest; import com.baomidou.mybatisplus.core.toolkit.PluginUtils; import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.operators.relational.EqualsTo; import net.sf.jsqlparser.schema.Column; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; import java.sql.Connection; import java.util.*; import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; /** * {@link DataPermissionDatabaseInterceptor} 的单元测试 * 主要测试 {@link DataPermissionDatabaseInterceptor#beforePrepare(StatementHandler, Connection, Integer)} * 和 {@link DataPermissionDatabaseInterceptor#beforeUpdate(Executor, MappedStatement, Object)} * 以及在这个过程中,ContextHolder 和 MappedStatementCache * * @author yshop */ public class DataPermissionDatabaseInterceptorTest extends BaseMockitoUnitTest { @InjectMocks private DataPermissionDatabaseInterceptor interceptor; @Mock private DataPermissionRuleFactory ruleFactory; @BeforeEach public void setUp() { // 清理上下文 DataPermissionDatabaseInterceptor.ContextHolder.clear(); // 清空缓存 interceptor.getMappedStatementCache().clear(); } @Test // 不存在规则,且不匹配 public void testBeforeQuery_withoutRule() { try (MockedStatic pluginUtilsMock = mockStatic(PluginUtils.class)) { // 准备参数 MappedStatement mappedStatement = mock(MappedStatement.class); BoundSql boundSql = mock(BoundSql.class); // 调用 interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql); // 断言 pluginUtilsMock.verify(() -> PluginUtils.mpBoundSql(boundSql), never()); } } @Test // 存在规则,且不匹配 public void testBeforeQuery_withMatchRule() { try (MockedStatic pluginUtilsMock = mockStatic(PluginUtils.class)) { // 准备参数 MappedStatement mappedStatement = mock(MappedStatement.class); BoundSql boundSql = mock(BoundSql.class); // mock 方法(数据权限) when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId()))) .thenReturn(singletonList(new DeptDataPermissionRule())); // mock 方法(MPBoundSql) PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class); pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs); // mock 方法(SQL) String sql = "select * from t_user where id = 1"; when(mpBs.sql()).thenReturn(sql); // 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确 // 调用 interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql); // 断言 verify(mpBs, times(1)).sql( eq("SELECT * FROM t_user WHERE id = 1 AND t_user.dept_id = 100")); // 断言缓存 assertTrue(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty()); } } @Test // 存在规则,但不匹配 public void testBeforeQuery_withoutMatchRule() { try (MockedStatic pluginUtilsMock = mockStatic(PluginUtils.class)) { // 准备参数 MappedStatement mappedStatement = mock(MappedStatement.class); BoundSql boundSql = mock(BoundSql.class); // mock 方法(数据权限) when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId()))) .thenReturn(singletonList(new DeptDataPermissionRule())); // mock 方法(MPBoundSql) PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class); pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs); // mock 方法(SQL) String sql = "select * from t_role where id = 1"; when(mpBs.sql()).thenReturn(sql); // 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确 // 调用 interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql); // 断言 verify(mpBs, times(1)).sql( eq("SELECT * FROM t_role WHERE id = 1")); // 断言缓存 assertFalse(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty()); } } @Test public void testAddNoRewritable() { // 准备参数 MappedStatement ms = mock(MappedStatement.class); List rules = singletonList(new DeptDataPermissionRule()); // mock 方法 when(ms.getId()).thenReturn("selectById"); // 调用 interceptor.getMappedStatementCache().addNoRewritable(ms, rules); // 断言 Map, Set> noRewritableMappedStatements = interceptor.getMappedStatementCache().getNoRewritableMappedStatements(); assertEquals(1, noRewritableMappedStatements.size()); assertEquals(SetUtils.asSet("selectById"), noRewritableMappedStatements.get(DeptDataPermissionRule.class)); } @Test public void testNoRewritable() { // 准备参数 MappedStatement ms = mock(MappedStatement.class); // mock 方法 when(ms.getId()).thenReturn("selectById"); // mock 数据 List rules = singletonList(new DeptDataPermissionRule()); interceptor.getMappedStatementCache().addNoRewritable(ms, rules); // 场景一,rules 为空 assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, null)); // 场景二,rules 非空,可重写 assertFalse(interceptor.getMappedStatementCache().noRewritable(ms, singletonList(new EmptyDataPermissionRule()))); // 场景三,rule 非空,不可重写 assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, rules)); } private static class DeptDataPermissionRule implements DataPermissionRule { private static final String COLUMN = "dept_id"; @Override public Set getTableNames() { return SetUtils.asSet("t_user"); } @Override public Expression getExpression(String tableName, Alias tableAlias) { Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); LongValue value = new LongValue(100L); return new EqualsTo(column, value); } } private static class EmptyDataPermissionRule implements DataPermissionRule { @Override public Set getTableNames() { return Collections.emptySet(); } @Override public Expression getExpression(String tableName, Alias tableAlias) { return null; } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/test/java/co/yixiang/yshop/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java ================================================ package co.yixiang.yshop.framework.datapermission.core.db; import co.yixiang.yshop.framework.datapermission.core.rule.DataPermissionRule; import co.yixiang.yshop.framework.datapermission.core.rule.DataPermissionRuleFactory; import co.yixiang.yshop.framework.mybatis.core.util.MyBatisUtils; import co.yixiang.yshop.framework.test.core.ut.BaseMockitoUnitTest; import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.operators.relational.EqualsTo; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.schema.Column; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import java.util.Arrays; import java.util.Set; import static co.yixiang.yshop.framework.common.util.collection.SetUtils.asSet; import static org.junit.jupiter.api.Assertions.assertEquals; /** * {@link DataPermissionDatabaseInterceptor} 的单元测试 * 主要复用了 MyBatis Plus 的 TenantLineInnerInterceptorTest 的单元测试 * 不过它的单元测试不是很规范,考虑到是复用的,所以暂时不进行修改~ * * @author yshop */ public class DataPermissionDatabaseInterceptorTest2 extends BaseMockitoUnitTest { @InjectMocks private DataPermissionDatabaseInterceptor interceptor; @Mock private DataPermissionRuleFactory ruleFactory; @BeforeEach public void setUp() { // 租户的数据权限规则 DataPermissionRule tenantRule = new DataPermissionRule() { private static final String COLUMN = "tenant_id"; @Override public Set getTableNames() { return asSet("entity", "entity1", "entity2", "entity3", "t1", "t2", "sys_dict_item", // 支持 MyBatis Plus 的单元测试 "t_user", "t_role"); // 满足自己的单元测试 } @Override public Expression getExpression(String tableName, Alias tableAlias) { Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); LongValue value = new LongValue(1L); return new EqualsTo(column, value); } }; // 部门的数据权限规则 DataPermissionRule deptRule = new DataPermissionRule() { private static final String COLUMN = "dept_id"; @Override public Set getTableNames() { return asSet("t_user"); // 满足自己的单元测试 } @Override public Expression getExpression(String tableName, Alias tableAlias) { Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); ExpressionList values = new ExpressionList(new LongValue(10L), new LongValue(20L)); return new InExpression(column, values); } }; // 设置到上下文,保证 DataPermissionDatabaseInterceptor.ContextHolder.init(Arrays.asList(tenantRule, deptRule)); } @Test void delete() { assertSql("delete from entity where id = ?", "DELETE FROM entity WHERE id = ? AND entity.tenant_id = 1"); } @Test void update() { assertSql("update entity set name = ? where id = ?", "UPDATE entity SET name = ? WHERE id = ? AND entity.tenant_id = 1"); } @Test void selectSingle() { // 单表 assertSql("select * from entity where id = ?", "SELECT * FROM entity WHERE id = ? AND entity.tenant_id = 1"); assertSql("select * from entity where id = ? or name = ?", "SELECT * FROM entity WHERE (id = ? OR name = ?) AND entity.tenant_id = 1"); assertSql("SELECT * FROM entity WHERE (id = ? OR name = ?)", "SELECT * FROM entity WHERE (id = ? OR name = ?) AND entity.tenant_id = 1"); /* not */ assertSql("SELECT * FROM entity WHERE not (id = ? OR name = ?)", "SELECT * FROM entity WHERE NOT (id = ? OR name = ?) AND entity.tenant_id = 1"); } @Test void selectSubSelectIn() { /* in */ assertSql("SELECT * FROM entity e WHERE e.id IN (select e1.id from entity1 e1 where e1.id = ?)", "SELECT * FROM entity e WHERE e.id IN (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); // 在最前 assertSql("SELECT * FROM entity e WHERE e.id IN " + "(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?", "SELECT * FROM entity e WHERE e.id IN " + "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1"); // 在最后 assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " + "(select e1.id from entity1 e1 where e1.id = ?)", "SELECT * FROM entity e WHERE e.id = ? AND e.id IN " + "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); // 在中间 assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " + "(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?", "SELECT * FROM entity e WHERE e.id = ? AND e.id IN " + "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1"); } @Test void selectSubSelectEq() { /* = */ assertSql("SELECT * FROM entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?)", "SELECT * FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); } @Test void selectSubSelectInnerNotEq() { /* inner not = */ assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?))", "SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1)) AND e.tenant_id = 1"); assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?) and e.id = ?)", "SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ?) AND e.tenant_id = 1"); } @Test void selectSubSelectExists() { /* EXISTS */ assertSql("SELECT * FROM entity e WHERE EXISTS (select e1.id from entity1 e1 where e1.id = ?)", "SELECT * FROM entity e WHERE EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); /* NOT EXISTS */ assertSql("SELECT * FROM entity e WHERE NOT EXISTS (select e1.id from entity1 e1 where e1.id = ?)", "SELECT * FROM entity e WHERE NOT EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); } @Test void selectSubSelect() { /* >= */ assertSql("SELECT * FROM entity e WHERE e.id >= (select e1.id from entity1 e1 where e1.id = ?)", "SELECT * FROM entity e WHERE e.id >= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); /* <= */ assertSql("SELECT * FROM entity e WHERE e.id <= (select e1.id from entity1 e1 where e1.id = ?)", "SELECT * FROM entity e WHERE e.id <= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); /* <> */ assertSql("SELECT * FROM entity e WHERE e.id <> (select e1.id from entity1 e1 where e1.id = ?)", "SELECT * FROM entity e WHERE e.id <> (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); } @Test void selectFromSelect() { assertSql("SELECT * FROM (select e.id from entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?))", "SELECT * FROM (SELECT e.id FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1)"); } @Test void selectBodySubSelect() { assertSql("select t1.col1,(select t2.col2 from t2 t2 where t1.col1=t2.col1) from t1 t1", "SELECT t1.col1, (SELECT t2.col2 FROM t2 t2 WHERE t1.col1 = t2.col1 AND t2.tenant_id = 1) FROM t1 t1 WHERE t1.tenant_id = 1"); } @Test void selectLeftJoin() { // left join assertSql("SELECT * FROM entity e " + "left join entity1 e1 on e1.id = e.id " + "WHERE e.id = ? OR e.name = ?", "SELECT * FROM entity e " + "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); assertSql("SELECT * FROM entity e " + "left join entity1 e1 on e1.id = e.id " + "WHERE (e.id = ? OR e.name = ?)", "SELECT * FROM entity e " + "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); assertSql("SELECT * FROM entity e " + "left join entity1 e1 on e1.id = e.id " + "left join entity2 e2 on e1.id = e2.id", "SELECT * FROM entity e " + "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + "LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1 " + "WHERE e.tenant_id = 1"); } @Test void selectRightJoin() { // right join assertSql("SELECT * FROM entity e " + "right join entity1 e1 on e1.id = e.id", "SELECT * FROM entity e " + "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " + "WHERE e1.tenant_id = 1"); assertSql("SELECT * FROM with_as_1 e " + "right join entity1 e1 on e1.id = e.id", "SELECT * FROM with_as_1 e " + "RIGHT JOIN entity1 e1 ON e1.id = e.id " + "WHERE e1.tenant_id = 1"); assertSql("SELECT * FROM entity e " + "right join entity1 e1 on e1.id = e.id " + "WHERE e.id = ? OR e.name = ?", "SELECT * FROM entity e " + "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " + "WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1"); assertSql("SELECT * FROM entity e " + "right join entity1 e1 on e1.id = e.id " + "right join entity2 e2 on e1.id = e2.id ", "SELECT * FROM entity e " + "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " + "RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1 " + "WHERE e2.tenant_id = 1"); } @Test void selectMixJoin() { assertSql("SELECT * FROM entity e " + "right join entity1 e1 on e1.id = e.id " + "left join entity2 e2 on e1.id = e2.id", "SELECT * FROM entity e " + "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " + "LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1 " + "WHERE e1.tenant_id = 1"); assertSql("SELECT * FROM entity e " + "left join entity1 e1 on e1.id = e.id " + "right join entity2 e2 on e1.id = e2.id", "SELECT * FROM entity e " + "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + "RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1 " + "WHERE e2.tenant_id = 1"); assertSql("SELECT * FROM entity e " + "left join entity1 e1 on e1.id = e.id " + "inner join entity2 e2 on e1.id = e2.id", "SELECT * FROM entity e " + "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + "INNER JOIN entity2 e2 ON e1.id = e2.id AND e.tenant_id = 1 AND e2.tenant_id = 1"); } @Test void selectJoinSubSelect() { assertSql("select * from (select * from entity) e1 " + "left join entity2 e2 on e1.id = e2.id", "SELECT * FROM (SELECT * FROM entity WHERE entity.tenant_id = 1) e1 " + "LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1"); assertSql("select * from entity1 e1 " + "left join (select * from entity2) e2 " + "on e1.id = e2.id", "SELECT * FROM entity1 e1 " + "LEFT JOIN (SELECT * FROM entity2 WHERE entity2.tenant_id = 1) e2 " + "ON e1.id = e2.id " + "WHERE e1.tenant_id = 1"); } @Test void selectSubJoin() { assertSql("select * FROM " + "(entity1 e1 right JOIN entity2 e2 ON e1.id = e2.id)", "SELECT * FROM " + "(entity1 e1 RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1) " + "WHERE e2.tenant_id = 1"); assertSql("select * FROM " + "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id)", "SELECT * FROM " + "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " + "WHERE e1.tenant_id = 1"); assertSql("select * FROM " + "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id) " + "right join entity3 e3 on e1.id = e3.id", "SELECT * FROM " + "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " + "RIGHT JOIN entity3 e3 ON e1.id = e3.id AND e1.tenant_id = 1 " + "WHERE e3.tenant_id = 1"); assertSql("select * FROM entity e " + "LEFT JOIN (entity1 e1 right join entity2 e2 ON e1.id = e2.id) " + "on e.id = e2.id", "SELECT * FROM entity e " + "LEFT JOIN (entity1 e1 RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1) " + "ON e.id = e2.id AND e2.tenant_id = 1 " + "WHERE e.tenant_id = 1"); assertSql("select * FROM entity e " + "LEFT JOIN (entity1 e1 left join entity2 e2 ON e1.id = e2.id) " + "on e.id = e2.id", "SELECT * FROM entity e " + "LEFT JOIN (entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " + "ON e.id = e2.id AND e1.tenant_id = 1 " + "WHERE e.tenant_id = 1"); assertSql("select * FROM entity e " + "RIGHT JOIN (entity1 e1 left join entity2 e2 ON e1.id = e2.id) " + "on e.id = e2.id", "SELECT * FROM entity e " + "RIGHT JOIN (entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " + "ON e.id = e2.id AND e.tenant_id = 1 " + "WHERE e1.tenant_id = 1"); } @Test void selectLeftJoinMultipleTrailingOn() { // 多个 on 尾缀的 assertSql("SELECT * FROM entity e " + "LEFT JOIN entity1 e1 " + "LEFT JOIN entity2 e2 ON e2.id = e1.id " + "ON e1.id = e.id " + "WHERE (e.id = ? OR e.NAME = ?)", "SELECT * FROM entity e " + "LEFT JOIN entity1 e1 " + "LEFT JOIN entity2 e2 ON e2.id = e1.id AND e2.tenant_id = 1 " + "ON e1.id = e.id AND e1.tenant_id = 1 " + "WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1"); assertSql("SELECT * FROM entity e " + "LEFT JOIN entity1 e1 " + "LEFT JOIN with_as_A e2 ON e2.id = e1.id " + "ON e1.id = e.id " + "WHERE (e.id = ? OR e.NAME = ?)", "SELECT * FROM entity e " + "LEFT JOIN entity1 e1 " + "LEFT JOIN with_as_A e2 ON e2.id = e1.id " + "ON e1.id = e.id AND e1.tenant_id = 1 " + "WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1"); } @Test void selectInnerJoin() { // inner join assertSql("SELECT * FROM entity e " + "inner join entity1 e1 on e1.id = e.id " + "WHERE e.id = ? OR e.name = ?", "SELECT * FROM entity e " + "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e1.tenant_id = 1 " + "WHERE e.id = ? OR e.name = ?"); assertSql("SELECT * FROM entity e " + "inner join entity1 e1 on e1.id = e.id " + "WHERE (e.id = ? OR e.name = ?)", "SELECT * FROM entity e " + "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e1.tenant_id = 1 " + "WHERE (e.id = ? OR e.name = ?)"); // 隐式内连接 assertSql("SELECT * FROM entity,entity1 " + "WHERE entity.id = entity1.id", "SELECT * FROM entity, entity1 " + "WHERE entity.id = entity1.id AND entity.tenant_id = 1 AND entity1.tenant_id = 1"); // 隐式内连接 assertSql("SELECT * FROM entity a, with_as_entity1 b " + "WHERE a.id = b.id", "SELECT * FROM entity a, with_as_entity1 b " + "WHERE a.id = b.id AND a.tenant_id = 1"); assertSql("SELECT * FROM with_as_entity a, with_as_entity1 b " + "WHERE a.id = b.id", "SELECT * FROM with_as_entity a, with_as_entity1 b " + "WHERE a.id = b.id"); // SubJoin with 隐式内连接 assertSql("SELECT * FROM (entity,entity1) " + "WHERE entity.id = entity1.id", "SELECT * FROM (entity, entity1) " + "WHERE entity.id = entity1.id " + "AND entity.tenant_id = 1 AND entity1.tenant_id = 1"); assertSql("SELECT * FROM ((entity,entity1),entity2) " + "WHERE entity.id = entity1.id and entity.id = entity2.id", "SELECT * FROM ((entity, entity1), entity2) " + "WHERE entity.id = entity1.id AND entity.id = entity2.id " + "AND entity.tenant_id = 1 AND entity1.tenant_id = 1 AND entity2.tenant_id = 1"); assertSql("SELECT * FROM (entity,(entity1,entity2)) " + "WHERE entity.id = entity1.id and entity.id = entity2.id", "SELECT * FROM (entity, (entity1, entity2)) " + "WHERE entity.id = entity1.id AND entity.id = entity2.id " + "AND entity.tenant_id = 1 AND entity1.tenant_id = 1 AND entity2.tenant_id = 1"); // 沙雕的括号写法 assertSql("SELECT * FROM (((entity,entity1))) " + "WHERE entity.id = entity1.id", "SELECT * FROM (((entity, entity1))) " + "WHERE entity.id = entity1.id " + "AND entity.tenant_id = 1 AND entity1.tenant_id = 1"); } @Test void selectWithAs() { assertSql("with with_as_A as (select * from entity) select * from with_as_A", "WITH with_as_A AS (SELECT * FROM entity WHERE entity.tenant_id = 1) SELECT * FROM with_as_A"); } @Test void selectIgnoreTable() { assertSql(" SELECT dict.dict_code, item.item_text AS \"text\", item.item_value AS \"value\" FROM sys_dict_item item INNER JOIN sys_dict dict ON dict.id = item.dict_id WHERE dict.dict_code IN (1, 2, 3) AND item.item_value IN (1, 2, 3)", "SELECT dict.dict_code, item.item_text AS \"text\", item.item_value AS \"value\" FROM sys_dict_item item INNER JOIN sys_dict dict ON dict.id = item.dict_id AND item.tenant_id = 1 WHERE dict.dict_code IN (1, 2, 3) AND item.item_value IN (1, 2, 3)"); } private void assertSql(String sql, String targetSql) { assertEquals(targetSql, interceptor.parserSingle(sql, null)); } // ========== 额外的测试 ========== @Test public void testSelectSingle() { // 单表 assertSql("select * from t_user where id = ?", "SELECT * FROM t_user WHERE id = ? AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)"); assertSql("select * from t_user where id = ? or name = ?", "SELECT * FROM t_user WHERE (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)"); assertSql("SELECT * FROM t_user WHERE (id = ? OR name = ?)", "SELECT * FROM t_user WHERE (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)"); /* not */ assertSql("SELECT * FROM t_user WHERE not (id = ? OR name = ?)", "SELECT * FROM t_user WHERE NOT (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)"); } @Test public void testSelectLeftJoin() { // left join assertSql("SELECT * FROM t_user e " + "left join t_role e1 on e1.id = e.id " + "WHERE e.id = ? OR e.name = ?", "SELECT * FROM t_user e " + "LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); // 条件 e.id = ? OR e.name = ? 带括号 assertSql("SELECT * FROM t_user e " + "left join t_role e1 on e1.id = e.id " + "WHERE (e.id = ? OR e.name = ?)", "SELECT * FROM t_user e " + "LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); } @Test public void testSelectRightJoin() { // right join assertSql("SELECT * FROM t_user e " + "right join t_role e1 on e1.id = e.id " + "WHERE e.id = ? OR e.name = ?", "SELECT * FROM t_user e " + "RIGHT JOIN t_role e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) " + "WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1"); // 条件 e.id = ? OR e.name = ? 带括号 assertSql("SELECT * FROM t_user e " + "right join t_role e1 on e1.id = e.id " + "WHERE (e.id = ? OR e.name = ?)", "SELECT * FROM t_user e " + "RIGHT JOIN t_role e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) " + "WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1"); } @Test public void testSelectInnerJoin() { // inner join assertSql("SELECT * FROM t_user e " + "inner join entity1 e1 on e1.id = e.id " + "WHERE e.id = ? OR e.name = ?", "SELECT * FROM t_user e " + "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) AND e1.tenant_id = 1 " + "WHERE e.id = ? OR e.name = ?"); // 条件 e.id = ? OR e.name = ? 带括号 assertSql("SELECT * FROM t_user e " + "inner join entity1 e1 on e1.id = e.id " + "WHERE (e.id = ? OR e.name = ?)", "SELECT * FROM t_user e " + "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) AND e1.tenant_id = 1 " + "WHERE (e.id = ? OR e.name = ?)"); // 没有 On 的 inner join assertSql("SELECT * FROM entity,entity1 " + "WHERE entity.id = entity1.id", "SELECT * FROM entity, entity1 " + "WHERE entity.id = entity1.id AND entity.tenant_id = 1 AND entity1.tenant_id = 1"); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/test/java/co/yixiang/yshop/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java ================================================ package co.yixiang.yshop.framework.datapermission.core.rule; import co.yixiang.yshop.framework.datapermission.core.annotation.DataPermission; import co.yixiang.yshop.framework.datapermission.core.aop.DataPermissionContextHolder; import co.yixiang.yshop.framework.test.core.ut.BaseMockitoUnitTest; import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.Expression; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Spy; import org.springframework.core.annotation.AnnotationUtils; import java.util.Arrays; import java.util.List; import java.util.Set; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomString; import static org.junit.jupiter.api.Assertions.*; /** * {@link DataPermissionRuleFactoryImpl} 单元测试 * * @author yshop */ class DataPermissionRuleFactoryImplTest extends BaseMockitoUnitTest { @InjectMocks private DataPermissionRuleFactoryImpl dataPermissionRuleFactory; @Spy private List rules = Arrays.asList(new DataPermissionRule01(), new DataPermissionRule02()); @BeforeEach public void setUp() { DataPermissionContextHolder.clear(); } @Test public void testGetDataPermissionRule_02() { // 准备参数 String mappedStatementId = randomString(); // 调用 List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); // 断言 assertSame(rules, result); } @Test public void testGetDataPermissionRule_03() { // 准备参数 String mappedStatementId = randomString(); // mock 方法 DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass03.class, DataPermission.class)); // 调用 List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); // 断言 assertTrue(result.isEmpty()); } @Test public void testGetDataPermissionRule_04() { // 准备参数 String mappedStatementId = randomString(); // mock 方法 DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass04.class, DataPermission.class)); // 调用 List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); // 断言 assertEquals(1, result.size()); assertEquals(DataPermissionRule01.class, result.get(0).getClass()); } @Test public void testGetDataPermissionRule_05() { // 准备参数 String mappedStatementId = randomString(); // mock 方法 DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass05.class, DataPermission.class)); // 调用 List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); // 断言 assertEquals(1, result.size()); assertEquals(DataPermissionRule02.class, result.get(0).getClass()); } @Test public void testGetDataPermissionRule_06() { // 准备参数 String mappedStatementId = randomString(); // mock 方法 DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass06.class, DataPermission.class)); // 调用 List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); // 断言 assertSame(rules, result); } @DataPermission(enable = false) static class TestClass03 {} @DataPermission(includeRules = DataPermissionRule01.class) static class TestClass04 {} @DataPermission(excludeRules = DataPermissionRule01.class) static class TestClass05 {} @DataPermission static class TestClass06 {} static class DataPermissionRule01 implements DataPermissionRule { @Override public Set getTableNames() { return null; } @Override public Expression getExpression(String tableName, Alias tableAlias) { return null; } } static class DataPermissionRule02 implements DataPermissionRule { @Override public Set getTableNames() { return null; } @Override public Expression getExpression(String tableName, Alias tableAlias) { return null; } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/test/java/co/yixiang/yshop/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java ================================================ package co.yixiang.yshop.framework.datapermission.core.rule.dept; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ReflectUtil; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.util.collection.SetUtils; import co.yixiang.yshop.framework.security.core.LoginUser; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import co.yixiang.yshop.framework.test.core.ut.BaseMockitoUnitTest; import co.yixiang.yshop.module.system.api.permission.PermissionApi; import co.yixiang.yshop.module.system.api.permission.dto.DeptDataPermissionRespDTO; import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.Expression; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; import java.util.Map; import static co.yixiang.yshop.framework.datapermission.core.rule.dept.DeptDataPermissionRule.EXPRESSION_NULL; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomPojo; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomString; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; /** * {@link DeptDataPermissionRule} 的单元测试 * * @author yshop */ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { @InjectMocks private DeptDataPermissionRule rule; @Mock private PermissionApi permissionApi; @BeforeEach @SuppressWarnings("unchecked") public void setUp() { // 清空 rule rule.getTableNames().clear(); ((Map) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); ((Map) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); } @Test // 无 LoginUser public void testGetExpression_noLoginUser() { // 准备参数 String tableName = randomString(); Alias tableAlias = new Alias(randomString()); // mock 方法 // 调用 Expression expression = rule.getExpression(tableName, tableAlias); // 断言 assertNull(expression); } @Test // 无数据权限时 public void testGetExpression_noDeptDataPermission() { try (MockedStatic securityFrameworkUtilsMock = mockStatic(SecurityFrameworkUtils.class)) { // 准备参数 String tableName = "t_user"; Alias tableAlias = new Alias("u"); // mock 方法 LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) .setUserType(UserTypeEnum.ADMIN.getValue())); securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); // mock 方法(permissionApi 返回 null) when(permissionApi.getDeptDataPermission(eq(loginUser.getId()))).thenReturn(null); // 调用 NullPointerException exception = assertThrows(NullPointerException.class, () -> rule.getExpression(tableName, tableAlias)); // 断言 assertEquals("LoginUser(1) Table(t_user/u) 未返回数据权限", exception.getMessage()); } } @Test // 全部数据权限 public void testGetExpression_allDeptDataPermission() { try (MockedStatic securityFrameworkUtilsMock = mockStatic(SecurityFrameworkUtils.class)) { // 准备参数 String tableName = "t_user"; Alias tableAlias = new Alias("u"); // mock 方法(LoginUser) LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) .setUserType(UserTypeEnum.ADMIN.getValue())); securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); // mock 方法(DeptDataPermissionRespDTO) DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO().setAll(true); when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); // 调用 Expression expression = rule.getExpression(tableName, tableAlias); // 断言 assertNull(expression); assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); } } @Test // 即不能查看部门,又不能查看自己,则说明 100% 无权限 public void testGetExpression_noDept_noSelf() { try (MockedStatic securityFrameworkUtilsMock = mockStatic(SecurityFrameworkUtils.class)) { // 准备参数 String tableName = "t_user"; Alias tableAlias = new Alias("u"); // mock 方法(LoginUser) LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) .setUserType(UserTypeEnum.ADMIN.getValue())); securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); // mock 方法(DeptDataPermissionRespDTO) DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO(); when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); // 调用 Expression expression = rule.getExpression(tableName, tableAlias); // 断言 assertEquals("null = null", expression.toString()); assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); } } @Test // 拼接 Dept 和 User 的条件(字段都不符合) public void testGetExpression_noDeptColumn_noSelfColumn() { try (MockedStatic securityFrameworkUtilsMock = mockStatic(SecurityFrameworkUtils.class)) { // 准备参数 String tableName = "t_user"; Alias tableAlias = new Alias("u"); // mock 方法(LoginUser) LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) .setUserType(UserTypeEnum.ADMIN.getValue())); securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); // mock 方法(DeptDataPermissionRespDTO) DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() .setDeptIds(SetUtils.asSet(10L, 20L)).setSelf(true); when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); // 调用 Expression expression = rule.getExpression(tableName, tableAlias); // 断言 assertSame(EXPRESSION_NULL, expression); assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); } } @Test // 拼接 Dept 和 User 的条件(self 符合) public void testGetExpression_noDeptColumn_yesSelfColumn() { try (MockedStatic securityFrameworkUtilsMock = mockStatic(SecurityFrameworkUtils.class)) { // 准备参数 String tableName = "t_user"; Alias tableAlias = new Alias("u"); // mock 方法(LoginUser) LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) .setUserType(UserTypeEnum.ADMIN.getValue())); securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); // mock 方法(DeptDataPermissionRespDTO) DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() .setSelf(true); when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); // 添加 user 字段配置 rule.addUserColumn("t_user", "id"); // 调用 Expression expression = rule.getExpression(tableName, tableAlias); // 断言 assertEquals("u.id = 1", expression.toString()); assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); } } @Test // 拼接 Dept 和 User 的条件(dept 符合) public void testGetExpression_yesDeptColumn_noSelfColumn() { try (MockedStatic securityFrameworkUtilsMock = mockStatic(SecurityFrameworkUtils.class)) { // 准备参数 String tableName = "t_user"; Alias tableAlias = new Alias("u"); // mock 方法(LoginUser) LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) .setUserType(UserTypeEnum.ADMIN.getValue())); securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); // mock 方法(DeptDataPermissionRespDTO) DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)); when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); // 添加 dept 字段配置 rule.addDeptColumn("t_user", "dept_id"); // 调用 Expression expression = rule.getExpression(tableName, tableAlias); // 断言 assertEquals("u.dept_id IN (10, 20)", expression.toString()); assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); } } @Test // 拼接 Dept 和 User 的条件(dept + self 符合) public void testGetExpression_yesDeptColumn_yesSelfColumn() { try (MockedStatic securityFrameworkUtilsMock = mockStatic(SecurityFrameworkUtils.class)) { // 准备参数 String tableName = "t_user"; Alias tableAlias = new Alias("u"); // mock 方法(LoginUser) LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) .setUserType(UserTypeEnum.ADMIN.getValue())); securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); // mock 方法(DeptDataPermissionRespDTO) DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)).setSelf(true); when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); // 添加 user 字段配置 rule.addUserColumn("t_user", "id"); // 添加 dept 字段配置 rule.addDeptColumn("t_user", "dept_id"); // 调用 Expression expression = rule.getExpression(tableName, tableAlias); // 断言 assertEquals("(u.dept_id IN (10, 20) OR u.id = 1)", expression.toString()); assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-data-permission/src/test/java/co/yixiang/yshop/framework/datapermission/core/util/DataPermissionUtilsTest.java ================================================ package co.yixiang.yshop.framework.datapermission.core.util; import co.yixiang.yshop.framework.datapermission.core.aop.DataPermissionContextHolder; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; public class DataPermissionUtilsTest { @Test public void testExecuteIgnore() { DataPermissionUtils.executeIgnore(() -> assertFalse(DataPermissionContextHolder.get().enable())); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-ip/pom.xml ================================================ yshop-framework co.yixiang.boot ${revision} 4.0.0 yshop-spring-boot-starter-biz-ip jar ${project.artifactId} IP 拓展,支持如下功能: 1. IP 功能:查询 IP 对应的城市信息 基于 https://gitee.com/lionsoul/ip2region 实现 2. 城市功能:查询城市编码对应的城市信息 基于 https://github.com/modood/Administrative-divisions-of-China 实现 https://gitee.com/guchengwuyue/yshop-drink co.yixiang.boot yshop-common org.lionsoul ip2region org.projectlombok lombok org.slf4j slf4j-api provided co.yixiang.boot yshop-spring-boot-starter-test test ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-ip/src/main/java/co/yixiang/yshop/framework/ip/core/Area.java ================================================ package co.yixiang.yshop.framework.ip.core; import co.yixiang.yshop.framework.ip.core.enums.AreaTypeEnum; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; /** * 区域节点,包括国家、省份、城市、地区等信息 * * 数据可见 resources/area.csv 文件 * * @author yshop */ @Data @AllArgsConstructor @NoArgsConstructor public class Area { /** * 编号 - 全球,即根目录 */ public static final Integer ID_GLOBAL = 0; /** * 编号 - 中国 */ public static final Integer ID_CHINA = 1; /** * 编号 */ private Integer id; /** * 名字 */ private String name; /** * 类型 * * 枚举 {@link AreaTypeEnum} */ private Integer type; /** * 父节点 */ private Area parent; /** * 子节点 */ private List children; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-ip/src/main/java/co/yixiang/yshop/framework/ip/core/enums/AreaTypeEnum.java ================================================ package co.yixiang.yshop.framework.ip.core.enums; import co.yixiang.yshop.framework.common.core.IntArrayValuable; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.Arrays; /** * 区域类型枚举 * * @author yshop */ @AllArgsConstructor @Getter public enum AreaTypeEnum implements IntArrayValuable { COUNTRY(1, "国家"), PROVINCE(2, "省份"), CITY(3, "城市"), DISTRICT(4, "地区"), // 县、镇、区等 ; public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AreaTypeEnum::getType).toArray(); /** * 类型 */ private final Integer type; /** * 名字 */ private final String name; @Override public int[] array() { return ARRAYS; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-ip/src/main/java/co/yixiang/yshop/framework/ip/core/utils/AreaUtils.java ================================================ package co.yixiang.yshop.framework.ip.core.utils; import cn.hutool.core.io.resource.ResourceUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.text.csv.CsvRow; import cn.hutool.core.text.csv.CsvUtil; import co.yixiang.yshop.framework.common.util.object.ObjectUtils; import co.yixiang.yshop.framework.ip.core.Area; import co.yixiang.yshop.framework.ip.core.enums.AreaTypeEnum; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Function; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertList; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.findFirst; /** * 区域工具类 * * @author yshop */ @Slf4j public class AreaUtils { /** * 初始化 SEARCHER */ @SuppressWarnings("InstantiationOfUtilityClass") private final static AreaUtils INSTANCE = new AreaUtils(); /** * Area 内存缓存,提升访问速度 */ private static Map areas; private AreaUtils() { long now = System.currentTimeMillis(); areas = new HashMap<>(); areas.put(Area.ID_GLOBAL, new Area(Area.ID_GLOBAL, "全球", 0, null, new ArrayList<>())); // 从 csv 中加载数据 List rows = CsvUtil.getReader().read(ResourceUtil.getUtf8Reader("area.csv")).getRows(); rows.remove(0); // 删除 header for (CsvRow row : rows) { // 创建 Area 对象 Area area = new Area(Integer.valueOf(row.get(0)), row.get(1), Integer.valueOf(row.get(2)), null, new ArrayList<>()); // 添加到 areas 中 areas.put(area.getId(), area); } // 构建父子关系:因为 Area 中没有 parentId 字段,所以需要重复读取 for (CsvRow row : rows) { Area area = areas.get(Integer.valueOf(row.get(0))); // 自己 Area parent = areas.get(Integer.valueOf(row.get(3))); // 父 Assert.isTrue(area != parent, "{}:父子节点相同", area.getName()); area.setParent(parent); parent.getChildren().add(area); } log.info("启动加载 AreaUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now); } /** * 获得指定编号对应的区域 * * @param id 区域编号 * @return 区域 */ public static Area getArea(Integer id) { return areas.get(id); } /** * 获得指定区域对应的编号 * * @param pathStr 区域路径,例如说:河南省/石家庄市/新华区 * @return 区域 */ public static Area parseArea(String pathStr) { String[] paths = pathStr.split("/"); Area area = null; for (String path : paths) { if (area == null) { area = findFirst(areas.values(), item -> item.getName().equals(path)); } else { area = findFirst(area.getChildren(), item -> item.getName().equals(path)); } } return area; } /** * 获取所有节点的全路径名称如:河南省/石家庄市/新华区 * * @param areas 地区树 * @return 所有节点的全路径名称 */ public static List getAreaNodePathList(List areas) { List paths = new ArrayList<>(); areas.forEach(area -> getAreaNodePathList(area, "", paths)); return paths; } /** * 构建一棵树的所有节点的全路径名称,并将其存储为 "祖先/父级/子级" 的形式 * * @param node 父节点 * @param path 全路径名称 * @param paths 全路径名称列表,省份/城市/地区 */ private static void getAreaNodePathList(Area node, String path, List paths) { if (node == null) { return; } // 构建当前节点的路径 String currentPath = path.isEmpty() ? node.getName() : path + "/" + node.getName(); paths.add(currentPath); // 递归遍历子节点 for (Area child : node.getChildren()) { getAreaNodePathList(child, currentPath, paths); } } /** * 格式化区域 * * @param id 区域编号 * @return 格式化后的区域 */ public static String format(Integer id) { return format(id, " "); } /** * 格式化区域 * * 例如说: * 1. id = “静安区”时:上海 上海市 静安区 * 2. id = “上海市”时:上海 上海市 * 3. id = “上海”时:上海 * 4. id = “美国”时:美国 * 当区域在中国时,默认不显示中国 * * @param id 区域编号 * @param separator 分隔符 * @return 格式化后的区域 */ public static String format(Integer id, String separator) { // 获得区域 Area area = areas.get(id); if (area == null) { return null; } // 格式化 StringBuilder sb = new StringBuilder(); for (int i = 0; i < AreaTypeEnum.values().length; i++) { // 避免死循环 sb.insert(0, area.getName()); // “递归”父节点 area = area.getParent(); if (area == null || ObjectUtils.equalsAny(area.getId(), Area.ID_GLOBAL, Area.ID_CHINA)) { // 跳过父节点为中国的情况 break; } sb.insert(0, separator); } return sb.toString(); } /** * 获取指定类型的区域列表 * * @param type 区域类型 * @param func 转换函数 * @param 结果类型 * @return 区域列表 */ public static List getByType(AreaTypeEnum type, Function func) { return convertList(areas.values(), func, area -> type.getType().equals(area.getType())); } /** * 根据区域编号、上级区域类型,获取上级区域编号 * * @param id 区域编号 * @param type 区域类型 * @return 上级区域编号 */ public static Integer getParentIdByType(Integer id, @NonNull AreaTypeEnum type) { for (int i = 0; i < Byte.MAX_VALUE; i++) { Area area = AreaUtils.getArea(id); if (area == null) { return null; } // 情况一:匹配到,返回它 if (type.getType().equals(area.getType())) { return area.getId(); } // 情况二:找到根节点,返回空 if (area.getParent() == null || area.getParent().getId() == null) { return null; } // 其它:继续向上查找 id = area.getParent().getId(); } return null; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-ip/src/main/java/co/yixiang/yshop/framework/ip/core/utils/IPUtils.java ================================================ package co.yixiang.yshop.framework.ip.core.utils; import cn.hutool.core.io.resource.ResourceUtil; import co.yixiang.yshop.framework.ip.core.Area; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.lionsoul.ip2region.xdb.Searcher; import java.io.IOException; /** * IP 工具类 * * IP 数据源来自 ip2region.xdb 精简版,基于 项目 * * @author wanglhup */ @Slf4j public class IPUtils { /** * 初始化 SEARCHER */ @SuppressWarnings("InstantiationOfUtilityClass") private final static IPUtils INSTANCE = new IPUtils(); /** * IP 查询器,启动加载到内存中 */ private static Searcher SEARCHER; /** * 私有化构造 */ private IPUtils() { try { long now = System.currentTimeMillis(); byte[] bytes = ResourceUtil.readBytes("ip2region.xdb"); SEARCHER = Searcher.newWithBuffer(bytes); log.info("启动加载 IPUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now); } catch (IOException e) { log.error("启动加载 IPUtils 失败", e); } } /** * 查询 IP 对应的地区编号 * * @param ip IP 地址,格式为 127.0.0.1 * @return 地区id */ @SneakyThrows public static Integer getAreaId(String ip) { return Integer.parseInt(SEARCHER.search(ip.trim())); } /** * 查询 IP 对应的地区编号 * * @param ip IP 地址的时间戳,格式参考{@link Searcher#checkIP(String)} 的返回 * @return 地区编号 */ @SneakyThrows public static Integer getAreaId(long ip) { return Integer.parseInt(SEARCHER.search(ip)); } /** * 查询 IP 对应的地区 * * @param ip IP 地址,格式为 127.0.0.1 * @return 地区 */ public static Area getArea(String ip) { return AreaUtils.getArea(getAreaId(ip)); } /** * 查询 IP 对应的地区 * * @param ip IP 地址的时间戳,格式参考{@link Searcher#checkIP(String)} 的返回 * @return 地区 */ public static Area getArea(long ip) { return AreaUtils.getArea(getAreaId(ip)); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-ip/src/main/resources/area.csv ================================================ id,name,type,parentId 1,中国,1,0 2,蒙古,1,0 3,朝鲜,1,0 4,韩国,1,0 5,日本,1,0 6,菲律宾,1,0 7,越南,1,0 8,老挝,1,0 9,柬埔寨,1,0 10,缅甸,1,0 11,泰国,1,0 12,马来西亚,1,0 13,文莱,1,0 14,新加坡,1,0 15,印度尼西亚,1,0 16,东帝汶,1,0 17,尼泊尔,1,0 18,不丹,1,0 19,孟加拉国,1,0 20,印度,1,0 21,巴基斯坦,1,0 22,斯里兰卡,1,0 23,马尔代夫,1,0 24,哈萨克斯坦,1,0 25,吉尔吉斯斯坦,1,0 26,塔吉克斯坦,1,0 27,乌兹别克斯坦,1,0 28,土库曼斯坦,1,0 29,阿富汗,1,0 30,伊拉克,1,0 31,伊朗,1,0 32,叙利亚,1,0 33,约旦,1,0 34,黎巴嫩,1,0 35,以色列,1,0 36,巴勒斯坦,1,0 37,沙特阿拉伯,1,0 38,巴林,1,0 39,卡塔尔,1,0 40,科威特,1,0 41,阿拉伯联合酋长国,1,0 42,阿曼,1,0 43,也门,1,0 44,格鲁吉亚,1,0 45,亚美尼亚,1,0 46,阿塞拜疆,1,0 47,土耳其,1,0 48,塞浦路斯,1,0 49,芬兰,1,0 50,瑞典,1,0 51,挪威,1,0 52,冰岛,1,0 53,丹麦,1,0 54,爱沙尼亚,1,0 55,拉脱维亚,1,0 56,立陶宛,1,0 57,白俄罗斯,1,0 58,俄罗斯,1,0 59,乌克兰,1,0 60,摩尔多瓦,1,0 61,波兰,1,0 62,捷克,1,0 63,斯洛伐克,1,0 64,匈牙利,1,0 65,德国,1,0 66,奥地利,1,0 67,瑞士,1,0 68,列支敦士登,1,0 69,英国,1,0 70,爱尔兰,1,0 71,荷兰,1,0 72,比利时,1,0 73,卢森堡,1,0 74,法国,1,0 75,摩纳哥,1,0 76,罗马尼亚,1,0 77,保加利亚,1,0 78,塞尔维亚,1,0 79,马其顿,1,0 80,阿尔巴尼亚,1,0 81,希腊,1,0 82,斯洛文尼亚,1,0 83,克罗地亚,1,0 84,波斯尼亚和墨塞哥维那,1,0 85,意大利,1,0 86,梵蒂冈,1,0 87,圣马力诺,1,0 88,马耳他,1,0 89,西班牙,1,0 90,葡萄牙,1,0 91,安道尔共和国,1,0 92,埃及,1,0 93,利比亚,1,0 94,苏丹,1,0 95,突尼斯,1,0 96,阿尔及利亚,1,0 97,摩洛哥,1,0 98,亚速尔群岛,1,0 99,马德拉群岛,1,0 100,埃塞俄比亚,1,0 101,厄立特里亚,1,0 102,索马里,1,0 103,吉布提,1,0 104,肯尼亚,1,0 105,坦桑尼亚,1,0 106,乌干达,1,0 107,卢旺达,1,0 108,布隆迪,1,0 109,塞舌尔,1,0 110,圣多美及普林西比,1,0 111,塞内加尔,1,0 112,冈比亚,1,0 113,马里,1,0 114,布基纳法索,1,0 115,几内亚,1,0 116,几内亚比绍,1,0 117,佛得角,1,0 118,塞拉利昂,1,0 119,利比里亚,1,0 120,科特迪瓦,1,0 121,加纳,1,0 122,多哥,1,0 123,贝宁,1,0 124,尼日尔,1,0 125,加那利群岛,1,0 126,赞比亚,1,0 127,安哥拉,1,0 128,津巴布韦,1,0 129,马拉维,1,0 130,莫桑比克,1,0 131,博茨瓦纳,1,0 132,纳米比亚,1,0 133,南非,1,0 134,斯威士兰,1,0 135,莱索托,1,0 136,马达加斯加,1,0 137,科摩罗,1,0 138,毛里求斯,1,0 139,留尼旺,1,0 140,圣赫勒拿,1,0 141,澳大利亚,1,0 142,新西兰,1,0 143,巴布亚新几内亚,1,0 144,所罗门群岛,1,0 145,瓦努阿图共和国,1,0 146,密克罗尼西亚,1,0 147,马绍尔群岛,1,0 148,帕劳,1,0 149,瑙鲁,1,0 150,基里巴斯,1,0 151,图瓦卢,1,0 152,萨摩亚,1,0 153,斐济,1,0 154,汤加,1,0 155,库克群岛,1,0 156,关岛,1,0 157,新喀里多尼亚,1,0 158,法属波利尼西亚,1,0 159,皮特凯恩岛,1,0 160,瓦利斯与富图纳,1,0 161,纽埃,1,0 162,托克劳,1,0 163,美属萨摩亚,1,0 164,北马里亚纳,1,0 165,加拿大,1,0 166,美国,1,0 167,墨西哥,1,0 168,格陵兰,1,0 169,危地马拉,1,0 170,伯利兹,1,0 171,萨尔瓦多,1,0 172,洪都拉斯,1,0 173,尼加拉瓜,1,0 174,哥斯达黎加,1,0 175,巴拿马,1,0 176,巴哈马,1,0 177,古巴,1,0 178,牙买加,1,0 179,海地,1,0 180,多米尼加共和国,1,0 181,安提瓜和巴布达,1,0 182,圣基茨和尼维斯,1,0 183,多米尼克,1,0 184,圣卢西亚,1,0 185,圣文森特和格林纳丁斯,1,0 186,格林纳达,1,0 187,巴巴多斯,1,0 188,特立尼达和多巴哥,1,0 189,波多黎各,1,0 190,英属维尔京群岛,1,0 191,美属维尔京群岛,1,0 192,安圭拉,1,0 193,蒙特塞拉特岛,1,0 194,瓜德罗普,1,0 195,马提尼克,1,0 196,荷属安的列斯,1,0 197,阿鲁巴,1,0 198,特克斯和凯科斯群岛,1,0 199,开曼群岛,1,0 200,百慕大,1,0 201,哥伦比亚,1,0 202,委内瑞拉,1,0 203,圭亚那,1,0 204,法属圭亚那,1,0 205,苏里南,1,0 206,厄瓜多尔,1,0 207,秘鲁,1,0 208,玻利维亚,1,0 209,巴西,1,0 210,智利,1,0 211,阿根廷,1,0 212,乌拉圭,1,0 213,巴拉圭,1,0 214,波黑,1,0 215,直布罗陀,1,0 216,新喀里多尼亚群岛,1,0 217,瓦利斯和富图纳群岛,1,0 218,泽西岛,1,0 219,黑山,1,0 220,英属马恩岛,1,0 221,尼日利亚,1,0 222,喀麦隆,1,0 223,加蓬,1,0 224,乍得,1,0 225,刚果共和国,1,0 226,中非共和国,1,0 227,南苏丹,1,0 228,赤道几内亚,1,0 229,毛里塔尼亚,1,0 230,刚果民主共和国,1,0 231,留尼汪岛,1,0 232,格陵兰岛,1,0 233,法罗群岛,1,0 234,根西岛,1,0 235,百慕大群岛,1,0 236,圣皮埃尔和密克隆群岛,1,0 237,法属圣马丁,1,0 238,奥兰群岛,1,0 239,北马里亚纳群岛,1,0 240,库拉索,1,0 241,博内尔岛,1,0 242,圣马丁岛,1,0 243,圣巴泰勒米岛,1,0 244,福克兰群岛,1,0 245,圣多美和普林西比,1,0 246,英属印度洋领地,1,0 247,东萨摩亚,1,0 248,诺福克岛,1,0 110000,北京,2,1 120000,天津,2,1 130000,河北省,2,1 140000,山西省,2,1 150000,内蒙古自治区,2,1 210000,辽宁省,2,1 220000,吉林省,2,1 230000,黑龙江省,2,1 310000,上海,2,1 320000,江苏省,2,1 330000,浙江省,2,1 340000,安徽省,2,1 350000,福建省,2,1 360000,江西省,2,1 370000,山东省,2,1 410000,河南省,2,1 420000,湖北省,2,1 430000,湖南省,2,1 440000,广东省,2,1 450000,广西壮族自治区,2,1 460000,海南省,2,1 500000,重庆,2,1 510000,四川省,2,1 520000,贵州省,2,1 530000,云南省,2,1 540000,西藏自治区,2,1 610000,陕西省,2,1 620000,甘肃省,2,1 630000,青海省,2,1 640000,宁夏回族自治区,2,1 650000,新疆维吾尔自治区,2,1 110100,北京市,3,110000 120100,天津市,3,120000 130100,石家庄市,3,130000 130200,唐山市,3,130000 130300,秦皇岛市,3,130000 130400,邯郸市,3,130000 130500,邢台市,3,130000 130600,保定市,3,130000 130700,张家口市,3,130000 130800,承德市,3,130000 130900,沧州市,3,130000 131000,廊坊市,3,130000 131100,衡水市,3,130000 140100,太原市,3,140000 140200,大同市,3,140000 140300,阳泉市,3,140000 140400,长治市,3,140000 140500,晋城市,3,140000 140600,朔州市,3,140000 140700,晋中市,3,140000 140800,运城市,3,140000 140900,忻州市,3,140000 141000,临汾市,3,140000 141100,吕梁市,3,140000 150100,呼和浩特市,3,150000 150200,包头市,3,150000 150300,乌海市,3,150000 150400,赤峰市,3,150000 150500,通辽市,3,150000 150600,鄂尔多斯市,3,150000 150700,呼伦贝尔市,3,150000 150800,巴彦淖尔市,3,150000 150900,乌兰察布市,3,150000 152200,兴安盟,3,150000 152500,锡林郭勒盟,3,150000 152900,阿拉善盟,3,150000 210100,沈阳市,3,210000 210200,大连市,3,210000 210300,鞍山市,3,210000 210400,抚顺市,3,210000 210500,本溪市,3,210000 210600,丹东市,3,210000 210700,锦州市,3,210000 210800,营口市,3,210000 210900,阜新市,3,210000 211000,辽阳市,3,210000 211100,盘锦市,3,210000 211200,铁岭市,3,210000 211300,朝阳市,3,210000 211400,葫芦岛市,3,210000 220100,长春市,3,220000 220200,吉林市,3,220000 220300,四平市,3,220000 220400,辽源市,3,220000 220500,通化市,3,220000 220600,白山市,3,220000 220700,松原市,3,220000 220800,白城市,3,220000 222400,延边朝鲜族自治州,3,220000 230100,哈尔滨市,3,230000 230200,齐齐哈尔市,3,230000 230300,鸡西市,3,230000 230400,鹤岗市,3,230000 230500,双鸭山市,3,230000 230600,大庆市,3,230000 230700,伊春市,3,230000 230800,佳木斯市,3,230000 230900,七台河市,3,230000 231000,牡丹江市,3,230000 231100,黑河市,3,230000 231200,绥化市,3,230000 232700,大兴安岭地区,3,230000 310100,上海市,3,310000 320100,南京市,3,320000 320200,无锡市,3,320000 320300,徐州市,3,320000 320400,常州市,3,320000 320500,苏州市,3,320000 320600,南通市,3,320000 320700,连云港市,3,320000 320800,淮安市,3,320000 320900,盐城市,3,320000 321000,扬州市,3,320000 321100,镇江市,3,320000 321200,泰州市,3,320000 321300,宿迁市,3,320000 330100,杭州市,3,330000 330200,宁波市,3,330000 330300,温州市,3,330000 330400,嘉兴市,3,330000 330500,湖州市,3,330000 330600,绍兴市,3,330000 330700,金华市,3,330000 330800,衢州市,3,330000 330900,舟山市,3,330000 331000,台州市,3,330000 331100,丽水市,3,330000 340100,合肥市,3,340000 340200,芜湖市,3,340000 340300,蚌埠市,3,340000 340400,淮南市,3,340000 340500,马鞍山市,3,340000 340600,淮北市,3,340000 340700,铜陵市,3,340000 340800,安庆市,3,340000 341000,黄山市,3,340000 341100,滁州市,3,340000 341200,阜阳市,3,340000 341300,宿州市,3,340000 341500,六安市,3,340000 341600,亳州市,3,340000 341700,池州市,3,340000 341800,宣城市,3,340000 350100,福州市,3,350000 350200,厦门市,3,350000 350300,莆田市,3,350000 350400,三明市,3,350000 350500,泉州市,3,350000 350600,漳州市,3,350000 350700,南平市,3,350000 350800,龙岩市,3,350000 350900,宁德市,3,350000 360100,南昌市,3,360000 360200,景德镇市,3,360000 360300,萍乡市,3,360000 360400,九江市,3,360000 360500,新余市,3,360000 360600,鹰潭市,3,360000 360700,赣州市,3,360000 360800,吉安市,3,360000 360900,宜春市,3,360000 361000,抚州市,3,360000 361100,上饶市,3,360000 370100,济南市,3,370000 370200,青岛市,3,370000 370300,淄博市,3,370000 370400,枣庄市,3,370000 370500,东营市,3,370000 370600,烟台市,3,370000 370700,潍坊市,3,370000 370800,济宁市,3,370000 370900,泰安市,3,370000 371000,威海市,3,370000 371100,日照市,3,370000 371300,临沂市,3,370000 371400,德州市,3,370000 371500,聊城市,3,370000 371600,滨州市,3,370000 371700,菏泽市,3,370000 410100,郑州市,3,410000 410200,开封市,3,410000 410300,洛阳市,3,410000 410400,平顶山市,3,410000 410500,安阳市,3,410000 410600,鹤壁市,3,410000 410700,新乡市,3,410000 410800,焦作市,3,410000 410900,濮阳市,3,410000 411000,许昌市,3,410000 411100,漯河市,3,410000 411200,三门峡市,3,410000 411300,南阳市,3,410000 411400,商丘市,3,410000 411500,信阳市,3,410000 411600,周口市,3,410000 411700,驻马店市,3,410000 419000,省直辖县级行政区划,3,410000 420100,武汉市,3,420000 420200,黄石市,3,420000 420300,十堰市,3,420000 420500,宜昌市,3,420000 420600,襄阳市,3,420000 420700,鄂州市,3,420000 420800,荆门市,3,420000 420900,孝感市,3,420000 421000,荆州市,3,420000 421100,黄冈市,3,420000 421200,咸宁市,3,420000 421300,随州市,3,420000 422800,恩施土家族苗族自治州,3,420000 429000,省直辖县级行政区划,3,420000 430100,长沙市,3,430000 430200,株洲市,3,430000 430300,湘潭市,3,430000 430400,衡阳市,3,430000 430500,邵阳市,3,430000 430600,岳阳市,3,430000 430700,常德市,3,430000 430800,张家界市,3,430000 430900,益阳市,3,430000 431000,郴州市,3,430000 431100,永州市,3,430000 431200,怀化市,3,430000 431300,娄底市,3,430000 433100,湘西土家族苗族自治州,3,430000 440100,广州市,3,440000 440200,韶关市,3,440000 440300,深圳市,3,440000 440400,珠海市,3,440000 440500,汕头市,3,440000 440600,佛山市,3,440000 440700,江门市,3,440000 440800,湛江市,3,440000 440900,茂名市,3,440000 441200,肇庆市,3,440000 441300,惠州市,3,440000 441400,梅州市,3,440000 441500,汕尾市,3,440000 441600,河源市,3,440000 441700,阳江市,3,440000 441800,清远市,3,440000 441900,东莞市,3,440000 441901,莞城区,4,441900 441902,南城区,4,441900 441904,万江区,4,441900 441905,石碣镇,4,441900 441906,石龙镇,4,441900 441907,茶山镇,4,441900 441908,石排镇,4,441900 441909,企石镇,4,441900 441910,横沥镇,4,441900 441911,桥头镇,4,441900 441912,谢岗镇,4,441900 441913,东坑镇,4,441900 441914,常平镇,4,441900 441915,寮步镇,4,441900 441916,大朗镇,4,441900 441917,麻涌镇,4,441900 441918,中堂镇,4,441900 441919,高埗镇,4,441900 441920,樟木头镇,4,441900 441921,大岭山镇,4,441900 441922,望牛墩镇,4,441900 441923,黄江镇,4,441900 441924,洪梅镇,4,441900 441925,清溪镇,4,441900 441926,沙田镇,4,441900 441927,道滘镇,4,441900 441928,塘厦镇,4,441900 441929,虎门镇,4,441900 441930,厚街镇,4,441900 441931,凤岗镇,4,441900 441932,长安镇,4,441900 442000,中山市,3,440000 442001,石岐街道,4,442000 442002,东区街道,4,442000 442003,中山港街道,4,442000 442004,西区街道,4,442000 442005,南区街道,4,442000 442006,五桂山街道,4,442000 442007,民众街道,4,442000 442008,南朗街道,4,442000 442009,黄圃镇,4,442000 442010,东凤镇,4,442000 442011,古镇镇,4,442000 442012,沙溪镇,4,442000 442013,坦洲镇,4,442000 442014,港口镇,4,442000 442015,三角镇,4,442000 442016,横栏镇,4,442000 442017,南头镇,4,442000 442018,阜沙镇,4,442000 442019,三乡镇,4,442000 442020,板芙镇,4,442000 442021,大涌镇,4,442000 442022,神湾镇,4,442000 442023,小榄镇,4,442000 445100,潮州市,3,440000 445200,揭阳市,3,440000 445300,云浮市,3,440000 450100,南宁市,3,450000 450200,柳州市,3,450000 450300,桂林市,3,450000 450400,梧州市,3,450000 450500,北海市,3,450000 450600,防城港市,3,450000 450700,钦州市,3,450000 450800,贵港市,3,450000 450900,玉林市,3,450000 451000,百色市,3,450000 451100,贺州市,3,450000 451200,河池市,3,450000 451300,来宾市,3,450000 451400,崇左市,3,450000 460100,海口市,3,460000 460200,三亚市,3,460000 460300,三沙市,3,460000 460400,儋州市,3,460000 469000,省直辖县级行政区划,3,460000 500100,重庆市,3,500000 510100,成都市,3,510000 510300,自贡市,3,510000 510400,攀枝花市,3,510000 510500,泸州市,3,510000 510600,德阳市,3,510000 510700,绵阳市,3,510000 510800,广元市,3,510000 510900,遂宁市,3,510000 511000,内江市,3,510000 511100,乐山市,3,510000 511300,南充市,3,510000 511400,眉山市,3,510000 511500,宜宾市,3,510000 511600,广安市,3,510000 511700,达州市,3,510000 511800,雅安市,3,510000 511900,巴中市,3,510000 512000,资阳市,3,510000 513200,阿坝藏族羌族自治州,3,510000 513300,甘孜藏族自治州,3,510000 513400,凉山彝族自治州,3,510000 520100,贵阳市,3,520000 520200,六盘水市,3,520000 520300,遵义市,3,520000 520400,安顺市,3,520000 520500,毕节市,3,520000 520600,铜仁市,3,520000 522300,黔西南布依族苗族自治州,3,520000 522600,黔东南苗族侗族自治州,3,520000 522700,黔南布依族苗族自治州,3,520000 530100,昆明市,3,530000 530300,曲靖市,3,530000 530400,玉溪市,3,530000 530500,保山市,3,530000 530600,昭通市,3,530000 530700,丽江市,3,530000 530800,普洱市,3,530000 530900,临沧市,3,530000 532300,楚雄彝族自治州,3,530000 532500,红河哈尼族彝族自治州,3,530000 532600,文山壮族苗族自治州,3,530000 532800,西双版纳傣族自治州,3,530000 532900,大理白族自治州,3,530000 533100,德宏傣族景颇族自治州,3,530000 533300,怒江傈僳族自治州,3,530000 533400,迪庆藏族自治州,3,530000 540100,拉萨市,3,540000 540200,日喀则市,3,540000 540300,昌都市,3,540000 540400,林芝市,3,540000 540500,山南市,3,540000 540600,那曲市,3,540000 542500,阿里地区,3,540000 610100,西安市,3,610000 610200,铜川市,3,610000 610300,宝鸡市,3,610000 610400,咸阳市,3,610000 610500,渭南市,3,610000 610600,延安市,3,610000 610700,汉中市,3,610000 610800,榆林市,3,610000 610900,安康市,3,610000 611000,商洛市,3,610000 620100,兰州市,3,620000 620200,嘉峪关市,3,620000 620300,金昌市,3,620000 620400,白银市,3,620000 620500,天水市,3,620000 620600,武威市,3,620000 620700,张掖市,3,620000 620800,平凉市,3,620000 620900,酒泉市,3,620000 621000,庆阳市,3,620000 621100,定西市,3,620000 621200,陇南市,3,620000 622900,临夏回族自治州,3,620000 623000,甘南藏族自治州,3,620000 630100,西宁市,3,630000 630200,海东市,3,630000 632200,海北藏族自治州,3,630000 632300,黄南藏族自治州,3,630000 632500,海南藏族自治州,3,630000 632600,果洛藏族自治州,3,630000 632700,玉树藏族自治州,3,630000 632800,海西蒙古族藏族自治州,3,630000 640100,银川市,3,640000 640200,石嘴山市,3,640000 640300,吴忠市,3,640000 640400,固原市,3,640000 640500,中卫市,3,640000 650100,乌鲁木齐市,3,650000 650200,克拉玛依市,3,650000 650400,吐鲁番市,3,650000 650500,哈密市,3,650000 652300,昌吉回族自治州,3,650000 652700,博尔塔拉蒙古自治州,3,650000 652800,巴音郭楞蒙古自治州,3,650000 652900,阿克苏地区,3,650000 653000,克孜勒苏柯尔克孜自治州,3,650000 653100,喀什地区,3,650000 653200,和田地区,3,650000 654000,伊犁哈萨克自治州,3,650000 654200,塔城地区,3,650000 654300,阿勒泰地区,3,650000 659000,自治区直辖县级行政区划,3,650000 110101,东城区,4,110100 110102,西城区,4,110100 110105,朝阳区,4,110100 110106,丰台区,4,110100 110107,石景山区,4,110100 110108,海淀区,4,110100 110109,门头沟区,4,110100 110111,房山区,4,110100 110112,通州区,4,110100 110113,顺义区,4,110100 110114,昌平区,4,110100 110115,大兴区,4,110100 110116,怀柔区,4,110100 110117,平谷区,4,110100 110118,密云区,4,110100 110119,延庆区,4,110100 120101,和平区,4,120100 120102,河东区,4,120100 120103,河西区,4,120100 120104,南开区,4,120100 120105,河北区,4,120100 120106,红桥区,4,120100 120110,东丽区,4,120100 120111,西青区,4,120100 120112,津南区,4,120100 120113,北辰区,4,120100 120114,武清区,4,120100 120115,宝坻区,4,120100 120116,滨海新区,4,120100 120117,宁河区,4,120100 120118,静海区,4,120100 120119,蓟州区,4,120100 130102,长安区,4,130100 130104,桥西区,4,130100 130105,新华区,4,130100 130107,井陉矿区,4,130100 130108,裕华区,4,130100 130109,藁城区,4,130100 130110,鹿泉区,4,130100 130111,栾城区,4,130100 130121,井陉县,4,130100 130123,正定县,4,130100 130125,行唐县,4,130100 130126,灵寿县,4,130100 130127,高邑县,4,130100 130128,深泽县,4,130100 130129,赞皇县,4,130100 130130,无极县,4,130100 130131,平山县,4,130100 130132,元氏县,4,130100 130133,赵县,4,130100 130171,石家庄高新技术产业开发区,4,130100 130172,石家庄循环化工园区,4,130100 130181,辛集市,4,130100 130183,晋州市,4,130100 130184,新乐市,4,130100 130202,路南区,4,130200 130203,路北区,4,130200 130204,古冶区,4,130200 130205,开平区,4,130200 130207,丰南区,4,130200 130208,丰润区,4,130200 130209,曹妃甸区,4,130200 130224,滦南县,4,130200 130225,乐亭县,4,130200 130227,迁西县,4,130200 130229,玉田县,4,130200 130271,河北唐山芦台经济开发区,4,130200 130272,唐山市汉沽管理区,4,130200 130273,唐山高新技术产业开发区,4,130200 130274,河北唐山海港经济开发区,4,130200 130281,遵化市,4,130200 130283,迁安市,4,130200 130284,滦州市,4,130200 130302,海港区,4,130300 130303,山海关区,4,130300 130304,北戴河区,4,130300 130306,抚宁区,4,130300 130321,青龙满族自治县,4,130300 130322,昌黎县,4,130300 130324,卢龙县,4,130300 130371,秦皇岛市经济技术开发区,4,130300 130372,北戴河新区,4,130300 130402,邯山区,4,130400 130403,丛台区,4,130400 130404,复兴区,4,130400 130406,峰峰矿区,4,130400 130407,肥乡区,4,130400 130408,永年区,4,130400 130423,临漳县,4,130400 130424,成安县,4,130400 130425,大名县,4,130400 130426,涉县,4,130400 130427,磁县,4,130400 130430,邱县,4,130400 130431,鸡泽县,4,130400 130432,广平县,4,130400 130433,馆陶县,4,130400 130434,魏县,4,130400 130435,曲周县,4,130400 130471,邯郸经济技术开发区,4,130400 130473,邯郸冀南新区,4,130400 130481,武安市,4,130400 130502,襄都区,4,130500 130503,信都区,4,130500 130505,任泽区,4,130500 130506,南和区,4,130500 130522,临城县,4,130500 130523,内丘县,4,130500 130524,柏乡县,4,130500 130525,隆尧县,4,130500 130528,宁晋县,4,130500 130529,巨鹿县,4,130500 130530,新河县,4,130500 130531,广宗县,4,130500 130532,平乡县,4,130500 130533,威县,4,130500 130534,清河县,4,130500 130535,临西县,4,130500 130571,河北邢台经济开发区,4,130500 130581,南宫市,4,130500 130582,沙河市,4,130500 130602,竞秀区,4,130600 130606,莲池区,4,130600 130607,满城区,4,130600 130608,清苑区,4,130600 130609,徐水区,4,130600 130623,涞水县,4,130600 130624,阜平县,4,130600 130626,定兴县,4,130600 130627,唐县,4,130600 130628,高阳县,4,130600 130629,容城县,4,130600 130630,涞源县,4,130600 130631,望都县,4,130600 130632,安新县,4,130600 130633,易县,4,130600 130634,曲阳县,4,130600 130635,蠡县,4,130600 130636,顺平县,4,130600 130637,博野县,4,130600 130638,雄县,4,130600 130671,保定高新技术产业开发区,4,130600 130672,保定白沟新城,4,130600 130681,涿州市,4,130600 130682,定州市,4,130600 130683,安国市,4,130600 130684,高碑店市,4,130600 130702,桥东区,4,130700 130703,桥西区,4,130700 130705,宣化区,4,130700 130706,下花园区,4,130700 130708,万全区,4,130700 130709,崇礼区,4,130700 130722,张北县,4,130700 130723,康保县,4,130700 130724,沽源县,4,130700 130725,尚义县,4,130700 130726,蔚县,4,130700 130727,阳原县,4,130700 130728,怀安县,4,130700 130730,怀来县,4,130700 130731,涿鹿县,4,130700 130732,赤城县,4,130700 130771,张家口经济开发区,4,130700 130772,张家口市察北管理区,4,130700 130773,张家口市塞北管理区,4,130700 130802,双桥区,4,130800 130803,双滦区,4,130800 130804,鹰手营子矿区,4,130800 130821,承德县,4,130800 130822,兴隆县,4,130800 130824,滦平县,4,130800 130825,隆化县,4,130800 130826,丰宁满族自治县,4,130800 130827,宽城满族自治县,4,130800 130828,围场满族蒙古族自治县,4,130800 130871,承德高新技术产业开发区,4,130800 130881,平泉市,4,130800 130902,新华区,4,130900 130903,运河区,4,130900 130921,沧县,4,130900 130922,青县,4,130900 130923,东光县,4,130900 130924,海兴县,4,130900 130925,盐山县,4,130900 130926,肃宁县,4,130900 130927,南皮县,4,130900 130928,吴桥县,4,130900 130929,献县,4,130900 130930,孟村回族自治县,4,130900 130971,河北沧州经济开发区,4,130900 130972,沧州高新技术产业开发区,4,130900 130973,沧州渤海新区,4,130900 130981,泊头市,4,130900 130982,任丘市,4,130900 130983,黄骅市,4,130900 130984,河间市,4,130900 131002,安次区,4,131000 131003,广阳区,4,131000 131022,固安县,4,131000 131023,永清县,4,131000 131024,香河县,4,131000 131025,大城县,4,131000 131026,文安县,4,131000 131028,大厂回族自治县,4,131000 131071,廊坊经济技术开发区,4,131000 131081,霸州市,4,131000 131082,三河市,4,131000 131102,桃城区,4,131100 131103,冀州区,4,131100 131121,枣强县,4,131100 131122,武邑县,4,131100 131123,武强县,4,131100 131124,饶阳县,4,131100 131125,安平县,4,131100 131126,故城县,4,131100 131127,景县,4,131100 131128,阜城县,4,131100 131171,河北衡水高新技术产业开发区,4,131100 131172,衡水滨湖新区,4,131100 131182,深州市,4,131100 140105,小店区,4,140100 140106,迎泽区,4,140100 140107,杏花岭区,4,140100 140108,尖草坪区,4,140100 140109,万柏林区,4,140100 140110,晋源区,4,140100 140121,清徐县,4,140100 140122,阳曲县,4,140100 140123,娄烦县,4,140100 140171,山西转型综合改革示范区,4,140100 140181,古交市,4,140100 140212,新荣区,4,140200 140213,平城区,4,140200 140214,云冈区,4,140200 140215,云州区,4,140200 140221,阳高县,4,140200 140222,天镇县,4,140200 140223,广灵县,4,140200 140224,灵丘县,4,140200 140225,浑源县,4,140200 140226,左云县,4,140200 140271,山西大同经济开发区,4,140200 140302,城区,4,140300 140303,矿区,4,140300 140311,郊区,4,140300 140321,平定县,4,140300 140322,盂县,4,140300 140403,潞州区,4,140400 140404,上党区,4,140400 140405,屯留区,4,140400 140406,潞城区,4,140400 140423,襄垣县,4,140400 140425,平顺县,4,140400 140426,黎城县,4,140400 140427,壶关县,4,140400 140428,长子县,4,140400 140429,武乡县,4,140400 140430,沁县,4,140400 140431,沁源县,4,140400 140471,山西长治高新技术产业园区,4,140400 140502,城区,4,140500 140521,沁水县,4,140500 140522,阳城县,4,140500 140524,陵川县,4,140500 140525,泽州县,4,140500 140581,高平市,4,140500 140602,朔城区,4,140600 140603,平鲁区,4,140600 140621,山阴县,4,140600 140622,应县,4,140600 140623,右玉县,4,140600 140671,山西朔州经济开发区,4,140600 140681,怀仁市,4,140600 140702,榆次区,4,140700 140703,太谷区,4,140700 140721,榆社县,4,140700 140722,左权县,4,140700 140723,和顺县,4,140700 140724,昔阳县,4,140700 140725,寿阳县,4,140700 140727,祁县,4,140700 140728,平遥县,4,140700 140729,灵石县,4,140700 140781,介休市,4,140700 140802,盐湖区,4,140800 140821,临猗县,4,140800 140822,万荣县,4,140800 140823,闻喜县,4,140800 140824,稷山县,4,140800 140825,新绛县,4,140800 140826,绛县,4,140800 140827,垣曲县,4,140800 140828,夏县,4,140800 140829,平陆县,4,140800 140830,芮城县,4,140800 140881,永济市,4,140800 140882,河津市,4,140800 140902,忻府区,4,140900 140921,定襄县,4,140900 140922,五台县,4,140900 140923,代县,4,140900 140924,繁峙县,4,140900 140925,宁武县,4,140900 140926,静乐县,4,140900 140927,神池县,4,140900 140928,五寨县,4,140900 140929,岢岚县,4,140900 140930,河曲县,4,140900 140931,保德县,4,140900 140932,偏关县,4,140900 140971,五台山风景名胜区,4,140900 140981,原平市,4,140900 141002,尧都区,4,141000 141021,曲沃县,4,141000 141022,翼城县,4,141000 141023,襄汾县,4,141000 141024,洪洞县,4,141000 141025,古县,4,141000 141026,安泽县,4,141000 141027,浮山县,4,141000 141028,吉县,4,141000 141029,乡宁县,4,141000 141030,大宁县,4,141000 141031,隰县,4,141000 141032,永和县,4,141000 141033,蒲县,4,141000 141034,汾西县,4,141000 141081,侯马市,4,141000 141082,霍州市,4,141000 141102,离石区,4,141100 141121,文水县,4,141100 141122,交城县,4,141100 141123,兴县,4,141100 141124,临县,4,141100 141125,柳林县,4,141100 141126,石楼县,4,141100 141127,岚县,4,141100 141128,方山县,4,141100 141129,中阳县,4,141100 141130,交口县,4,141100 141181,孝义市,4,141100 141182,汾阳市,4,141100 150102,新城区,4,150100 150103,回民区,4,150100 150104,玉泉区,4,150100 150105,赛罕区,4,150100 150121,土默特左旗,4,150100 150122,托克托县,4,150100 150123,和林格尔县,4,150100 150124,清水河县,4,150100 150125,武川县,4,150100 150172,呼和浩特经济技术开发区,4,150100 150202,东河区,4,150200 150203,昆都仑区,4,150200 150204,青山区,4,150200 150205,石拐区,4,150200 150206,白云鄂博矿区,4,150200 150207,九原区,4,150200 150221,土默特右旗,4,150200 150222,固阳县,4,150200 150223,达尔罕茂明安联合旗,4,150200 150271,包头稀土高新技术产业开发区,4,150200 150302,海勃湾区,4,150300 150303,海南区,4,150300 150304,乌达区,4,150300 150402,红山区,4,150400 150403,元宝山区,4,150400 150404,松山区,4,150400 150421,阿鲁科尔沁旗,4,150400 150422,巴林左旗,4,150400 150423,巴林右旗,4,150400 150424,林西县,4,150400 150425,克什克腾旗,4,150400 150426,翁牛特旗,4,150400 150428,喀喇沁旗,4,150400 150429,宁城县,4,150400 150430,敖汉旗,4,150400 150502,科尔沁区,4,150500 150521,科尔沁左翼中旗,4,150500 150522,科尔沁左翼后旗,4,150500 150523,开鲁县,4,150500 150524,库伦旗,4,150500 150525,奈曼旗,4,150500 150526,扎鲁特旗,4,150500 150571,通辽经济技术开发区,4,150500 150581,霍林郭勒市,4,150500 150602,东胜区,4,150600 150603,康巴什区,4,150600 150621,达拉特旗,4,150600 150622,准格尔旗,4,150600 150623,鄂托克前旗,4,150600 150624,鄂托克旗,4,150600 150625,杭锦旗,4,150600 150626,乌审旗,4,150600 150627,伊金霍洛旗,4,150600 150702,海拉尔区,4,150700 150703,扎赉诺尔区,4,150700 150721,阿荣旗,4,150700 150722,莫力达瓦达斡尔族自治旗,4,150700 150723,鄂伦春自治旗,4,150700 150724,鄂温克族自治旗,4,150700 150725,陈巴尔虎旗,4,150700 150726,新巴尔虎左旗,4,150700 150727,新巴尔虎右旗,4,150700 150781,满洲里市,4,150700 150782,牙克石市,4,150700 150783,扎兰屯市,4,150700 150784,额尔古纳市,4,150700 150785,根河市,4,150700 150802,临河区,4,150800 150821,五原县,4,150800 150822,磴口县,4,150800 150823,乌拉特前旗,4,150800 150824,乌拉特中旗,4,150800 150825,乌拉特后旗,4,150800 150826,杭锦后旗,4,150800 150902,集宁区,4,150900 150921,卓资县,4,150900 150922,化德县,4,150900 150923,商都县,4,150900 150924,兴和县,4,150900 150925,凉城县,4,150900 150926,察哈尔右翼前旗,4,150900 150927,察哈尔右翼中旗,4,150900 150928,察哈尔右翼后旗,4,150900 150929,四子王旗,4,150900 150981,丰镇市,4,150900 152201,乌兰浩特市,4,152200 152202,阿尔山市,4,152200 152221,科尔沁右翼前旗,4,152200 152222,科尔沁右翼中旗,4,152200 152223,扎赉特旗,4,152200 152224,突泉县,4,152200 152501,二连浩特市,4,152500 152502,锡林浩特市,4,152500 152522,阿巴嘎旗,4,152500 152523,苏尼特左旗,4,152500 152524,苏尼特右旗,4,152500 152525,东乌珠穆沁旗,4,152500 152526,西乌珠穆沁旗,4,152500 152527,太仆寺旗,4,152500 152528,镶黄旗,4,152500 152529,正镶白旗,4,152500 152530,正蓝旗,4,152500 152531,多伦县,4,152500 152571,乌拉盖管委会,4,152500 152921,阿拉善左旗,4,152900 152922,阿拉善右旗,4,152900 152923,额济纳旗,4,152900 152971,内蒙古阿拉善高新技术产业开发区,4,152900 210102,和平区,4,210100 210103,沈河区,4,210100 210104,大东区,4,210100 210105,皇姑区,4,210100 210106,铁西区,4,210100 210111,苏家屯区,4,210100 210112,浑南区,4,210100 210113,沈北新区,4,210100 210114,于洪区,4,210100 210115,辽中区,4,210100 210123,康平县,4,210100 210124,法库县,4,210100 210181,新民市,4,210100 210202,中山区,4,210200 210203,西岗区,4,210200 210204,沙河口区,4,210200 210211,甘井子区,4,210200 210212,旅顺口区,4,210200 210213,金州区,4,210200 210214,普兰店区,4,210200 210224,长海县,4,210200 210281,瓦房店市,4,210200 210283,庄河市,4,210200 210302,铁东区,4,210300 210303,铁西区,4,210300 210304,立山区,4,210300 210311,千山区,4,210300 210321,台安县,4,210300 210323,岫岩满族自治县,4,210300 210381,海城市,4,210300 210402,新抚区,4,210400 210403,东洲区,4,210400 210404,望花区,4,210400 210411,顺城区,4,210400 210421,抚顺县,4,210400 210422,新宾满族自治县,4,210400 210423,清原满族自治县,4,210400 210502,平山区,4,210500 210503,溪湖区,4,210500 210504,明山区,4,210500 210505,南芬区,4,210500 210521,本溪满族自治县,4,210500 210522,桓仁满族自治县,4,210500 210602,元宝区,4,210600 210603,振兴区,4,210600 210604,振安区,4,210600 210624,宽甸满族自治县,4,210600 210681,东港市,4,210600 210682,凤城市,4,210600 210702,古塔区,4,210700 210703,凌河区,4,210700 210711,太和区,4,210700 210726,黑山县,4,210700 210727,义县,4,210700 210781,凌海市,4,210700 210782,北镇市,4,210700 210802,站前区,4,210800 210803,西市区,4,210800 210804,鲅鱼圈区,4,210800 210811,老边区,4,210800 210881,盖州市,4,210800 210882,大石桥市,4,210800 210902,海州区,4,210900 210903,新邱区,4,210900 210904,太平区,4,210900 210905,清河门区,4,210900 210911,细河区,4,210900 210921,阜新蒙古族自治县,4,210900 210922,彰武县,4,210900 211002,白塔区,4,211000 211003,文圣区,4,211000 211004,宏伟区,4,211000 211005,弓长岭区,4,211000 211011,太子河区,4,211000 211021,辽阳县,4,211000 211081,灯塔市,4,211000 211102,双台子区,4,211100 211103,兴隆台区,4,211100 211104,大洼区,4,211100 211122,盘山县,4,211100 211202,银州区,4,211200 211204,清河区,4,211200 211221,铁岭县,4,211200 211223,西丰县,4,211200 211224,昌图县,4,211200 211281,调兵山市,4,211200 211282,开原市,4,211200 211302,双塔区,4,211300 211303,龙城区,4,211300 211321,朝阳县,4,211300 211322,建平县,4,211300 211324,喀喇沁左翼蒙古族自治县,4,211300 211381,北票市,4,211300 211382,凌源市,4,211300 211402,连山区,4,211400 211403,龙港区,4,211400 211404,南票区,4,211400 211421,绥中县,4,211400 211422,建昌县,4,211400 211481,兴城市,4,211400 220102,南关区,4,220100 220103,宽城区,4,220100 220104,朝阳区,4,220100 220105,二道区,4,220100 220106,绿园区,4,220100 220112,双阳区,4,220100 220113,九台区,4,220100 220122,农安县,4,220100 220171,长春经济技术开发区,4,220100 220172,长春净月高新技术产业开发区,4,220100 220173,长春高新技术产业开发区,4,220100 220174,长春汽车经济技术开发区,4,220100 220182,榆树市,4,220100 220183,德惠市,4,220100 220184,公主岭市,4,220100 220202,昌邑区,4,220200 220203,龙潭区,4,220200 220204,船营区,4,220200 220211,丰满区,4,220200 220221,永吉县,4,220200 220271,吉林经济开发区,4,220200 220272,吉林高新技术产业开发区,4,220200 220273,吉林中国新加坡食品区,4,220200 220281,蛟河市,4,220200 220282,桦甸市,4,220200 220283,舒兰市,4,220200 220284,磐石市,4,220200 220302,铁西区,4,220300 220303,铁东区,4,220300 220322,梨树县,4,220300 220323,伊通满族自治县,4,220300 220382,双辽市,4,220300 220402,龙山区,4,220400 220403,西安区,4,220400 220421,东丰县,4,220400 220422,东辽县,4,220400 220502,东昌区,4,220500 220503,二道江区,4,220500 220521,通化县,4,220500 220523,辉南县,4,220500 220524,柳河县,4,220500 220581,梅河口市,4,220500 220582,集安市,4,220500 220602,浑江区,4,220600 220605,江源区,4,220600 220621,抚松县,4,220600 220622,靖宇县,4,220600 220623,长白朝鲜族自治县,4,220600 220681,临江市,4,220600 220702,宁江区,4,220700 220721,前郭尔罗斯蒙古族自治县,4,220700 220722,长岭县,4,220700 220723,乾安县,4,220700 220771,吉林松原经济开发区,4,220700 220781,扶余市,4,220700 220802,洮北区,4,220800 220821,镇赉县,4,220800 220822,通榆县,4,220800 220871,吉林白城经济开发区,4,220800 220881,洮南市,4,220800 220882,大安市,4,220800 222401,延吉市,4,222400 222402,图们市,4,222400 222403,敦化市,4,222400 222404,珲春市,4,222400 222405,龙井市,4,222400 222406,和龙市,4,222400 222424,汪清县,4,222400 222426,安图县,4,222400 230102,道里区,4,230100 230103,南岗区,4,230100 230104,道外区,4,230100 230108,平房区,4,230100 230109,松北区,4,230100 230110,香坊区,4,230100 230111,呼兰区,4,230100 230112,阿城区,4,230100 230113,双城区,4,230100 230123,依兰县,4,230100 230124,方正县,4,230100 230125,宾县,4,230100 230126,巴彦县,4,230100 230127,木兰县,4,230100 230128,通河县,4,230100 230129,延寿县,4,230100 230183,尚志市,4,230100 230184,五常市,4,230100 230202,龙沙区,4,230200 230203,建华区,4,230200 230204,铁锋区,4,230200 230205,昂昂溪区,4,230200 230206,富拉尔基区,4,230200 230207,碾子山区,4,230200 230208,梅里斯达斡尔族区,4,230200 230221,龙江县,4,230200 230223,依安县,4,230200 230224,泰来县,4,230200 230225,甘南县,4,230200 230227,富裕县,4,230200 230229,克山县,4,230200 230230,克东县,4,230200 230231,拜泉县,4,230200 230281,讷河市,4,230200 230302,鸡冠区,4,230300 230303,恒山区,4,230300 230304,滴道区,4,230300 230305,梨树区,4,230300 230306,城子河区,4,230300 230307,麻山区,4,230300 230321,鸡东县,4,230300 230381,虎林市,4,230300 230382,密山市,4,230300 230402,向阳区,4,230400 230403,工农区,4,230400 230404,南山区,4,230400 230405,兴安区,4,230400 230406,东山区,4,230400 230407,兴山区,4,230400 230421,萝北县,4,230400 230422,绥滨县,4,230400 230502,尖山区,4,230500 230503,岭东区,4,230500 230505,四方台区,4,230500 230506,宝山区,4,230500 230521,集贤县,4,230500 230522,友谊县,4,230500 230523,宝清县,4,230500 230524,饶河县,4,230500 230602,萨尔图区,4,230600 230603,龙凤区,4,230600 230604,让胡路区,4,230600 230605,红岗区,4,230600 230606,大同区,4,230600 230621,肇州县,4,230600 230622,肇源县,4,230600 230623,林甸县,4,230600 230624,杜尔伯特蒙古族自治县,4,230600 230671,大庆高新技术产业开发区,4,230600 230717,伊美区,4,230700 230718,乌翠区,4,230700 230719,友好区,4,230700 230722,嘉荫县,4,230700 230723,汤旺县,4,230700 230724,丰林县,4,230700 230725,大箐山县,4,230700 230726,南岔县,4,230700 230751,金林区,4,230700 230781,铁力市,4,230700 230803,向阳区,4,230800 230804,前进区,4,230800 230805,东风区,4,230800 230811,郊区,4,230800 230822,桦南县,4,230800 230826,桦川县,4,230800 230828,汤原县,4,230800 230881,同江市,4,230800 230882,富锦市,4,230800 230883,抚远市,4,230800 230902,新兴区,4,230900 230903,桃山区,4,230900 230904,茄子河区,4,230900 230921,勃利县,4,230900 231002,东安区,4,231000 231003,阳明区,4,231000 231004,爱民区,4,231000 231005,西安区,4,231000 231025,林口县,4,231000 231071,牡丹江经济技术开发区,4,231000 231081,绥芬河市,4,231000 231083,海林市,4,231000 231084,宁安市,4,231000 231085,穆棱市,4,231000 231086,东宁市,4,231000 231102,爱辉区,4,231100 231123,逊克县,4,231100 231124,孙吴县,4,231100 231181,北安市,4,231100 231182,五大连池市,4,231100 231183,嫩江市,4,231100 231202,北林区,4,231200 231221,望奎县,4,231200 231222,兰西县,4,231200 231223,青冈县,4,231200 231224,庆安县,4,231200 231225,明水县,4,231200 231226,绥棱县,4,231200 231281,安达市,4,231200 231282,肇东市,4,231200 231283,海伦市,4,231200 232701,漠河市,4,232700 232721,呼玛县,4,232700 232722,塔河县,4,232700 232761,加格达奇区,4,232700 232762,松岭区,4,232700 232763,新林区,4,232700 232764,呼中区,4,232700 310101,黄浦区,4,310100 310104,徐汇区,4,310100 310105,长宁区,4,310100 310106,静安区,4,310100 310107,普陀区,4,310100 310109,虹口区,4,310100 310110,杨浦区,4,310100 310112,闵行区,4,310100 310113,宝山区,4,310100 310114,嘉定区,4,310100 310115,浦东新区,4,310100 310116,金山区,4,310100 310117,松江区,4,310100 310118,青浦区,4,310100 310120,奉贤区,4,310100 310151,崇明区,4,310100 320102,玄武区,4,320100 320104,秦淮区,4,320100 320105,建邺区,4,320100 320106,鼓楼区,4,320100 320111,浦口区,4,320100 320113,栖霞区,4,320100 320114,雨花台区,4,320100 320115,江宁区,4,320100 320116,六合区,4,320100 320117,溧水区,4,320100 320118,高淳区,4,320100 320205,锡山区,4,320200 320206,惠山区,4,320200 320211,滨湖区,4,320200 320213,梁溪区,4,320200 320214,新吴区,4,320200 320281,江阴市,4,320200 320282,宜兴市,4,320200 320302,鼓楼区,4,320300 320303,云龙区,4,320300 320305,贾汪区,4,320300 320311,泉山区,4,320300 320312,铜山区,4,320300 320321,丰县,4,320300 320322,沛县,4,320300 320324,睢宁县,4,320300 320371,徐州经济技术开发区,4,320300 320381,新沂市,4,320300 320382,邳州市,4,320300 320402,天宁区,4,320400 320404,钟楼区,4,320400 320411,新北区,4,320400 320412,武进区,4,320400 320413,金坛区,4,320400 320481,溧阳市,4,320400 320505,虎丘区,4,320500 320506,吴中区,4,320500 320507,相城区,4,320500 320508,姑苏区,4,320500 320509,吴江区,4,320500 320571,苏州工业园区,4,320500 320581,常熟市,4,320500 320582,张家港市,4,320500 320583,昆山市,4,320500 320585,太仓市,4,320500 320612,通州区,4,320600 320613,崇川区,4,320600 320614,海门区,4,320600 320623,如东县,4,320600 320671,南通经济技术开发区,4,320600 320681,启东市,4,320600 320682,如皋市,4,320600 320685,海安市,4,320600 320703,连云区,4,320700 320706,海州区,4,320700 320707,赣榆区,4,320700 320722,东海县,4,320700 320723,灌云县,4,320700 320724,灌南县,4,320700 320771,连云港经济技术开发区,4,320700 320772,连云港高新技术产业开发区,4,320700 320803,淮安区,4,320800 320804,淮阴区,4,320800 320812,清江浦区,4,320800 320813,洪泽区,4,320800 320826,涟水县,4,320800 320830,盱眙县,4,320800 320831,金湖县,4,320800 320871,淮安经济技术开发区,4,320800 320902,亭湖区,4,320900 320903,盐都区,4,320900 320904,大丰区,4,320900 320921,响水县,4,320900 320922,滨海县,4,320900 320923,阜宁县,4,320900 320924,射阳县,4,320900 320925,建湖县,4,320900 320971,盐城经济技术开发区,4,320900 320981,东台市,4,320900 321002,广陵区,4,321000 321003,邗江区,4,321000 321012,江都区,4,321000 321023,宝应县,4,321000 321071,扬州经济技术开发区,4,321000 321081,仪征市,4,321000 321084,高邮市,4,321000 321102,京口区,4,321100 321111,润州区,4,321100 321112,丹徒区,4,321100 321171,镇江新区,4,321100 321181,丹阳市,4,321100 321182,扬中市,4,321100 321183,句容市,4,321100 321202,海陵区,4,321200 321203,高港区,4,321200 321204,姜堰区,4,321200 321271,泰州医药高新技术产业开发区,4,321200 321281,兴化市,4,321200 321282,靖江市,4,321200 321283,泰兴市,4,321200 321302,宿城区,4,321300 321311,宿豫区,4,321300 321322,沭阳县,4,321300 321323,泗阳县,4,321300 321324,泗洪县,4,321300 321371,宿迁经济技术开发区,4,321300 330102,上城区,4,330100 330105,拱墅区,4,330100 330106,西湖区,4,330100 330108,滨江区,4,330100 330109,萧山区,4,330100 330110,余杭区,4,330100 330111,富阳区,4,330100 330112,临安区,4,330100 330113,临平区,4,330100 330114,钱塘区,4,330100 330122,桐庐县,4,330100 330127,淳安县,4,330100 330182,建德市,4,330100 330203,海曙区,4,330200 330205,江北区,4,330200 330206,北仑区,4,330200 330211,镇海区,4,330200 330212,鄞州区,4,330200 330213,奉化区,4,330200 330225,象山县,4,330200 330226,宁海县,4,330200 330281,余姚市,4,330200 330282,慈溪市,4,330200 330302,鹿城区,4,330300 330303,龙湾区,4,330300 330304,瓯海区,4,330300 330305,洞头区,4,330300 330324,永嘉县,4,330300 330326,平阳县,4,330300 330327,苍南县,4,330300 330328,文成县,4,330300 330329,泰顺县,4,330300 330371,温州经济技术开发区,4,330300 330381,瑞安市,4,330300 330382,乐清市,4,330300 330383,龙港市,4,330300 330402,南湖区,4,330400 330411,秀洲区,4,330400 330421,嘉善县,4,330400 330424,海盐县,4,330400 330481,海宁市,4,330400 330482,平湖市,4,330400 330483,桐乡市,4,330400 330502,吴兴区,4,330500 330503,南浔区,4,330500 330521,德清县,4,330500 330522,长兴县,4,330500 330523,安吉县,4,330500 330602,越城区,4,330600 330603,柯桥区,4,330600 330604,上虞区,4,330600 330624,新昌县,4,330600 330681,诸暨市,4,330600 330683,嵊州市,4,330600 330702,婺城区,4,330700 330703,金东区,4,330700 330723,武义县,4,330700 330726,浦江县,4,330700 330727,磐安县,4,330700 330781,兰溪市,4,330700 330782,义乌市,4,330700 330783,东阳市,4,330700 330784,永康市,4,330700 330802,柯城区,4,330800 330803,衢江区,4,330800 330822,常山县,4,330800 330824,开化县,4,330800 330825,龙游县,4,330800 330881,江山市,4,330800 330902,定海区,4,330900 330903,普陀区,4,330900 330921,岱山县,4,330900 330922,嵊泗县,4,330900 331002,椒江区,4,331000 331003,黄岩区,4,331000 331004,路桥区,4,331000 331022,三门县,4,331000 331023,天台县,4,331000 331024,仙居县,4,331000 331081,温岭市,4,331000 331082,临海市,4,331000 331083,玉环市,4,331000 331102,莲都区,4,331100 331121,青田县,4,331100 331122,缙云县,4,331100 331123,遂昌县,4,331100 331124,松阳县,4,331100 331125,云和县,4,331100 331126,庆元县,4,331100 331127,景宁畲族自治县,4,331100 331181,龙泉市,4,331100 340102,瑶海区,4,340100 340103,庐阳区,4,340100 340104,蜀山区,4,340100 340111,包河区,4,340100 340121,长丰县,4,340100 340122,肥东县,4,340100 340123,肥西县,4,340100 340124,庐江县,4,340100 340171,合肥高新技术产业开发区,4,340100 340172,合肥经济技术开发区,4,340100 340173,合肥新站高新技术产业开发区,4,340100 340181,巢湖市,4,340100 340202,镜湖区,4,340200 340207,鸠江区,4,340200 340209,弋江区,4,340200 340210,湾沚区,4,340200 340212,繁昌区,4,340200 340223,南陵县,4,340200 340271,芜湖经济技术开发区,4,340200 340272,安徽芜湖三山经济开发区,4,340200 340281,无为市,4,340200 340302,龙子湖区,4,340300 340303,蚌山区,4,340300 340304,禹会区,4,340300 340311,淮上区,4,340300 340321,怀远县,4,340300 340322,五河县,4,340300 340323,固镇县,4,340300 340371,蚌埠市高新技术开发区,4,340300 340372,蚌埠市经济开发区,4,340300 340402,大通区,4,340400 340403,田家庵区,4,340400 340404,谢家集区,4,340400 340405,八公山区,4,340400 340406,潘集区,4,340400 340421,凤台县,4,340400 340422,寿县,4,340400 340503,花山区,4,340500 340504,雨山区,4,340500 340506,博望区,4,340500 340521,当涂县,4,340500 340522,含山县,4,340500 340523,和县,4,340500 340602,杜集区,4,340600 340603,相山区,4,340600 340604,烈山区,4,340600 340621,濉溪县,4,340600 340705,铜官区,4,340700 340706,义安区,4,340700 340711,郊区,4,340700 340722,枞阳县,4,340700 340802,迎江区,4,340800 340803,大观区,4,340800 340811,宜秀区,4,340800 340822,怀宁县,4,340800 340825,太湖县,4,340800 340826,宿松县,4,340800 340827,望江县,4,340800 340828,岳西县,4,340800 340871,安徽安庆经济开发区,4,340800 340881,桐城市,4,340800 340882,潜山市,4,340800 341002,屯溪区,4,341000 341003,黄山区,4,341000 341004,徽州区,4,341000 341021,歙县,4,341000 341022,休宁县,4,341000 341023,黟县,4,341000 341024,祁门县,4,341000 341102,琅琊区,4,341100 341103,南谯区,4,341100 341122,来安县,4,341100 341124,全椒县,4,341100 341125,定远县,4,341100 341126,凤阳县,4,341100 341171,中新苏滁高新技术产业开发区,4,341100 341172,滁州经济技术开发区,4,341100 341181,天长市,4,341100 341182,明光市,4,341100 341202,颍州区,4,341200 341203,颍东区,4,341200 341204,颍泉区,4,341200 341221,临泉县,4,341200 341222,太和县,4,341200 341225,阜南县,4,341200 341226,颍上县,4,341200 341271,阜阳合肥现代产业园区,4,341200 341272,阜阳经济技术开发区,4,341200 341282,界首市,4,341200 341302,埇桥区,4,341300 341321,砀山县,4,341300 341322,萧县,4,341300 341323,灵璧县,4,341300 341324,泗县,4,341300 341371,宿州马鞍山现代产业园区,4,341300 341372,宿州经济技术开发区,4,341300 341502,金安区,4,341500 341503,裕安区,4,341500 341504,叶集区,4,341500 341522,霍邱县,4,341500 341523,舒城县,4,341500 341524,金寨县,4,341500 341525,霍山县,4,341500 341602,谯城区,4,341600 341621,涡阳县,4,341600 341622,蒙城县,4,341600 341623,利辛县,4,341600 341702,贵池区,4,341700 341721,东至县,4,341700 341722,石台县,4,341700 341723,青阳县,4,341700 341802,宣州区,4,341800 341821,郎溪县,4,341800 341823,泾县,4,341800 341824,绩溪县,4,341800 341825,旌德县,4,341800 341871,宣城市经济开发区,4,341800 341881,宁国市,4,341800 341882,广德市,4,341800 350102,鼓楼区,4,350100 350103,台江区,4,350100 350104,仓山区,4,350100 350105,马尾区,4,350100 350111,晋安区,4,350100 350112,长乐区,4,350100 350121,闽侯县,4,350100 350122,连江县,4,350100 350123,罗源县,4,350100 350124,闽清县,4,350100 350125,永泰县,4,350100 350128,平潭县,4,350100 350181,福清市,4,350100 350203,思明区,4,350200 350205,海沧区,4,350200 350206,湖里区,4,350200 350211,集美区,4,350200 350212,同安区,4,350200 350213,翔安区,4,350200 350302,城厢区,4,350300 350303,涵江区,4,350300 350304,荔城区,4,350300 350305,秀屿区,4,350300 350322,仙游县,4,350300 350404,三元区,4,350400 350405,沙县区,4,350400 350421,明溪县,4,350400 350423,清流县,4,350400 350424,宁化县,4,350400 350425,大田县,4,350400 350426,尤溪县,4,350400 350428,将乐县,4,350400 350429,泰宁县,4,350400 350430,建宁县,4,350400 350481,永安市,4,350400 350502,鲤城区,4,350500 350503,丰泽区,4,350500 350504,洛江区,4,350500 350505,泉港区,4,350500 350521,惠安县,4,350500 350524,安溪县,4,350500 350525,永春县,4,350500 350526,德化县,4,350500 350527,金门县,4,350500 350581,石狮市,4,350500 350582,晋江市,4,350500 350583,南安市,4,350500 350602,芗城区,4,350600 350603,龙文区,4,350600 350604,龙海区,4,350600 350605,长泰区,4,350600 350622,云霄县,4,350600 350623,漳浦县,4,350600 350624,诏安县,4,350600 350626,东山县,4,350600 350627,南靖县,4,350600 350628,平和县,4,350600 350629,华安县,4,350600 350702,延平区,4,350700 350703,建阳区,4,350700 350721,顺昌县,4,350700 350722,浦城县,4,350700 350723,光泽县,4,350700 350724,松溪县,4,350700 350725,政和县,4,350700 350781,邵武市,4,350700 350782,武夷山市,4,350700 350783,建瓯市,4,350700 350802,新罗区,4,350800 350803,永定区,4,350800 350821,长汀县,4,350800 350823,上杭县,4,350800 350824,武平县,4,350800 350825,连城县,4,350800 350881,漳平市,4,350800 350902,蕉城区,4,350900 350921,霞浦县,4,350900 350922,古田县,4,350900 350923,屏南县,4,350900 350924,寿宁县,4,350900 350925,周宁县,4,350900 350926,柘荣县,4,350900 350981,福安市,4,350900 350982,福鼎市,4,350900 360102,东湖区,4,360100 360103,西湖区,4,360100 360104,青云谱区,4,360100 360111,青山湖区,4,360100 360112,新建区,4,360100 360113,红谷滩区,4,360100 360121,南昌县,4,360100 360123,安义县,4,360100 360124,进贤县,4,360100 360202,昌江区,4,360200 360203,珠山区,4,360200 360222,浮梁县,4,360200 360281,乐平市,4,360200 360302,安源区,4,360300 360313,湘东区,4,360300 360321,莲花县,4,360300 360322,上栗县,4,360300 360323,芦溪县,4,360300 360402,濂溪区,4,360400 360403,浔阳区,4,360400 360404,柴桑区,4,360400 360423,武宁县,4,360400 360424,修水县,4,360400 360425,永修县,4,360400 360426,德安县,4,360400 360428,都昌县,4,360400 360429,湖口县,4,360400 360430,彭泽县,4,360400 360481,瑞昌市,4,360400 360482,共青城市,4,360400 360483,庐山市,4,360400 360502,渝水区,4,360500 360521,分宜县,4,360500 360602,月湖区,4,360600 360603,余江区,4,360600 360681,贵溪市,4,360600 360702,章贡区,4,360700 360703,南康区,4,360700 360704,赣县区,4,360700 360722,信丰县,4,360700 360723,大余县,4,360700 360724,上犹县,4,360700 360725,崇义县,4,360700 360726,安远县,4,360700 360728,定南县,4,360700 360729,全南县,4,360700 360730,宁都县,4,360700 360731,于都县,4,360700 360732,兴国县,4,360700 360733,会昌县,4,360700 360734,寻乌县,4,360700 360735,石城县,4,360700 360781,瑞金市,4,360700 360783,龙南市,4,360700 360802,吉州区,4,360800 360803,青原区,4,360800 360821,吉安县,4,360800 360822,吉水县,4,360800 360823,峡江县,4,360800 360824,新干县,4,360800 360825,永丰县,4,360800 360826,泰和县,4,360800 360827,遂川县,4,360800 360828,万安县,4,360800 360829,安福县,4,360800 360830,永新县,4,360800 360881,井冈山市,4,360800 360902,袁州区,4,360900 360921,奉新县,4,360900 360922,万载县,4,360900 360923,上高县,4,360900 360924,宜丰县,4,360900 360925,靖安县,4,360900 360926,铜鼓县,4,360900 360981,丰城市,4,360900 360982,樟树市,4,360900 360983,高安市,4,360900 361002,临川区,4,361000 361003,东乡区,4,361000 361021,南城县,4,361000 361022,黎川县,4,361000 361023,南丰县,4,361000 361024,崇仁县,4,361000 361025,乐安县,4,361000 361026,宜黄县,4,361000 361027,金溪县,4,361000 361028,资溪县,4,361000 361030,广昌县,4,361000 361102,信州区,4,361100 361103,广丰区,4,361100 361104,广信区,4,361100 361123,玉山县,4,361100 361124,铅山县,4,361100 361125,横峰县,4,361100 361126,弋阳县,4,361100 361127,余干县,4,361100 361128,鄱阳县,4,361100 361129,万年县,4,361100 361130,婺源县,4,361100 361181,德兴市,4,361100 370102,历下区,4,370100 370103,市中区,4,370100 370104,槐荫区,4,370100 370105,天桥区,4,370100 370112,历城区,4,370100 370113,长清区,4,370100 370114,章丘区,4,370100 370115,济阳区,4,370100 370116,莱芜区,4,370100 370117,钢城区,4,370100 370124,平阴县,4,370100 370126,商河县,4,370100 370171,济南高新技术产业开发区,4,370100 370202,市南区,4,370200 370203,市北区,4,370200 370211,黄岛区,4,370200 370212,崂山区,4,370200 370213,李沧区,4,370200 370214,城阳区,4,370200 370215,即墨区,4,370200 370271,青岛高新技术产业开发区,4,370200 370281,胶州市,4,370200 370283,平度市,4,370200 370285,莱西市,4,370200 370302,淄川区,4,370300 370303,张店区,4,370300 370304,博山区,4,370300 370305,临淄区,4,370300 370306,周村区,4,370300 370321,桓台县,4,370300 370322,高青县,4,370300 370323,沂源县,4,370300 370402,市中区,4,370400 370403,薛城区,4,370400 370404,峄城区,4,370400 370405,台儿庄区,4,370400 370406,山亭区,4,370400 370481,滕州市,4,370400 370502,东营区,4,370500 370503,河口区,4,370500 370505,垦利区,4,370500 370522,利津县,4,370500 370523,广饶县,4,370500 370571,东营经济技术开发区,4,370500 370572,东营港经济开发区,4,370500 370602,芝罘区,4,370600 370611,福山区,4,370600 370612,牟平区,4,370600 370613,莱山区,4,370600 370614,蓬莱区,4,370600 370671,烟台高新技术产业开发区,4,370600 370672,烟台经济技术开发区,4,370600 370681,龙口市,4,370600 370682,莱阳市,4,370600 370683,莱州市,4,370600 370685,招远市,4,370600 370686,栖霞市,4,370600 370687,海阳市,4,370600 370702,潍城区,4,370700 370703,寒亭区,4,370700 370704,坊子区,4,370700 370705,奎文区,4,370700 370724,临朐县,4,370700 370725,昌乐县,4,370700 370772,潍坊滨海经济技术开发区,4,370700 370781,青州市,4,370700 370782,诸城市,4,370700 370783,寿光市,4,370700 370784,安丘市,4,370700 370785,高密市,4,370700 370786,昌邑市,4,370700 370811,任城区,4,370800 370812,兖州区,4,370800 370826,微山县,4,370800 370827,鱼台县,4,370800 370828,金乡县,4,370800 370829,嘉祥县,4,370800 370830,汶上县,4,370800 370831,泗水县,4,370800 370832,梁山县,4,370800 370871,济宁高新技术产业开发区,4,370800 370881,曲阜市,4,370800 370883,邹城市,4,370800 370902,泰山区,4,370900 370911,岱岳区,4,370900 370921,宁阳县,4,370900 370923,东平县,4,370900 370982,新泰市,4,370900 370983,肥城市,4,370900 371002,环翠区,4,371000 371003,文登区,4,371000 371071,威海火炬高技术产业开发区,4,371000 371072,威海经济技术开发区,4,371000 371073,威海临港经济技术开发区,4,371000 371082,荣成市,4,371000 371083,乳山市,4,371000 371102,东港区,4,371100 371103,岚山区,4,371100 371121,五莲县,4,371100 371122,莒县,4,371100 371171,日照经济技术开发区,4,371100 371302,兰山区,4,371300 371311,罗庄区,4,371300 371312,河东区,4,371300 371321,沂南县,4,371300 371322,郯城县,4,371300 371323,沂水县,4,371300 371324,兰陵县,4,371300 371325,费县,4,371300 371326,平邑县,4,371300 371327,莒南县,4,371300 371328,蒙阴县,4,371300 371329,临沭县,4,371300 371371,临沂高新技术产业开发区,4,371300 371402,德城区,4,371400 371403,陵城区,4,371400 371422,宁津县,4,371400 371423,庆云县,4,371400 371424,临邑县,4,371400 371425,齐河县,4,371400 371426,平原县,4,371400 371427,夏津县,4,371400 371428,武城县,4,371400 371471,德州经济技术开发区,4,371400 371472,德州运河经济开发区,4,371400 371481,乐陵市,4,371400 371482,禹城市,4,371400 371502,东昌府区,4,371500 371503,茌平区,4,371500 371521,阳谷县,4,371500 371522,莘县,4,371500 371524,东阿县,4,371500 371525,冠县,4,371500 371526,高唐县,4,371500 371581,临清市,4,371500 371602,滨城区,4,371600 371603,沾化区,4,371600 371621,惠民县,4,371600 371622,阳信县,4,371600 371623,无棣县,4,371600 371625,博兴县,4,371600 371681,邹平市,4,371600 371702,牡丹区,4,371700 371703,定陶区,4,371700 371721,曹县,4,371700 371722,单县,4,371700 371723,成武县,4,371700 371724,巨野县,4,371700 371725,郓城县,4,371700 371726,鄄城县,4,371700 371728,东明县,4,371700 371771,菏泽经济技术开发区,4,371700 371772,菏泽高新技术开发区,4,371700 410102,中原区,4,410100 410103,二七区,4,410100 410104,管城回族区,4,410100 410105,金水区,4,410100 410106,上街区,4,410100 410108,惠济区,4,410100 410122,中牟县,4,410100 410171,郑州经济技术开发区,4,410100 410172,郑州高新技术产业开发区,4,410100 410173,郑州航空港经济综合实验区,4,410100 410181,巩义市,4,410100 410182,荥阳市,4,410100 410183,新密市,4,410100 410184,新郑市,4,410100 410185,登封市,4,410100 410202,龙亭区,4,410200 410203,顺河回族区,4,410200 410204,鼓楼区,4,410200 410205,禹王台区,4,410200 410212,祥符区,4,410200 410221,杞县,4,410200 410222,通许县,4,410200 410223,尉氏县,4,410200 410225,兰考县,4,410200 410302,老城区,4,410300 410303,西工区,4,410300 410304,瀍河回族区,4,410300 410305,涧西区,4,410300 410307,偃师区,4,410300 410308,孟津区,4,410300 410311,洛龙区,4,410300 410323,新安县,4,410300 410324,栾川县,4,410300 410325,嵩县,4,410300 410326,汝阳县,4,410300 410327,宜阳县,4,410300 410328,洛宁县,4,410300 410329,伊川县,4,410300 410371,洛阳高新技术产业开发区,4,410300 410402,新华区,4,410400 410403,卫东区,4,410400 410404,石龙区,4,410400 410411,湛河区,4,410400 410421,宝丰县,4,410400 410422,叶县,4,410400 410423,鲁山县,4,410400 410425,郏县,4,410400 410471,平顶山高新技术产业开发区,4,410400 410472,平顶山市城乡一体化示范区,4,410400 410481,舞钢市,4,410400 410482,汝州市,4,410400 410502,文峰区,4,410500 410503,北关区,4,410500 410505,殷都区,4,410500 410506,龙安区,4,410500 410522,安阳县,4,410500 410523,汤阴县,4,410500 410526,滑县,4,410500 410527,内黄县,4,410500 410571,安阳高新技术产业开发区,4,410500 410581,林州市,4,410500 410602,鹤山区,4,410600 410603,山城区,4,410600 410611,淇滨区,4,410600 410621,浚县,4,410600 410622,淇县,4,410600 410671,鹤壁经济技术开发区,4,410600 410702,红旗区,4,410700 410703,卫滨区,4,410700 410704,凤泉区,4,410700 410711,牧野区,4,410700 410721,新乡县,4,410700 410724,获嘉县,4,410700 410725,原阳县,4,410700 410726,延津县,4,410700 410727,封丘县,4,410700 410771,新乡高新技术产业开发区,4,410700 410772,新乡经济技术开发区,4,410700 410773,新乡市平原城乡一体化示范区,4,410700 410781,卫辉市,4,410700 410782,辉县市,4,410700 410783,长垣市,4,410700 410802,解放区,4,410800 410803,中站区,4,410800 410804,马村区,4,410800 410811,山阳区,4,410800 410821,修武县,4,410800 410822,博爱县,4,410800 410823,武陟县,4,410800 410825,温县,4,410800 410871,焦作城乡一体化示范区,4,410800 410882,沁阳市,4,410800 410883,孟州市,4,410800 410902,华龙区,4,410900 410922,清丰县,4,410900 410923,南乐县,4,410900 410926,范县,4,410900 410927,台前县,4,410900 410928,濮阳县,4,410900 410971,河南濮阳工业园区,4,410900 410972,濮阳经济技术开发区,4,410900 411002,魏都区,4,411000 411003,建安区,4,411000 411024,鄢陵县,4,411000 411025,襄城县,4,411000 411071,许昌经济技术开发区,4,411000 411081,禹州市,4,411000 411082,长葛市,4,411000 411102,源汇区,4,411100 411103,郾城区,4,411100 411104,召陵区,4,411100 411121,舞阳县,4,411100 411122,临颍县,4,411100 411171,漯河经济技术开发区,4,411100 411202,湖滨区,4,411200 411203,陕州区,4,411200 411221,渑池县,4,411200 411224,卢氏县,4,411200 411271,河南三门峡经济开发区,4,411200 411281,义马市,4,411200 411282,灵宝市,4,411200 411302,宛城区,4,411300 411303,卧龙区,4,411300 411321,南召县,4,411300 411322,方城县,4,411300 411323,西峡县,4,411300 411324,镇平县,4,411300 411325,内乡县,4,411300 411326,淅川县,4,411300 411327,社旗县,4,411300 411328,唐河县,4,411300 411329,新野县,4,411300 411330,桐柏县,4,411300 411371,南阳高新技术产业开发区,4,411300 411372,南阳市城乡一体化示范区,4,411300 411381,邓州市,4,411300 411402,梁园区,4,411400 411403,睢阳区,4,411400 411421,民权县,4,411400 411422,睢县,4,411400 411423,宁陵县,4,411400 411424,柘城县,4,411400 411425,虞城县,4,411400 411426,夏邑县,4,411400 411471,豫东综合物流产业聚集区,4,411400 411472,河南商丘经济开发区,4,411400 411481,永城市,4,411400 411502,浉河区,4,411500 411503,平桥区,4,411500 411521,罗山县,4,411500 411522,光山县,4,411500 411523,新县,4,411500 411524,商城县,4,411500 411525,固始县,4,411500 411526,潢川县,4,411500 411527,淮滨县,4,411500 411528,息县,4,411500 411571,信阳高新技术产业开发区,4,411500 411602,川汇区,4,411600 411603,淮阳区,4,411600 411621,扶沟县,4,411600 411622,西华县,4,411600 411623,商水县,4,411600 411624,沈丘县,4,411600 411625,郸城县,4,411600 411627,太康县,4,411600 411628,鹿邑县,4,411600 411671,河南周口经济开发区,4,411600 411681,项城市,4,411600 411702,驿城区,4,411700 411721,西平县,4,411700 411722,上蔡县,4,411700 411723,平舆县,4,411700 411724,正阳县,4,411700 411725,确山县,4,411700 411726,泌阳县,4,411700 411727,汝南县,4,411700 411728,遂平县,4,411700 411729,新蔡县,4,411700 411771,河南驻马店经济开发区,4,411700 419001,济源市,4,419000 420102,江岸区,4,420100 420103,江汉区,4,420100 420104,硚口区,4,420100 420105,汉阳区,4,420100 420106,武昌区,4,420100 420107,青山区,4,420100 420111,洪山区,4,420100 420112,东西湖区,4,420100 420113,汉南区,4,420100 420114,蔡甸区,4,420100 420115,江夏区,4,420100 420116,黄陂区,4,420100 420117,新洲区,4,420100 420202,黄石港区,4,420200 420203,西塞山区,4,420200 420204,下陆区,4,420200 420205,铁山区,4,420200 420222,阳新县,4,420200 420281,大冶市,4,420200 420302,茅箭区,4,420300 420303,张湾区,4,420300 420304,郧阳区,4,420300 420322,郧西县,4,420300 420323,竹山县,4,420300 420324,竹溪县,4,420300 420325,房县,4,420300 420381,丹江口市,4,420300 420502,西陵区,4,420500 420503,伍家岗区,4,420500 420504,点军区,4,420500 420505,猇亭区,4,420500 420506,夷陵区,4,420500 420525,远安县,4,420500 420526,兴山县,4,420500 420527,秭归县,4,420500 420528,长阳土家族自治县,4,420500 420529,五峰土家族自治县,4,420500 420581,宜都市,4,420500 420582,当阳市,4,420500 420583,枝江市,4,420500 420602,襄城区,4,420600 420606,樊城区,4,420600 420607,襄州区,4,420600 420624,南漳县,4,420600 420625,谷城县,4,420600 420626,保康县,4,420600 420682,老河口市,4,420600 420683,枣阳市,4,420600 420684,宜城市,4,420600 420702,梁子湖区,4,420700 420703,华容区,4,420700 420704,鄂城区,4,420700 420802,东宝区,4,420800 420804,掇刀区,4,420800 420822,沙洋县,4,420800 420881,钟祥市,4,420800 420882,京山市,4,420800 420902,孝南区,4,420900 420921,孝昌县,4,420900 420922,大悟县,4,420900 420923,云梦县,4,420900 420981,应城市,4,420900 420982,安陆市,4,420900 420984,汉川市,4,420900 421002,沙市区,4,421000 421003,荆州区,4,421000 421022,公安县,4,421000 421024,江陵县,4,421000 421071,荆州经济技术开发区,4,421000 421081,石首市,4,421000 421083,洪湖市,4,421000 421087,松滋市,4,421000 421088,监利市,4,421000 421102,黄州区,4,421100 421121,团风县,4,421100 421122,红安县,4,421100 421123,罗田县,4,421100 421124,英山县,4,421100 421125,浠水县,4,421100 421126,蕲春县,4,421100 421127,黄梅县,4,421100 421171,龙感湖管理区,4,421100 421181,麻城市,4,421100 421182,武穴市,4,421100 421202,咸安区,4,421200 421221,嘉鱼县,4,421200 421222,通城县,4,421200 421223,崇阳县,4,421200 421224,通山县,4,421200 421281,赤壁市,4,421200 421303,曾都区,4,421300 421321,随县,4,421300 421381,广水市,4,421300 422801,恩施市,4,422800 422802,利川市,4,422800 422822,建始县,4,422800 422823,巴东县,4,422800 422825,宣恩县,4,422800 422826,咸丰县,4,422800 422827,来凤县,4,422800 422828,鹤峰县,4,422800 429004,仙桃市,4,429000 429005,潜江市,4,429000 429006,天门市,4,429000 429021,神农架林区,4,429000 430102,芙蓉区,4,430100 430103,天心区,4,430100 430104,岳麓区,4,430100 430105,开福区,4,430100 430111,雨花区,4,430100 430112,望城区,4,430100 430121,长沙县,4,430100 430181,浏阳市,4,430100 430182,宁乡市,4,430100 430202,荷塘区,4,430200 430203,芦淞区,4,430200 430204,石峰区,4,430200 430211,天元区,4,430200 430212,渌口区,4,430200 430223,攸县,4,430200 430224,茶陵县,4,430200 430225,炎陵县,4,430200 430271,云龙示范区,4,430200 430281,醴陵市,4,430200 430302,雨湖区,4,430300 430304,岳塘区,4,430300 430321,湘潭县,4,430300 430371,湖南湘潭高新技术产业园区,4,430300 430372,湘潭昭山示范区,4,430300 430373,湘潭九华示范区,4,430300 430381,湘乡市,4,430300 430382,韶山市,4,430300 430405,珠晖区,4,430400 430406,雁峰区,4,430400 430407,石鼓区,4,430400 430408,蒸湘区,4,430400 430412,南岳区,4,430400 430421,衡阳县,4,430400 430422,衡南县,4,430400 430423,衡山县,4,430400 430424,衡东县,4,430400 430426,祁东县,4,430400 430471,衡阳综合保税区,4,430400 430472,湖南衡阳高新技术产业园区,4,430400 430473,湖南衡阳松木经济开发区,4,430400 430481,耒阳市,4,430400 430482,常宁市,4,430400 430502,双清区,4,430500 430503,大祥区,4,430500 430511,北塔区,4,430500 430522,新邵县,4,430500 430523,邵阳县,4,430500 430524,隆回县,4,430500 430525,洞口县,4,430500 430527,绥宁县,4,430500 430528,新宁县,4,430500 430529,城步苗族自治县,4,430500 430581,武冈市,4,430500 430582,邵东市,4,430500 430602,岳阳楼区,4,430600 430603,云溪区,4,430600 430611,君山区,4,430600 430621,岳阳县,4,430600 430623,华容县,4,430600 430624,湘阴县,4,430600 430626,平江县,4,430600 430671,岳阳市屈原管理区,4,430600 430681,汨罗市,4,430600 430682,临湘市,4,430600 430702,武陵区,4,430700 430703,鼎城区,4,430700 430721,安乡县,4,430700 430722,汉寿县,4,430700 430723,澧县,4,430700 430724,临澧县,4,430700 430725,桃源县,4,430700 430726,石门县,4,430700 430771,常德市西洞庭管理区,4,430700 430781,津市市,4,430700 430802,永定区,4,430800 430811,武陵源区,4,430800 430821,慈利县,4,430800 430822,桑植县,4,430800 430902,资阳区,4,430900 430903,赫山区,4,430900 430921,南县,4,430900 430922,桃江县,4,430900 430923,安化县,4,430900 430971,益阳市大通湖管理区,4,430900 430972,湖南益阳高新技术产业园区,4,430900 430981,沅江市,4,430900 431002,北湖区,4,431000 431003,苏仙区,4,431000 431021,桂阳县,4,431000 431022,宜章县,4,431000 431023,永兴县,4,431000 431024,嘉禾县,4,431000 431025,临武县,4,431000 431026,汝城县,4,431000 431027,桂东县,4,431000 431028,安仁县,4,431000 431081,资兴市,4,431000 431102,零陵区,4,431100 431103,冷水滩区,4,431100 431122,东安县,4,431100 431123,双牌县,4,431100 431124,道县,4,431100 431125,江永县,4,431100 431126,宁远县,4,431100 431127,蓝山县,4,431100 431128,新田县,4,431100 431129,江华瑶族自治县,4,431100 431171,永州经济技术开发区,4,431100 431173,永州市回龙圩管理区,4,431100 431181,祁阳市,4,431100 431202,鹤城区,4,431200 431221,中方县,4,431200 431222,沅陵县,4,431200 431223,辰溪县,4,431200 431224,溆浦县,4,431200 431225,会同县,4,431200 431226,麻阳苗族自治县,4,431200 431227,新晃侗族自治县,4,431200 431228,芷江侗族自治县,4,431200 431229,靖州苗族侗族自治县,4,431200 431230,通道侗族自治县,4,431200 431271,怀化市洪江管理区,4,431200 431281,洪江市,4,431200 431302,娄星区,4,431300 431321,双峰县,4,431300 431322,新化县,4,431300 431381,冷水江市,4,431300 431382,涟源市,4,431300 433101,吉首市,4,433100 433122,泸溪县,4,433100 433123,凤凰县,4,433100 433124,花垣县,4,433100 433125,保靖县,4,433100 433126,古丈县,4,433100 433127,永顺县,4,433100 433130,龙山县,4,433100 440103,荔湾区,4,440100 440104,越秀区,4,440100 440105,海珠区,4,440100 440106,天河区,4,440100 440111,白云区,4,440100 440112,黄埔区,4,440100 440113,番禺区,4,440100 440114,花都区,4,440100 440115,南沙区,4,440100 440117,从化区,4,440100 440118,增城区,4,440100 440203,武江区,4,440200 440204,浈江区,4,440200 440205,曲江区,4,440200 440222,始兴县,4,440200 440224,仁化县,4,440200 440229,翁源县,4,440200 440232,乳源瑶族自治县,4,440200 440233,新丰县,4,440200 440281,乐昌市,4,440200 440282,南雄市,4,440200 440303,罗湖区,4,440300 440304,福田区,4,440300 440305,南山区,4,440300 440306,宝安区,4,440300 440307,龙岗区,4,440300 440308,盐田区,4,440300 440309,龙华区,4,440300 440310,坪山区,4,440300 440311,光明区,4,440300 440402,香洲区,4,440400 440403,斗门区,4,440400 440404,金湾区,4,440400 440507,龙湖区,4,440500 440511,金平区,4,440500 440512,濠江区,4,440500 440513,潮阳区,4,440500 440514,潮南区,4,440500 440515,澄海区,4,440500 440523,南澳县,4,440500 440604,禅城区,4,440600 440605,南海区,4,440600 440606,顺德区,4,440600 440607,三水区,4,440600 440608,高明区,4,440600 440703,蓬江区,4,440700 440704,江海区,4,440700 440705,新会区,4,440700 440781,台山市,4,440700 440783,开平市,4,440700 440784,鹤山市,4,440700 440785,恩平市,4,440700 440802,赤坎区,4,440800 440803,霞山区,4,440800 440804,坡头区,4,440800 440811,麻章区,4,440800 440823,遂溪县,4,440800 440825,徐闻县,4,440800 440881,廉江市,4,440800 440882,雷州市,4,440800 440883,吴川市,4,440800 440902,茂南区,4,440900 440904,电白区,4,440900 440981,高州市,4,440900 440982,化州市,4,440900 440983,信宜市,4,440900 441202,端州区,4,441200 441203,鼎湖区,4,441200 441204,高要区,4,441200 441223,广宁县,4,441200 441224,怀集县,4,441200 441225,封开县,4,441200 441226,德庆县,4,441200 441284,四会市,4,441200 441302,惠城区,4,441300 441303,惠阳区,4,441300 441322,博罗县,4,441300 441323,惠东县,4,441300 441324,龙门县,4,441300 441402,梅江区,4,441400 441403,梅县区,4,441400 441422,大埔县,4,441400 441423,丰顺县,4,441400 441424,五华县,4,441400 441426,平远县,4,441400 441427,蕉岭县,4,441400 441481,兴宁市,4,441400 441502,城区,4,441500 441521,海丰县,4,441500 441523,陆河县,4,441500 441581,陆丰市,4,441500 441602,源城区,4,441600 441621,紫金县,4,441600 441622,龙川县,4,441600 441623,连平县,4,441600 441624,和平县,4,441600 441625,东源县,4,441600 441702,江城区,4,441700 441704,阳东区,4,441700 441721,阳西县,4,441700 441781,阳春市,4,441700 441802,清城区,4,441800 441803,清新区,4,441800 441821,佛冈县,4,441800 441823,阳山县,4,441800 441825,连山壮族瑶族自治县,4,441800 441826,连南瑶族自治县,4,441800 441881,英德市,4,441800 441882,连州市,4,441800 445102,湘桥区,4,445100 445103,潮安区,4,445100 445122,饶平县,4,445100 445202,榕城区,4,445200 445203,揭东区,4,445200 445222,揭西县,4,445200 445224,惠来县,4,445200 445281,普宁市,4,445200 445302,云城区,4,445300 445303,云安区,4,445300 445321,新兴县,4,445300 445322,郁南县,4,445300 445381,罗定市,4,445300 450102,兴宁区,4,450100 450103,青秀区,4,450100 450105,江南区,4,450100 450107,西乡塘区,4,450100 450108,良庆区,4,450100 450109,邕宁区,4,450100 450110,武鸣区,4,450100 450123,隆安县,4,450100 450124,马山县,4,450100 450125,上林县,4,450100 450126,宾阳县,4,450100 450181,横州市,4,450100 450202,城中区,4,450200 450203,鱼峰区,4,450200 450204,柳南区,4,450200 450205,柳北区,4,450200 450206,柳江区,4,450200 450222,柳城县,4,450200 450223,鹿寨县,4,450200 450224,融安县,4,450200 450225,融水苗族自治县,4,450200 450226,三江侗族自治县,4,450200 450302,秀峰区,4,450300 450303,叠彩区,4,450300 450304,象山区,4,450300 450305,七星区,4,450300 450311,雁山区,4,450300 450312,临桂区,4,450300 450321,阳朔县,4,450300 450323,灵川县,4,450300 450324,全州县,4,450300 450325,兴安县,4,450300 450326,永福县,4,450300 450327,灌阳县,4,450300 450328,龙胜各族自治县,4,450300 450329,资源县,4,450300 450330,平乐县,4,450300 450332,恭城瑶族自治县,4,450300 450381,荔浦市,4,450300 450403,万秀区,4,450400 450405,长洲区,4,450400 450406,龙圩区,4,450400 450421,苍梧县,4,450400 450422,藤县,4,450400 450423,蒙山县,4,450400 450481,岑溪市,4,450400 450502,海城区,4,450500 450503,银海区,4,450500 450512,铁山港区,4,450500 450521,合浦县,4,450500 450602,港口区,4,450600 450603,防城区,4,450600 450621,上思县,4,450600 450681,东兴市,4,450600 450702,钦南区,4,450700 450703,钦北区,4,450700 450721,灵山县,4,450700 450722,浦北县,4,450700 450802,港北区,4,450800 450803,港南区,4,450800 450804,覃塘区,4,450800 450821,平南县,4,450800 450881,桂平市,4,450800 450902,玉州区,4,450900 450903,福绵区,4,450900 450921,容县,4,450900 450922,陆川县,4,450900 450923,博白县,4,450900 450924,兴业县,4,450900 450981,北流市,4,450900 451002,右江区,4,451000 451003,田阳区,4,451000 451022,田东县,4,451000 451024,德保县,4,451000 451026,那坡县,4,451000 451027,凌云县,4,451000 451028,乐业县,4,451000 451029,田林县,4,451000 451030,西林县,4,451000 451031,隆林各族自治县,4,451000 451081,靖西市,4,451000 451082,平果市,4,451000 451102,八步区,4,451100 451103,平桂区,4,451100 451121,昭平县,4,451100 451122,钟山县,4,451100 451123,富川瑶族自治县,4,451100 451202,金城江区,4,451200 451203,宜州区,4,451200 451221,南丹县,4,451200 451222,天峨县,4,451200 451223,凤山县,4,451200 451224,东兰县,4,451200 451225,罗城仫佬族自治县,4,451200 451226,环江毛南族自治县,4,451200 451227,巴马瑶族自治县,4,451200 451228,都安瑶族自治县,4,451200 451229,大化瑶族自治县,4,451200 451302,兴宾区,4,451300 451321,忻城县,4,451300 451322,象州县,4,451300 451323,武宣县,4,451300 451324,金秀瑶族自治县,4,451300 451381,合山市,4,451300 451402,江州区,4,451400 451421,扶绥县,4,451400 451422,宁明县,4,451400 451423,龙州县,4,451400 451424,大新县,4,451400 451425,天等县,4,451400 451481,凭祥市,4,451400 460105,秀英区,4,460100 460106,龙华区,4,460100 460107,琼山区,4,460100 460108,美兰区,4,460100 460202,海棠区,4,460200 460203,吉阳区,4,460200 460204,天涯区,4,460200 460205,崖州区,4,460200 460321,西沙群岛,4,460300 460322,南沙群岛,4,460300 460323,中沙群岛的岛礁及其海域,4,460300 469001,五指山市,4,469000 469002,琼海市,4,469000 469005,文昌市,4,469000 469006,万宁市,4,469000 469007,东方市,4,469000 469021,定安县,4,469000 469022,屯昌县,4,469000 469023,澄迈县,4,469000 469024,临高县,4,469000 469025,白沙黎族自治县,4,469000 469026,昌江黎族自治县,4,469000 469027,乐东黎族自治县,4,469000 469028,陵水黎族自治县,4,469000 469029,保亭黎族苗族自治县,4,469000 469030,琼中黎族苗族自治县,4,469000 500101,万州区,4,500100 500102,涪陵区,4,500100 500103,渝中区,4,500100 500104,大渡口区,4,500100 500105,江北区,4,500100 500106,沙坪坝区,4,500100 500107,九龙坡区,4,500100 500108,南岸区,4,500100 500109,北碚区,4,500100 500110,綦江区,4,500100 500111,大足区,4,500100 500112,渝北区,4,500100 500113,巴南区,4,500100 500114,黔江区,4,500100 500115,长寿区,4,500100 500116,江津区,4,500100 500117,合川区,4,500100 500118,永川区,4,500100 500119,南川区,4,500100 500120,璧山区,4,500100 500151,铜梁区,4,500100 500152,潼南区,4,500100 500153,荣昌区,4,500100 500154,开州区,4,500100 500155,梁平区,4,500100 500156,武隆区,4,500100 500229,城口县,4,500100 500230,丰都县,4,500100 500231,垫江县,4,500100 500233,忠县,4,500100 500235,云阳县,4,500100 500236,奉节县,4,500100 500237,巫山县,4,500100 500238,巫溪县,4,500100 500240,石柱土家族自治县,4,500100 500241,秀山土家族苗族自治县,4,500100 500242,酉阳土家族苗族自治县,4,500100 500243,彭水苗族土家族自治县,4,500100 510104,锦江区,4,510100 510105,青羊区,4,510100 510106,金牛区,4,510100 510107,武侯区,4,510100 510108,成华区,4,510100 510112,龙泉驿区,4,510100 510113,青白江区,4,510100 510114,新都区,4,510100 510115,温江区,4,510100 510116,双流区,4,510100 510117,郫都区,4,510100 510118,新津区,4,510100 510121,金堂县,4,510100 510129,大邑县,4,510100 510131,蒲江县,4,510100 510181,都江堰市,4,510100 510182,彭州市,4,510100 510183,邛崃市,4,510100 510184,崇州市,4,510100 510185,简阳市,4,510100 510302,自流井区,4,510300 510303,贡井区,4,510300 510304,大安区,4,510300 510311,沿滩区,4,510300 510321,荣县,4,510300 510322,富顺县,4,510300 510402,东区,4,510400 510403,西区,4,510400 510411,仁和区,4,510400 510421,米易县,4,510400 510422,盐边县,4,510400 510502,江阳区,4,510500 510503,纳溪区,4,510500 510504,龙马潭区,4,510500 510521,泸县,4,510500 510522,合江县,4,510500 510524,叙永县,4,510500 510525,古蔺县,4,510500 510603,旌阳区,4,510600 510604,罗江区,4,510600 510623,中江县,4,510600 510681,广汉市,4,510600 510682,什邡市,4,510600 510683,绵竹市,4,510600 510703,涪城区,4,510700 510704,游仙区,4,510700 510705,安州区,4,510700 510722,三台县,4,510700 510723,盐亭县,4,510700 510725,梓潼县,4,510700 510726,北川羌族自治县,4,510700 510727,平武县,4,510700 510781,江油市,4,510700 510802,利州区,4,510800 510811,昭化区,4,510800 510812,朝天区,4,510800 510821,旺苍县,4,510800 510822,青川县,4,510800 510823,剑阁县,4,510800 510824,苍溪县,4,510800 510903,船山区,4,510900 510904,安居区,4,510900 510921,蓬溪县,4,510900 510923,大英县,4,510900 510981,射洪市,4,510900 511002,市中区,4,511000 511011,东兴区,4,511000 511024,威远县,4,511000 511025,资中县,4,511000 511071,内江经济开发区,4,511000 511083,隆昌市,4,511000 511102,市中区,4,511100 511111,沙湾区,4,511100 511112,五通桥区,4,511100 511113,金口河区,4,511100 511123,犍为县,4,511100 511124,井研县,4,511100 511126,夹江县,4,511100 511129,沐川县,4,511100 511132,峨边彝族自治县,4,511100 511133,马边彝族自治县,4,511100 511181,峨眉山市,4,511100 511302,顺庆区,4,511300 511303,高坪区,4,511300 511304,嘉陵区,4,511300 511321,南部县,4,511300 511322,营山县,4,511300 511323,蓬安县,4,511300 511324,仪陇县,4,511300 511325,西充县,4,511300 511381,阆中市,4,511300 511402,东坡区,4,511400 511403,彭山区,4,511400 511421,仁寿县,4,511400 511423,洪雅县,4,511400 511424,丹棱县,4,511400 511425,青神县,4,511400 511502,翠屏区,4,511500 511503,南溪区,4,511500 511504,叙州区,4,511500 511523,江安县,4,511500 511524,长宁县,4,511500 511525,高县,4,511500 511526,珙县,4,511500 511527,筠连县,4,511500 511528,兴文县,4,511500 511529,屏山县,4,511500 511602,广安区,4,511600 511603,前锋区,4,511600 511621,岳池县,4,511600 511622,武胜县,4,511600 511623,邻水县,4,511600 511681,华蓥市,4,511600 511702,通川区,4,511700 511703,达川区,4,511700 511722,宣汉县,4,511700 511723,开江县,4,511700 511724,大竹县,4,511700 511725,渠县,4,511700 511771,达州经济开发区,4,511700 511781,万源市,4,511700 511802,雨城区,4,511800 511803,名山区,4,511800 511822,荥经县,4,511800 511823,汉源县,4,511800 511824,石棉县,4,511800 511825,天全县,4,511800 511826,芦山县,4,511800 511827,宝兴县,4,511800 511902,巴州区,4,511900 511903,恩阳区,4,511900 511921,通江县,4,511900 511922,南江县,4,511900 511923,平昌县,4,511900 511971,巴中经济开发区,4,511900 512002,雁江区,4,512000 512021,安岳县,4,512000 512022,乐至县,4,512000 513201,马尔康市,4,513200 513221,汶川县,4,513200 513222,理县,4,513200 513223,茂县,4,513200 513224,松潘县,4,513200 513225,九寨沟县,4,513200 513226,金川县,4,513200 513227,小金县,4,513200 513228,黑水县,4,513200 513230,壤塘县,4,513200 513231,阿坝县,4,513200 513232,若尔盖县,4,513200 513233,红原县,4,513200 513301,康定市,4,513300 513322,泸定县,4,513300 513323,丹巴县,4,513300 513324,九龙县,4,513300 513325,雅江县,4,513300 513326,道孚县,4,513300 513327,炉霍县,4,513300 513328,甘孜县,4,513300 513329,新龙县,4,513300 513330,德格县,4,513300 513331,白玉县,4,513300 513332,石渠县,4,513300 513333,色达县,4,513300 513334,理塘县,4,513300 513335,巴塘县,4,513300 513336,乡城县,4,513300 513337,稻城县,4,513300 513338,得荣县,4,513300 513401,西昌市,4,513400 513402,会理市,4,513400 513422,木里藏族自治县,4,513400 513423,盐源县,4,513400 513424,德昌县,4,513400 513426,会东县,4,513400 513427,宁南县,4,513400 513428,普格县,4,513400 513429,布拖县,4,513400 513430,金阳县,4,513400 513431,昭觉县,4,513400 513432,喜德县,4,513400 513433,冕宁县,4,513400 513434,越西县,4,513400 513435,甘洛县,4,513400 513436,美姑县,4,513400 513437,雷波县,4,513400 520102,南明区,4,520100 520103,云岩区,4,520100 520111,花溪区,4,520100 520112,乌当区,4,520100 520113,白云区,4,520100 520115,观山湖区,4,520100 520121,开阳县,4,520100 520122,息烽县,4,520100 520123,修文县,4,520100 520181,清镇市,4,520100 520201,钟山区,4,520200 520203,六枝特区,4,520200 520204,水城区,4,520200 520281,盘州市,4,520200 520302,红花岗区,4,520300 520303,汇川区,4,520300 520304,播州区,4,520300 520322,桐梓县,4,520300 520323,绥阳县,4,520300 520324,正安县,4,520300 520325,道真仡佬族苗族自治县,4,520300 520326,务川仡佬族苗族自治县,4,520300 520327,凤冈县,4,520300 520328,湄潭县,4,520300 520329,余庆县,4,520300 520330,习水县,4,520300 520381,赤水市,4,520300 520382,仁怀市,4,520300 520402,西秀区,4,520400 520403,平坝区,4,520400 520422,普定县,4,520400 520423,镇宁布依族苗族自治县,4,520400 520424,关岭布依族苗族自治县,4,520400 520425,紫云苗族布依族自治县,4,520400 520502,七星关区,4,520500 520521,大方县,4,520500 520523,金沙县,4,520500 520524,织金县,4,520500 520525,纳雍县,4,520500 520526,威宁彝族回族苗族自治县,4,520500 520527,赫章县,4,520500 520581,黔西市,4,520500 520602,碧江区,4,520600 520603,万山区,4,520600 520621,江口县,4,520600 520622,玉屏侗族自治县,4,520600 520623,石阡县,4,520600 520624,思南县,4,520600 520625,印江土家族苗族自治县,4,520600 520626,德江县,4,520600 520627,沿河土家族自治县,4,520600 520628,松桃苗族自治县,4,520600 522301,兴义市,4,522300 522302,兴仁市,4,522300 522323,普安县,4,522300 522324,晴隆县,4,522300 522325,贞丰县,4,522300 522326,望谟县,4,522300 522327,册亨县,4,522300 522328,安龙县,4,522300 522601,凯里市,4,522600 522622,黄平县,4,522600 522623,施秉县,4,522600 522624,三穗县,4,522600 522625,镇远县,4,522600 522626,岑巩县,4,522600 522627,天柱县,4,522600 522628,锦屏县,4,522600 522629,剑河县,4,522600 522630,台江县,4,522600 522631,黎平县,4,522600 522632,榕江县,4,522600 522633,从江县,4,522600 522634,雷山县,4,522600 522635,麻江县,4,522600 522636,丹寨县,4,522600 522701,都匀市,4,522700 522702,福泉市,4,522700 522722,荔波县,4,522700 522723,贵定县,4,522700 522725,瓮安县,4,522700 522726,独山县,4,522700 522727,平塘县,4,522700 522728,罗甸县,4,522700 522729,长顺县,4,522700 522730,龙里县,4,522700 522731,惠水县,4,522700 522732,三都水族自治县,4,522700 530102,五华区,4,530100 530103,盘龙区,4,530100 530111,官渡区,4,530100 530112,西山区,4,530100 530113,东川区,4,530100 530114,呈贡区,4,530100 530115,晋宁区,4,530100 530124,富民县,4,530100 530125,宜良县,4,530100 530126,石林彝族自治县,4,530100 530127,嵩明县,4,530100 530128,禄劝彝族苗族自治县,4,530100 530129,寻甸回族彝族自治县,4,530100 530181,安宁市,4,530100 530302,麒麟区,4,530300 530303,沾益区,4,530300 530304,马龙区,4,530300 530322,陆良县,4,530300 530323,师宗县,4,530300 530324,罗平县,4,530300 530325,富源县,4,530300 530326,会泽县,4,530300 530381,宣威市,4,530300 530402,红塔区,4,530400 530403,江川区,4,530400 530423,通海县,4,530400 530424,华宁县,4,530400 530425,易门县,4,530400 530426,峨山彝族自治县,4,530400 530427,新平彝族傣族自治县,4,530400 530428,元江哈尼族彝族傣族自治县,4,530400 530481,澄江市,4,530400 530502,隆阳区,4,530500 530521,施甸县,4,530500 530523,龙陵县,4,530500 530524,昌宁县,4,530500 530581,腾冲市,4,530500 530602,昭阳区,4,530600 530621,鲁甸县,4,530600 530622,巧家县,4,530600 530623,盐津县,4,530600 530624,大关县,4,530600 530625,永善县,4,530600 530626,绥江县,4,530600 530627,镇雄县,4,530600 530628,彝良县,4,530600 530629,威信县,4,530600 530681,水富市,4,530600 530702,古城区,4,530700 530721,玉龙纳西族自治县,4,530700 530722,永胜县,4,530700 530723,华坪县,4,530700 530724,宁蒗彝族自治县,4,530700 530802,思茅区,4,530800 530821,宁洱哈尼族彝族自治县,4,530800 530822,墨江哈尼族自治县,4,530800 530823,景东彝族自治县,4,530800 530824,景谷傣族彝族自治县,4,530800 530825,镇沅彝族哈尼族拉祜族自治县,4,530800 530826,江城哈尼族彝族自治县,4,530800 530827,孟连傣族拉祜族佤族自治县,4,530800 530828,澜沧拉祜族自治县,4,530800 530829,西盟佤族自治县,4,530800 530902,临翔区,4,530900 530921,凤庆县,4,530900 530922,云县,4,530900 530923,永德县,4,530900 530924,镇康县,4,530900 530925,双江拉祜族佤族布朗族傣族自治县,4,530900 530926,耿马傣族佤族自治县,4,530900 530927,沧源佤族自治县,4,530900 532301,楚雄市,4,532300 532302,禄丰市,4,532300 532322,双柏县,4,532300 532323,牟定县,4,532300 532324,南华县,4,532300 532325,姚安县,4,532300 532326,大姚县,4,532300 532327,永仁县,4,532300 532328,元谋县,4,532300 532329,武定县,4,532300 532501,个旧市,4,532500 532502,开远市,4,532500 532503,蒙自市,4,532500 532504,弥勒市,4,532500 532523,屏边苗族自治县,4,532500 532524,建水县,4,532500 532525,石屏县,4,532500 532527,泸西县,4,532500 532528,元阳县,4,532500 532529,红河县,4,532500 532530,金平苗族瑶族傣族自治县,4,532500 532531,绿春县,4,532500 532532,河口瑶族自治县,4,532500 532601,文山市,4,532600 532622,砚山县,4,532600 532623,西畴县,4,532600 532624,麻栗坡县,4,532600 532625,马关县,4,532600 532626,丘北县,4,532600 532627,广南县,4,532600 532628,富宁县,4,532600 532801,景洪市,4,532800 532822,勐海县,4,532800 532823,勐腊县,4,532800 532901,大理市,4,532900 532922,漾濞彝族自治县,4,532900 532923,祥云县,4,532900 532924,宾川县,4,532900 532925,弥渡县,4,532900 532926,南涧彝族自治县,4,532900 532927,巍山彝族回族自治县,4,532900 532928,永平县,4,532900 532929,云龙县,4,532900 532930,洱源县,4,532900 532931,剑川县,4,532900 532932,鹤庆县,4,532900 533102,瑞丽市,4,533100 533103,芒市,4,533100 533122,梁河县,4,533100 533123,盈江县,4,533100 533124,陇川县,4,533100 533301,泸水市,4,533300 533323,福贡县,4,533300 533324,贡山独龙族怒族自治县,4,533300 533325,兰坪白族普米族自治县,4,533300 533401,香格里拉市,4,533400 533422,德钦县,4,533400 533423,维西傈僳族自治县,4,533400 540102,城关区,4,540100 540103,堆龙德庆区,4,540100 540104,达孜区,4,540100 540121,林周县,4,540100 540122,当雄县,4,540100 540123,尼木县,4,540100 540124,曲水县,4,540100 540127,墨竹工卡县,4,540100 540171,格尔木藏青工业园区,4,540100 540172,拉萨经济技术开发区,4,540100 540173,西藏文化旅游创意园区,4,540100 540174,达孜工业园区,4,540100 540202,桑珠孜区,4,540200 540221,南木林县,4,540200 540222,江孜县,4,540200 540223,定日县,4,540200 540224,萨迦县,4,540200 540225,拉孜县,4,540200 540226,昂仁县,4,540200 540227,谢通门县,4,540200 540228,白朗县,4,540200 540229,仁布县,4,540200 540230,康马县,4,540200 540231,定结县,4,540200 540232,仲巴县,4,540200 540233,亚东县,4,540200 540234,吉隆县,4,540200 540235,聂拉木县,4,540200 540236,萨嘎县,4,540200 540237,岗巴县,4,540200 540302,卡若区,4,540300 540321,江达县,4,540300 540322,贡觉县,4,540300 540323,类乌齐县,4,540300 540324,丁青县,4,540300 540325,察雅县,4,540300 540326,八宿县,4,540300 540327,左贡县,4,540300 540328,芒康县,4,540300 540329,洛隆县,4,540300 540330,边坝县,4,540300 540402,巴宜区,4,540400 540421,工布江达县,4,540400 540422,米林县,4,540400 540423,墨脱县,4,540400 540424,波密县,4,540400 540425,察隅县,4,540400 540426,朗县,4,540400 540502,乃东区,4,540500 540521,扎囊县,4,540500 540522,贡嘎县,4,540500 540523,桑日县,4,540500 540524,琼结县,4,540500 540525,曲松县,4,540500 540526,措美县,4,540500 540527,洛扎县,4,540500 540528,加查县,4,540500 540529,隆子县,4,540500 540530,错那县,4,540500 540531,浪卡子县,4,540500 540602,色尼区,4,540600 540621,嘉黎县,4,540600 540622,比如县,4,540600 540623,聂荣县,4,540600 540624,安多县,4,540600 540625,申扎县,4,540600 540626,索县,4,540600 540627,班戈县,4,540600 540628,巴青县,4,540600 540629,尼玛县,4,540600 540630,双湖县,4,540600 542521,普兰县,4,542500 542522,札达县,4,542500 542523,噶尔县,4,542500 542524,日土县,4,542500 542525,革吉县,4,542500 542526,改则县,4,542500 542527,措勤县,4,542500 610102,新城区,4,610100 610103,碑林区,4,610100 610104,莲湖区,4,610100 610111,灞桥区,4,610100 610112,未央区,4,610100 610113,雁塔区,4,610100 610114,阎良区,4,610100 610115,临潼区,4,610100 610116,长安区,4,610100 610117,高陵区,4,610100 610118,鄠邑区,4,610100 610122,蓝田县,4,610100 610124,周至县,4,610100 610202,王益区,4,610200 610203,印台区,4,610200 610204,耀州区,4,610200 610222,宜君县,4,610200 610302,渭滨区,4,610300 610303,金台区,4,610300 610304,陈仓区,4,610300 610305,凤翔区,4,610300 610323,岐山县,4,610300 610324,扶风县,4,610300 610326,眉县,4,610300 610327,陇县,4,610300 610328,千阳县,4,610300 610329,麟游县,4,610300 610330,凤县,4,610300 610331,太白县,4,610300 610402,秦都区,4,610400 610403,杨陵区,4,610400 610404,渭城区,4,610400 610422,三原县,4,610400 610423,泾阳县,4,610400 610424,乾县,4,610400 610425,礼泉县,4,610400 610426,永寿县,4,610400 610428,长武县,4,610400 610429,旬邑县,4,610400 610430,淳化县,4,610400 610431,武功县,4,610400 610481,兴平市,4,610400 610482,彬州市,4,610400 610502,临渭区,4,610500 610503,华州区,4,610500 610522,潼关县,4,610500 610523,大荔县,4,610500 610524,合阳县,4,610500 610525,澄城县,4,610500 610526,蒲城县,4,610500 610527,白水县,4,610500 610528,富平县,4,610500 610581,韩城市,4,610500 610582,华阴市,4,610500 610602,宝塔区,4,610600 610603,安塞区,4,610600 610621,延长县,4,610600 610622,延川县,4,610600 610625,志丹县,4,610600 610626,吴起县,4,610600 610627,甘泉县,4,610600 610628,富县,4,610600 610629,洛川县,4,610600 610630,宜川县,4,610600 610631,黄龙县,4,610600 610632,黄陵县,4,610600 610681,子长市,4,610600 610702,汉台区,4,610700 610703,南郑区,4,610700 610722,城固县,4,610700 610723,洋县,4,610700 610724,西乡县,4,610700 610725,勉县,4,610700 610726,宁强县,4,610700 610727,略阳县,4,610700 610728,镇巴县,4,610700 610729,留坝县,4,610700 610730,佛坪县,4,610700 610802,榆阳区,4,610800 610803,横山区,4,610800 610822,府谷县,4,610800 610824,靖边县,4,610800 610825,定边县,4,610800 610826,绥德县,4,610800 610827,米脂县,4,610800 610828,佳县,4,610800 610829,吴堡县,4,610800 610830,清涧县,4,610800 610831,子洲县,4,610800 610881,神木市,4,610800 610902,汉滨区,4,610900 610921,汉阴县,4,610900 610922,石泉县,4,610900 610923,宁陕县,4,610900 610924,紫阳县,4,610900 610925,岚皋县,4,610900 610926,平利县,4,610900 610927,镇坪县,4,610900 610929,白河县,4,610900 610981,旬阳市,4,610900 611002,商州区,4,611000 611021,洛南县,4,611000 611022,丹凤县,4,611000 611023,商南县,4,611000 611024,山阳县,4,611000 611025,镇安县,4,611000 611026,柞水县,4,611000 620102,城关区,4,620100 620103,七里河区,4,620100 620104,西固区,4,620100 620105,安宁区,4,620100 620111,红古区,4,620100 620121,永登县,4,620100 620122,皋兰县,4,620100 620123,榆中县,4,620100 620171,兰州新区,4,620100 620201,嘉峪关市,4,620200 620302,金川区,4,620300 620321,永昌县,4,620300 620402,白银区,4,620400 620403,平川区,4,620400 620421,靖远县,4,620400 620422,会宁县,4,620400 620423,景泰县,4,620400 620502,秦州区,4,620500 620503,麦积区,4,620500 620521,清水县,4,620500 620522,秦安县,4,620500 620523,甘谷县,4,620500 620524,武山县,4,620500 620525,张家川回族自治县,4,620500 620602,凉州区,4,620600 620621,民勤县,4,620600 620622,古浪县,4,620600 620623,天祝藏族自治县,4,620600 620702,甘州区,4,620700 620721,肃南裕固族自治县,4,620700 620722,民乐县,4,620700 620723,临泽县,4,620700 620724,高台县,4,620700 620725,山丹县,4,620700 620802,崆峒区,4,620800 620821,泾川县,4,620800 620822,灵台县,4,620800 620823,崇信县,4,620800 620825,庄浪县,4,620800 620826,静宁县,4,620800 620881,华亭市,4,620800 620902,肃州区,4,620900 620921,金塔县,4,620900 620922,瓜州县,4,620900 620923,肃北蒙古族自治县,4,620900 620924,阿克塞哈萨克族自治县,4,620900 620981,玉门市,4,620900 620982,敦煌市,4,620900 621002,西峰区,4,621000 621021,庆城县,4,621000 621022,环县,4,621000 621023,华池县,4,621000 621024,合水县,4,621000 621025,正宁县,4,621000 621026,宁县,4,621000 621027,镇原县,4,621000 621102,安定区,4,621100 621121,通渭县,4,621100 621122,陇西县,4,621100 621123,渭源县,4,621100 621124,临洮县,4,621100 621125,漳县,4,621100 621126,岷县,4,621100 621202,武都区,4,621200 621221,成县,4,621200 621222,文县,4,621200 621223,宕昌县,4,621200 621224,康县,4,621200 621225,西和县,4,621200 621226,礼县,4,621200 621227,徽县,4,621200 621228,两当县,4,621200 622901,临夏市,4,622900 622921,临夏县,4,622900 622922,康乐县,4,622900 622923,永靖县,4,622900 622924,广河县,4,622900 622925,和政县,4,622900 622926,东乡族自治县,4,622900 622927,积石山保安族东乡族撒拉族自治县,4,622900 623001,合作市,4,623000 623021,临潭县,4,623000 623022,卓尼县,4,623000 623023,舟曲县,4,623000 623024,迭部县,4,623000 623025,玛曲县,4,623000 623026,碌曲县,4,623000 623027,夏河县,4,623000 630102,城东区,4,630100 630103,城中区,4,630100 630104,城西区,4,630100 630105,城北区,4,630100 630106,湟中区,4,630100 630121,大通回族土族自治县,4,630100 630123,湟源县,4,630100 630202,乐都区,4,630200 630203,平安区,4,630200 630222,民和回族土族自治县,4,630200 630223,互助土族自治县,4,630200 630224,化隆回族自治县,4,630200 630225,循化撒拉族自治县,4,630200 632221,门源回族自治县,4,632200 632222,祁连县,4,632200 632223,海晏县,4,632200 632224,刚察县,4,632200 632301,同仁市,4,632300 632322,尖扎县,4,632300 632323,泽库县,4,632300 632324,河南蒙古族自治县,4,632300 632521,共和县,4,632500 632522,同德县,4,632500 632523,贵德县,4,632500 632524,兴海县,4,632500 632525,贵南县,4,632500 632621,玛沁县,4,632600 632622,班玛县,4,632600 632623,甘德县,4,632600 632624,达日县,4,632600 632625,久治县,4,632600 632626,玛多县,4,632600 632701,玉树市,4,632700 632722,杂多县,4,632700 632723,称多县,4,632700 632724,治多县,4,632700 632725,囊谦县,4,632700 632726,曲麻莱县,4,632700 632801,格尔木市,4,632800 632802,德令哈市,4,632800 632803,茫崖市,4,632800 632821,乌兰县,4,632800 632822,都兰县,4,632800 632823,天峻县,4,632800 632857,大柴旦行政委员会,4,632800 640104,兴庆区,4,640100 640105,西夏区,4,640100 640106,金凤区,4,640100 640121,永宁县,4,640100 640122,贺兰县,4,640100 640181,灵武市,4,640100 640202,大武口区,4,640200 640205,惠农区,4,640200 640221,平罗县,4,640200 640302,利通区,4,640300 640303,红寺堡区,4,640300 640323,盐池县,4,640300 640324,同心县,4,640300 640381,青铜峡市,4,640300 640402,原州区,4,640400 640422,西吉县,4,640400 640423,隆德县,4,640400 640424,泾源县,4,640400 640425,彭阳县,4,640400 640502,沙坡头区,4,640500 640521,中宁县,4,640500 640522,海原县,4,640500 650102,天山区,4,650100 650103,沙依巴克区,4,650100 650104,新市区,4,650100 650105,水磨沟区,4,650100 650106,头屯河区,4,650100 650107,达坂城区,4,650100 650109,米东区,4,650100 650121,乌鲁木齐县,4,650100 650202,独山子区,4,650200 650203,克拉玛依区,4,650200 650204,白碱滩区,4,650200 650205,乌尔禾区,4,650200 650402,高昌区,4,650400 650421,鄯善县,4,650400 650422,托克逊县,4,650400 650502,伊州区,4,650500 650521,巴里坤哈萨克自治县,4,650500 650522,伊吾县,4,650500 652301,昌吉市,4,652300 652302,阜康市,4,652300 652323,呼图壁县,4,652300 652324,玛纳斯县,4,652300 652325,奇台县,4,652300 652327,吉木萨尔县,4,652300 652328,木垒哈萨克自治县,4,652300 652701,博乐市,4,652700 652702,阿拉山口市,4,652700 652722,精河县,4,652700 652723,温泉县,4,652700 652801,库尔勒市,4,652800 652822,轮台县,4,652800 652823,尉犁县,4,652800 652824,若羌县,4,652800 652825,且末县,4,652800 652826,焉耆回族自治县,4,652800 652827,和静县,4,652800 652828,和硕县,4,652800 652829,博湖县,4,652800 652871,库尔勒经济技术开发区,4,652800 652901,阿克苏市,4,652900 652902,库车市,4,652900 652922,温宿县,4,652900 652924,沙雅县,4,652900 652925,新和县,4,652900 652926,拜城县,4,652900 652927,乌什县,4,652900 652928,阿瓦提县,4,652900 652929,柯坪县,4,652900 653001,阿图什市,4,653000 653022,阿克陶县,4,653000 653023,阿合奇县,4,653000 653024,乌恰县,4,653000 653101,喀什市,4,653100 653121,疏附县,4,653100 653122,疏勒县,4,653100 653123,英吉沙县,4,653100 653124,泽普县,4,653100 653125,莎车县,4,653100 653126,叶城县,4,653100 653127,麦盖提县,4,653100 653128,岳普湖县,4,653100 653129,伽师县,4,653100 653130,巴楚县,4,653100 653131,塔什库尔干塔吉克自治县,4,653100 653201,和田市,4,653200 653221,和田县,4,653200 653222,墨玉县,4,653200 653223,皮山县,4,653200 653224,洛浦县,4,653200 653225,策勒县,4,653200 653226,于田县,4,653200 653227,民丰县,4,653200 654002,伊宁市,4,654000 654003,奎屯市,4,654000 654004,霍尔果斯市,4,654000 654021,伊宁县,4,654000 654022,察布查尔锡伯自治县,4,654000 654023,霍城县,4,654000 654024,巩留县,4,654000 654025,新源县,4,654000 654026,昭苏县,4,654000 654027,特克斯县,4,654000 654028,尼勒克县,4,654000 654201,塔城市,4,654200 654202,乌苏市,4,654200 654203,沙湾市,4,654200 654221,额敏县,4,654200 654224,托里县,4,654200 654225,裕民县,4,654200 654226,和布克赛尔蒙古自治县,4,654200 654301,阿勒泰市,4,654300 654321,布尔津县,4,654300 654322,富蕴县,4,654300 654323,福海县,4,654300 654324,哈巴河县,4,654300 654325,青河县,4,654300 654326,吉木乃县,4,654300 659001,石河子市,4,659000 659002,阿拉尔市,4,659000 659003,图木舒克市,4,659000 659004,五家渠市,4,659000 659005,北屯市,4,659000 659006,铁门关市,4,659000 659007,双河市,4,659000 659008,可克达拉市,4,659000 659009,昆玉市,4,659000 659010,胡杨河市,4,659000 659011,新星市,4,659000 ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-ip/src/test/java/co/yixiang/yshop/framework/ip/core/utils/AreaUtilsTest.java ================================================ package co.yixiang.yshop.framework.ip.core.utils; import co.yixiang.yshop.framework.ip.core.Area; import co.yixiang.yshop.framework.ip.core.enums.AreaTypeEnum; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; /** * {@link AreaUtils} 的单元测试 * * @author yshop */ public class AreaUtilsTest { @Test public void testGetArea() { // 调用:北京 Area area = AreaUtils.getArea(110100); // 断言 assertEquals(area.getId(), 110100); assertEquals(area.getName(), "北京市"); assertEquals(area.getType(), AreaTypeEnum.CITY.getType()); assertEquals(area.getParent().getId(), 110000); assertEquals(area.getChildren().size(), 16); } @Test public void testFormat() { assertEquals(AreaUtils.format(110105), "北京 北京市 朝阳区"); assertEquals(AreaUtils.format(1), "中国"); assertEquals(AreaUtils.format(2), "蒙古"); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-ip/src/test/java/co/yixiang/yshop/framework/ip/core/utils/IPUtilsTest.java ================================================ package co.yixiang.yshop.framework.ip.core.utils; import co.yixiang.yshop.framework.ip.core.Area; import org.junit.jupiter.api.Test; import org.lionsoul.ip2region.xdb.Searcher; import static org.junit.jupiter.api.Assertions.assertEquals; /** * {@link IPUtils} 的单元测试 * * @author wanglhup */ public class IPUtilsTest { @Test public void testGetAreaId_string() { // 120.202.4.0|120.202.4.255|420600 Integer areaId = IPUtils.getAreaId("120.202.4.50"); assertEquals(420600, areaId); } @Test public void testGetAreaId_long() throws Exception { // 120.203.123.0|120.203.133.255|360900 long ip = Searcher.checkIP("120.203.123.250"); Integer areaId = IPUtils.getAreaId(ip); assertEquals(360900, areaId); } @Test public void testGetArea_string() { // 120.202.4.0|120.202.4.255|420600 Area area = IPUtils.getArea("120.202.4.50"); assertEquals("襄阳市", area.getName()); } @Test public void testGetArea_long() throws Exception { // 120.203.123.0|120.203.133.255|360900 long ip = Searcher.checkIP("120.203.123.252"); Area area = IPUtils.getArea(ip); assertEquals("宜春市", area.getName()); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/pom.xml ================================================ yshop-framework co.yixiang.boot ${revision} 4.0.0 yshop-spring-boot-starter-biz-tenant jar ${project.artifactId} 多租户 https://gitee.com/guchengwuyue/yshop-drink co.yixiang.boot yshop-common co.yixiang.boot yshop-spring-boot-starter-security co.yixiang.boot yshop-spring-boot-starter-mybatis co.yixiang.boot yshop-spring-boot-starter-redis co.yixiang.boot yshop-spring-boot-starter-job co.yixiang.boot yshop-spring-boot-starter-mq true org.springframework.kafka spring-kafka true org.springframework.amqp spring-rabbit true org.apache.rocketmq rocketmq-spring-boot-starter true co.yixiang.boot yshop-spring-boot-starter-test test com.google.guava guava ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/config/TenantProperties.java ================================================ package co.yixiang.yshop.framework.tenant.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import java.util.Collections; import java.util.Set; /** * 多租户配置 * * @author yshop */ @ConfigurationProperties(prefix = "yshop.tenant") @Data public class TenantProperties { /** * 租户是否开启 */ private static final Boolean ENABLE_DEFAULT = true; /** * 是否开启 */ private Boolean enable = ENABLE_DEFAULT; /** * 需要忽略多租户的请求 * * 默认情况下,每个请求需要带上 tenant-id 的请求头。但是,部分请求是无需带上的,例如说短信回调、支付回调等 Open API! */ private Set ignoreUrls = Collections.emptySet(); /** * 需要忽略多租户的表 * * 即默认所有表都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟 */ private Set ignoreTables = Collections.emptySet(); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/config/YshopTenantAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.tenant.config; import co.yixiang.yshop.framework.common.enums.WebFilterOrderEnum; import co.yixiang.yshop.framework.mybatis.core.util.MyBatisUtils; import co.yixiang.yshop.framework.redis.config.YshopCacheProperties; import co.yixiang.yshop.framework.tenant.core.aop.TenantIgnoreAspect; import co.yixiang.yshop.framework.tenant.core.db.TenantDatabaseInterceptor; import co.yixiang.yshop.framework.tenant.core.job.TenantJobAspect; import co.yixiang.yshop.framework.tenant.core.mq.rabbitmq.TenantRabbitMQInitializer; import co.yixiang.yshop.framework.tenant.core.mq.redis.TenantRedisMessageInterceptor; import co.yixiang.yshop.framework.tenant.core.mq.rocketmq.TenantRocketMQInitializer; import co.yixiang.yshop.framework.tenant.core.redis.TenantRedisCacheManager; import co.yixiang.yshop.framework.tenant.core.security.TenantSecurityWebFilter; import co.yixiang.yshop.framework.tenant.core.service.TenantFrameworkService; import co.yixiang.yshop.framework.tenant.core.service.TenantFrameworkServiceImpl; import co.yixiang.yshop.framework.tenant.core.web.TenantContextWebFilter; import co.yixiang.yshop.framework.web.config.WebProperties; import co.yixiang.yshop.framework.web.core.handler.GlobalExceptionHandler; import co.yixiang.yshop.module.system.api.tenant.TenantApi; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.data.redis.cache.BatchStrategies; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.cache.RedisCacheWriter; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import java.util.Objects; @AutoConfiguration @ConditionalOnProperty(prefix = "yshop.tenant", value = "enable", matchIfMissing = true) // 允许使用 yshop.tenant.enable=false 禁用多租户 @EnableConfigurationProperties(TenantProperties.class) public class YshopTenantAutoConfiguration { @Bean public TenantFrameworkService tenantFrameworkService(TenantApi tenantApi) { return new TenantFrameworkServiceImpl(tenantApi); } // ========== AOP ========== @Bean public TenantIgnoreAspect tenantIgnoreAspect() { return new TenantIgnoreAspect(); } // ========== DB ========== @Bean public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties, MybatisPlusInterceptor interceptor) { TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties)); // 添加到 interceptor 中 // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定 MyBatisUtils.addInterceptor(interceptor, inner, 0); return inner; } // ========== WEB ========== @Bean public FilterRegistrationBean tenantContextWebFilter() { FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new TenantContextWebFilter()); registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER); return registrationBean; } // ========== Security ========== @Bean public FilterRegistrationBean tenantSecurityWebFilter(TenantProperties tenantProperties, WebProperties webProperties, GlobalExceptionHandler globalExceptionHandler, TenantFrameworkService tenantFrameworkService) { FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new TenantSecurityWebFilter(tenantProperties, webProperties, globalExceptionHandler, tenantFrameworkService)); registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER); return registrationBean; } // ========== MQ ========== @Bean public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() { return new TenantRedisMessageInterceptor(); } @Bean @ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate") public TenantRabbitMQInitializer tenantRabbitMQInitializer() { return new TenantRabbitMQInitializer(); } @Bean @ConditionalOnClass(name = "org.apache.rocketmq.spring.core.RocketMQTemplate") public TenantRocketMQInitializer tenantRocketMQInitializer() { return new TenantRocketMQInitializer(); } // ========== Job ========== @Bean public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) { return new TenantJobAspect(tenantFrameworkService); } // ========== Redis ========== @Bean @Primary // 引入租户时,tenantRedisCacheManager 为主 Bean public RedisCacheManager tenantRedisCacheManager(RedisTemplate redisTemplate, RedisCacheConfiguration redisCacheConfiguration, YshopCacheProperties yshopCacheProperties) { // 创建 RedisCacheWriter 对象 RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory()); RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.scan(yshopCacheProperties.getRedisScanBatchSize())); // 创建 TenantRedisCacheManager 对象 return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/core/aop/TenantIgnore.java ================================================ package co.yixiang.yshop.framework.tenant.core.aop; import java.lang.annotation.*; /** * 忽略租户,标记指定方法不进行租户的自动过滤 * * 注意,只有 DB 的场景会过滤,其它场景暂时不过滤: * 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的 * 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略 * * @author yshop */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface TenantIgnore { } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/core/aop/TenantIgnoreAspect.java ================================================ package co.yixiang.yshop.framework.tenant.core.aop; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import co.yixiang.yshop.framework.tenant.core.util.TenantUtils; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; /** * 忽略多租户的 Aspect,基于 {@link TenantIgnore} 注解实现,用于一些全局的逻辑。 * 例如说,一个定时任务,读取所有数据,进行处理。 * 又例如说,读取所有数据,进行缓存。 * * 整体逻辑的实现,和 {@link TenantUtils#executeIgnore(Runnable)} 需要保持一致 * * @author yshop */ @Aspect @Slf4j public class TenantIgnoreAspect { @Around("@annotation(tenantIgnore)") public Object around(ProceedingJoinPoint joinPoint, TenantIgnore tenantIgnore) throws Throwable { Boolean oldIgnore = TenantContextHolder.isIgnore(); try { TenantContextHolder.setIgnore(true); // 执行逻辑 return joinPoint.proceed(); } finally { TenantContextHolder.setIgnore(oldIgnore); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/core/context/TenantContextHolder.java ================================================ package co.yixiang.yshop.framework.tenant.core.context; import co.yixiang.yshop.framework.common.enums.DocumentEnum; import com.alibaba.ttl.TransmittableThreadLocal; /** * 多租户上下文 Holder * * @author yshop */ public class TenantContextHolder { /** * 当前租户编号 */ private static final ThreadLocal TENANT_ID = new TransmittableThreadLocal<>(); /** * 是否忽略租户 */ private static final ThreadLocal IGNORE = new TransmittableThreadLocal<>(); /** * 获得租户编号 * * @return 租户编号 */ public static Long getTenantId() { return TENANT_ID.get(); } /** * 获得租户编号。如果不存在,则抛出 NullPointerException 异常 * * @return 租户编号 */ public static Long getRequiredTenantId() { Long tenantId = getTenantId(); if (tenantId == null) { throw new NullPointerException("TenantContextHolder 不存在租户编号!可参考文档:" + DocumentEnum.TENANT.getUrl()); } return tenantId; } public static void setTenantId(Long tenantId) { TENANT_ID.set(tenantId); } public static void setIgnore(Boolean ignore) { IGNORE.set(ignore); } /** * 当前是否忽略租户 * * @return 是否忽略 */ public static boolean isIgnore() { return Boolean.TRUE.equals(IGNORE.get()); } public static void clear() { TENANT_ID.remove(); IGNORE.remove(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/core/db/TenantBaseDO.java ================================================ package co.yixiang.yshop.framework.tenant.core.db; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import lombok.Data; import lombok.EqualsAndHashCode; /** * 拓展多租户的 BaseDO 基类 * * @author yshop */ @Data @EqualsAndHashCode(callSuper = true) public abstract class TenantBaseDO extends BaseDO { /** * 多租户编号 */ private Long tenantId; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/core/db/TenantDatabaseInterceptor.java ================================================ package co.yixiang.yshop.framework.tenant.core.db; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.tenant.config.TenantProperties; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; import java.util.HashSet; import java.util.Set; /** * 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能 * * @author yshop */ public class TenantDatabaseInterceptor implements TenantLineHandler { private final Set ignoreTables = new HashSet<>(); public TenantDatabaseInterceptor(TenantProperties properties) { // 不同 DB 下,大小写的习惯不同,所以需要都添加进去 properties.getIgnoreTables().forEach(table -> { ignoreTables.add(table.toLowerCase()); ignoreTables.add(table.toUpperCase()); }); // 在 OracleKeyGenerator 中,生成主键时,会查询这个表,查询这个表后,会自动拼接 TENANT_ID 导致报错 ignoreTables.add("DUAL"); } @Override public Expression getTenantId() { return new LongValue(TenantContextHolder.getRequiredTenantId()); } @Override public boolean ignoreTable(String tableName) { return TenantContextHolder.isIgnore() // 情况一,全局忽略多租户 || CollUtil.contains(ignoreTables, tableName); // 情况二,忽略多租户的表 } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/core/job/TenantJob.java ================================================ package co.yixiang.yshop.framework.tenant.core.job; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 多租户 Job 注解 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface TenantJob { } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/core/job/TenantJobAspect.java ================================================ package co.yixiang.yshop.framework.tenant.core.job; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.exceptions.ExceptionUtil; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.framework.tenant.core.service.TenantFrameworkService; import co.yixiang.yshop.framework.tenant.core.util.TenantUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * 多租户 JobHandler AOP * 任务执行时,会按照租户逐个执行 Job 的逻辑 * * 注意,需要保证 JobHandler 的幂等性。因为 Job 因为某个租户执行失败重试时,之前执行成功的租户也会再次执行。 * * @author yshop */ @Aspect @RequiredArgsConstructor @Slf4j public class TenantJobAspect { private final TenantFrameworkService tenantFrameworkService; @Around("@annotation(tenantJob)") public String around(ProceedingJoinPoint joinPoint, TenantJob tenantJob) { // 获得租户列表 List tenantIds = tenantFrameworkService.getTenantIds(); if (CollUtil.isEmpty(tenantIds)) { return null; } // 逐个租户,执行 Job Map results = new ConcurrentHashMap<>(); tenantIds.parallelStream().forEach(tenantId -> { // TODO yshop:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况 TenantUtils.execute(tenantId, () -> { try { joinPoint.proceed(); } catch (Throwable e) { results.put(tenantId, ExceptionUtil.getRootCauseMessage(e)); } }); }); return JsonUtils.toJsonString(results); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/core/mq/kafka/TenantKafkaEnvironmentPostProcessor.java ================================================ package co.yixiang.yshop.framework.tenant.core.mq.kafka; import cn.hutool.core.util.StrUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.env.EnvironmentPostProcessor; import org.springframework.core.env.ConfigurableEnvironment; /** * 多租户的 Kafka 的 {@link EnvironmentPostProcessor} 实现类 * * Kafka Producer 发送消息时,增加 {@link TenantKafkaProducerInterceptor} 拦截器 * * @author yshop */ @Slf4j public class TenantKafkaEnvironmentPostProcessor implements EnvironmentPostProcessor { private static final String PROPERTY_KEY_INTERCEPTOR_CLASSES = "spring.kafka.producer.properties.interceptor.classes"; @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { // 添加 TenantKafkaProducerInterceptor 拦截器 try { String value = environment.getProperty(PROPERTY_KEY_INTERCEPTOR_CLASSES); if (StrUtil.isEmpty(value)) { value = TenantKafkaProducerInterceptor.class.getName(); } else { value += "," + TenantKafkaProducerInterceptor.class.getName(); } environment.getSystemProperties().put(PROPERTY_KEY_INTERCEPTOR_CLASSES, value); } catch (NoClassDefFoundError ignore) { // 如果触发 NoClassDefFoundError 异常,说明 TenantKafkaProducerInterceptor 类不存在,即没引入 kafka-spring 依赖 } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/core/mq/kafka/TenantKafkaProducerInterceptor.java ================================================ package co.yixiang.yshop.framework.tenant.core.mq.kafka; import cn.hutool.core.util.ReflectUtil; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import org.apache.kafka.clients.producer.ProducerInterceptor; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.clients.producer.RecordMetadata; import org.apache.kafka.common.header.Headers; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; import java.util.Map; import static co.yixiang.yshop.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; /** * Kafka 消息队列的多租户 {@link ProducerInterceptor} 实现类 * * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现 * * @author yshop */ public class TenantKafkaProducerInterceptor implements ProducerInterceptor { @Override public ProducerRecord onSend(ProducerRecord record) { Long tenantId = TenantContextHolder.getTenantId(); if (tenantId != null) { Headers headers = (Headers) ReflectUtil.getFieldValue(record, "headers"); // private 属性,没有 get 方法,智能反射 headers.add(HEADER_TENANT_ID, tenantId.toString().getBytes()); } return record; } @Override public void onAcknowledgement(RecordMetadata metadata, Exception exception) { } @Override public void close() { } @Override public void configure(Map configs) { } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/core/mq/rabbitmq/TenantRabbitMQInitializer.java ================================================ package co.yixiang.yshop.framework.tenant.core.mq.rabbitmq; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; /** * 多租户的 RabbitMQ 初始化器 * * @author yshop */ public class TenantRabbitMQInitializer implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof RabbitTemplate) { RabbitTemplate rabbitTemplate = (RabbitTemplate) bean; rabbitTemplate.addBeforePublishPostProcessors(new TenantRabbitMQMessagePostProcessor()); } return bean; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/core/mq/rabbitmq/TenantRabbitMQMessagePostProcessor.java ================================================ package co.yixiang.yshop.framework.tenant.core.mq.rabbitmq; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import org.apache.kafka.clients.producer.ProducerInterceptor; import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; import static co.yixiang.yshop.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; /** * RabbitMQ 消息队列的多租户 {@link ProducerInterceptor} 实现类 * * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现 * * @author yshop */ public class TenantRabbitMQMessagePostProcessor implements MessagePostProcessor { @Override public Message postProcessMessage(Message message) throws AmqpException { Long tenantId = TenantContextHolder.getTenantId(); if (tenantId != null) { message.getMessageProperties().getHeaders().put(HEADER_TENANT_ID, tenantId); } return message; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/core/mq/redis/TenantRedisMessageInterceptor.java ================================================ package co.yixiang.yshop.framework.tenant.core.mq.redis; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.mq.redis.core.interceptor.RedisMessageInterceptor; import co.yixiang.yshop.framework.mq.redis.core.message.AbstractRedisMessage; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import static co.yixiang.yshop.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; /** * 多租户 {@link AbstractRedisMessage} 拦截器 * * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中 * * @author yshop */ public class TenantRedisMessageInterceptor implements RedisMessageInterceptor { @Override public void sendMessageBefore(AbstractRedisMessage message) { Long tenantId = TenantContextHolder.getTenantId(); if (tenantId != null) { message.addHeader(HEADER_TENANT_ID, tenantId.toString()); } } @Override public void consumeMessageBefore(AbstractRedisMessage message) { String tenantIdStr = message.getHeader(HEADER_TENANT_ID); if (StrUtil.isNotEmpty(tenantIdStr)) { TenantContextHolder.setTenantId(Long.valueOf(tenantIdStr)); } } @Override public void consumeMessageAfter(AbstractRedisMessage message) { // 注意,Consumer 是一个逻辑的入口,所以不考虑原本上下文就存在租户编号的情况 TenantContextHolder.clear(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/core/mq/rocketmq/TenantRocketMQConsumeMessageHook.java ================================================ package co.yixiang.yshop.framework.tenant.core.mq.rocketmq; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import org.apache.rocketmq.client.hook.ConsumeMessageContext; import org.apache.rocketmq.client.hook.ConsumeMessageHook; import org.apache.rocketmq.common.message.MessageExt; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; import java.util.List; import static co.yixiang.yshop.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; /** * RocketMQ 消息队列的多租户 {@link ConsumeMessageHook} 实现类 * * Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现 * * @author yshop */ public class TenantRocketMQConsumeMessageHook implements ConsumeMessageHook { @Override public String hookName() { return getClass().getSimpleName(); } @Override public void consumeMessageBefore(ConsumeMessageContext context) { // 校验,消息必须是单条,不然设置租户可能不正确 List messages = context.getMsgList(); Assert.isTrue(messages.size() == 1, "消息条数({})不正确", messages.size()); // 设置租户编号 String tenantId = messages.get(0).getUserProperty(HEADER_TENANT_ID); if (StrUtil.isNotEmpty(tenantId)) { TenantContextHolder.setTenantId(Long.parseLong(tenantId)); } } @Override public void consumeMessageAfter(ConsumeMessageContext context) { TenantContextHolder.clear(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/core/mq/rocketmq/TenantRocketMQInitializer.java ================================================ package co.yixiang.yshop.framework.tenant.core.mq.rocketmq; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl; import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.spring.core.RocketMQTemplate; import org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; /** * 多租户的 RocketMQ 初始化器 * * @author yshop */ public class TenantRocketMQInitializer implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof DefaultRocketMQListenerContainer) { DefaultRocketMQListenerContainer container = (DefaultRocketMQListenerContainer) bean; initTenantConsumer(container.getConsumer()); } else if (bean instanceof RocketMQTemplate) { RocketMQTemplate template = (RocketMQTemplate) bean; initTenantProducer(template.getProducer()); } return bean; } private void initTenantProducer(DefaultMQProducer producer) { if (producer == null) { return; } DefaultMQProducerImpl producerImpl = producer.getDefaultMQProducerImpl(); if (producerImpl == null) { return; } producerImpl.registerSendMessageHook(new TenantRocketMQSendMessageHook()); } private void initTenantConsumer(DefaultMQPushConsumer consumer) { if (consumer == null) { return; } DefaultMQPushConsumerImpl consumerImpl = consumer.getDefaultMQPushConsumerImpl(); if (consumerImpl == null) { return; } consumerImpl.registerConsumeMessageHook(new TenantRocketMQConsumeMessageHook()); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/core/mq/rocketmq/TenantRocketMQSendMessageHook.java ================================================ package co.yixiang.yshop.framework.tenant.core.mq.rocketmq; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import org.apache.rocketmq.client.hook.SendMessageContext; import org.apache.rocketmq.client.hook.SendMessageHook; import static co.yixiang.yshop.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; /** * RocketMQ 消息队列的多租户 {@link SendMessageHook} 实现类 * * Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 * * @author yshop */ public class TenantRocketMQSendMessageHook implements SendMessageHook { @Override public String hookName() { return getClass().getSimpleName(); } @Override public void sendMessageBefore(SendMessageContext sendMessageContext) { Long tenantId = TenantContextHolder.getTenantId(); if (tenantId == null) { return; } sendMessageContext.getMessage().putUserProperty(HEADER_TENANT_ID, tenantId.toString()); } @Override public void sendMessageAfter(SendMessageContext sendMessageContext) { } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/core/redis/TenantRedisCacheManager.java ================================================ package co.yixiang.yshop.framework.tenant.core.redis; import co.yixiang.yshop.framework.redis.core.TimeoutRedisCacheManager; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.Cache; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.cache.RedisCacheWriter; /** * 多租户的 {@link RedisCacheManager} 实现类 * * 操作指定 name 的 {@link Cache} 时,自动拼接租户后缀,格式为 name + ":" + tenantId + 后缀 * * @author airhead */ @Slf4j public class TenantRedisCacheManager extends TimeoutRedisCacheManager { public TenantRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) { super(cacheWriter, defaultCacheConfiguration); } @Override public Cache getCache(String name) { // 如果开启多租户,则 name 拼接租户后缀 if (!TenantContextHolder.isIgnore() && TenantContextHolder.getTenantId() != null) { name = name + ":" + TenantContextHolder.getTenantId(); } // 继续基于父方法 return super.getCache(name); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/core/security/TenantSecurityWebFilter.java ================================================ package co.yixiang.yshop.framework.tenant.core.security; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.common.exception.enums.GlobalErrorCodeConstants; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.util.servlet.ServletUtils; import co.yixiang.yshop.framework.security.core.LoginUser; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import co.yixiang.yshop.framework.tenant.config.TenantProperties; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import co.yixiang.yshop.framework.tenant.core.service.TenantFrameworkService; import co.yixiang.yshop.framework.web.config.WebProperties; import co.yixiang.yshop.framework.web.core.filter.ApiRequestFilter; import co.yixiang.yshop.framework.web.core.handler.GlobalExceptionHandler; import lombok.extern.slf4j.Slf4j; import org.springframework.util.AntPathMatcher; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Objects; /** * 多租户 Security Web 过滤器 * 1. 如果是登陆的用户,校验是否有权限访问该租户,避免越权问题。 * 2. 如果请求未带租户的编号,检查是否是忽略的 URL,否则也不允许访问。 * 3. 校验租户是合法,例如说被禁用、到期 * * @author yshop */ @Slf4j public class TenantSecurityWebFilter extends ApiRequestFilter { private final TenantProperties tenantProperties; private final AntPathMatcher pathMatcher; private final GlobalExceptionHandler globalExceptionHandler; private final TenantFrameworkService tenantFrameworkService; public TenantSecurityWebFilter(TenantProperties tenantProperties, WebProperties webProperties, GlobalExceptionHandler globalExceptionHandler, TenantFrameworkService tenantFrameworkService) { super(webProperties); this.tenantProperties = tenantProperties; this.pathMatcher = new AntPathMatcher(); this.globalExceptionHandler = globalExceptionHandler; this.tenantFrameworkService = tenantFrameworkService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { Long tenantId = TenantContextHolder.getTenantId(); // 1. 登陆的用户,校验是否有权限访问该租户,避免越权问题。 LoginUser user = SecurityFrameworkUtils.getLoginUser(); if (user != null) { // 如果获取不到租户编号,则尝试使用登陆用户的租户编号 if (tenantId == null) { tenantId = user.getTenantId(); TenantContextHolder.setTenantId(tenantId); // 如果传递了租户编号,则进行比对租户编号,避免越权问题 } else if (!Objects.equals(user.getTenantId(), TenantContextHolder.getTenantId())) { log.error("[doFilterInternal][租户({}) User({}/{}) 越权访问租户({}) URL({}/{})]", user.getTenantId(), user.getId(), user.getUserType(), TenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod()); ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.FORBIDDEN.getCode(), "您无权访问该租户的数据")); return; } } // 如果非允许忽略租户的 URL,则校验租户是否合法 if (!isIgnoreUrl(request)) { // 2. 如果请求未带租户的编号,不允许访问。 if (tenantId == null) { log.error("[doFilterInternal][URL({}/{}) 未传递租户编号]", request.getRequestURI(), request.getMethod()); ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "请求的租户标识未传递,请进行排查")); return; } // 3. 校验租户是合法,例如说被禁用、到期 try { tenantFrameworkService.validTenant(tenantId); } catch (Throwable ex) { CommonResult result = globalExceptionHandler.allExceptionHandler(request, ex); ServletUtils.writeJSON(response, result); return; } } else { // 如果是允许忽略租户的 URL,若未传递租户编号,则默认忽略租户编号,避免报错 if (tenantId == null) { TenantContextHolder.setIgnore(true); } } // 继续过滤 chain.doFilter(request, response); } private boolean isIgnoreUrl(HttpServletRequest request) { // 快速匹配,保证性能 if (CollUtil.contains(tenantProperties.getIgnoreUrls(), request.getRequestURI())) { return true; } // 逐个 Ant 路径匹配 for (String url : tenantProperties.getIgnoreUrls()) { if (pathMatcher.match(url, request.getRequestURI())) { return true; } } return false; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/core/service/TenantFrameworkService.java ================================================ package co.yixiang.yshop.framework.tenant.core.service; import java.util.List; /** * Tenant 框架 Service 接口,定义获取租户信息 * * @author yshop */ public interface TenantFrameworkService { /** * 获得所有租户 * * @return 租户编号数组 */ List getTenantIds(); /** * 校验租户是否合法 * * @param id 租户编号 */ void validTenant(Long id); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/core/service/TenantFrameworkServiceImpl.java ================================================ package co.yixiang.yshop.framework.tenant.core.service; import co.yixiang.yshop.framework.common.exception.ServiceException; import co.yixiang.yshop.framework.common.util.cache.CacheUtils; import co.yixiang.yshop.module.system.api.tenant.TenantApi; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import java.time.Duration; import java.util.List; /** * Tenant 框架 Service 实现类 * * @author yshop */ @RequiredArgsConstructor public class TenantFrameworkServiceImpl implements TenantFrameworkService { private static final ServiceException SERVICE_EXCEPTION_NULL = new ServiceException(); private final TenantApi tenantApi; /** * 针对 {@link #getTenantIds()} 的缓存 */ private final LoadingCache> getTenantIdsCache = CacheUtils.buildAsyncReloadingCache( Duration.ofMinutes(1L), // 过期时间 1 分钟 new CacheLoader>() { @Override public List load(Object key) { return tenantApi.getTenantIdList(); } }); /** * 针对 {@link #validTenant(Long)} 的缓存 */ private final LoadingCache validTenantCache = CacheUtils.buildAsyncReloadingCache( Duration.ofMinutes(1L), // 过期时间 1 分钟 new CacheLoader() { @Override public ServiceException load(Long id) { try { tenantApi.validateTenant(id); return SERVICE_EXCEPTION_NULL; } catch (ServiceException ex) { return ex; } } }); @Override @SneakyThrows public List getTenantIds() { return getTenantIdsCache.get(Boolean.TRUE); } @Override public void validTenant(Long id) { ServiceException serviceException = validTenantCache.getUnchecked(id); if (serviceException != SERVICE_EXCEPTION_NULL) { throw serviceException; } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/core/util/TenantUtils.java ================================================ package co.yixiang.yshop.framework.tenant.core.util; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import java.util.Map; import java.util.concurrent.Callable; import static co.yixiang.yshop.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; /** * 多租户 Util * * @author yshop */ public class TenantUtils { /** * 使用指定租户,执行对应的逻辑 * * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户 * 当然,执行完成后,还是会恢复回去 * * @param tenantId 租户编号 * @param runnable 逻辑 */ public static void execute(Long tenantId, Runnable runnable) { Long oldTenantId = TenantContextHolder.getTenantId(); Boolean oldIgnore = TenantContextHolder.isIgnore(); try { TenantContextHolder.setTenantId(tenantId); TenantContextHolder.setIgnore(false); // 执行逻辑 runnable.run(); } finally { TenantContextHolder.setTenantId(oldTenantId); TenantContextHolder.setIgnore(oldIgnore); } } /** * 使用指定租户,执行对应的逻辑 * * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户 * 当然,执行完成后,还是会恢复回去 * * @param tenantId 租户编号 * @param callable 逻辑 */ public static V execute(Long tenantId, Callable callable) { Long oldTenantId = TenantContextHolder.getTenantId(); Boolean oldIgnore = TenantContextHolder.isIgnore(); try { TenantContextHolder.setTenantId(tenantId); TenantContextHolder.setIgnore(false); // 执行逻辑 return callable.call(); } catch (Exception e) { throw new RuntimeException(e); } finally { TenantContextHolder.setTenantId(oldTenantId); TenantContextHolder.setIgnore(oldIgnore); } } /** * 忽略租户,执行对应的逻辑 * * @param runnable 逻辑 */ public static void executeIgnore(Runnable runnable) { Boolean oldIgnore = TenantContextHolder.isIgnore(); try { TenantContextHolder.setIgnore(true); // 执行逻辑 runnable.run(); } finally { TenantContextHolder.setIgnore(oldIgnore); } } /** * 将多租户编号,添加到 header 中 * * @param headers HTTP 请求 headers * @param tenantId 租户编号 */ public static void addTenantHeader(Map headers, Long tenantId) { if (tenantId != null) { headers.put(HEADER_TENANT_ID, tenantId.toString()); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/co/yixiang/yshop/framework/tenant/core/web/TenantContextWebFilter.java ================================================ package co.yixiang.yshop.framework.tenant.core.web; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import co.yixiang.yshop.framework.web.core.util.WebFrameworkUtils; import org.springframework.web.filter.OncePerRequestFilter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; /** * 多租户 Context Web 过滤器 * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。 * * @author yshop */ public class TenantContextWebFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // 设置 Long tenantId = WebFrameworkUtils.getTenantId(request); if (tenantId != null) { TenantContextHolder.setTenantId(tenantId); } try { chain.doFilter(request, response); } finally { // 清理 TenantContextHolder.clear(); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java ================================================ /* * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.messaging.handler.invocation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.Arrays; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import co.yixiang.yshop.framework.tenant.core.util.TenantUtils; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ResolvableType; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.handler.HandlerMethod; import org.springframework.util.ObjectUtils; import static co.yixiang.yshop.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; /** * Extension of {@link HandlerMethod} that invokes the underlying method with * argument values resolved from the current HTTP request through a list of * {@link HandlerMethodArgumentResolver}. * * 针对 rabbitmq-spring 和 kafka-spring,不存在合适的拓展点,可以实现 Consumer 消费前,读取 Header 中的 tenant-id 设置到 {@link TenantContextHolder} 中 * TODO yshop:持续跟进,看看有没新的拓展点 * * @author Rossen Stoyanchev * @author Juergen Hoeller * @since 4.0 */ public class InvocableHandlerMethod extends HandlerMethod { private static final Object[] EMPTY_ARGS = new Object[0]; private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite(); private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); /** * Create an instance from a {@code HandlerMethod}. */ public InvocableHandlerMethod(HandlerMethod handlerMethod) { super(handlerMethod); } /** * Create an instance from a bean instance and a method. */ public InvocableHandlerMethod(Object bean, Method method) { super(bean, method); } /** * Construct a new handler method with the given bean instance, method name and parameters. * @param bean the object bean * @param methodName the method name * @param parameterTypes the method parameter types * @throws NoSuchMethodException when the method cannot be found */ public InvocableHandlerMethod(Object bean, String methodName, Class... parameterTypes) throws NoSuchMethodException { super(bean, methodName, parameterTypes); } /** * Set {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers} to use for resolving method argument values. */ public void setMessageMethodArgumentResolvers(HandlerMethodArgumentResolverComposite argumentResolvers) { this.resolvers = argumentResolvers; } /** * Set the ParameterNameDiscoverer for resolving parameter names when needed * (e.g. default request attribute name). *

Default is a {@link org.springframework.core.DefaultParameterNameDiscoverer}. */ public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) { this.parameterNameDiscoverer = parameterNameDiscoverer; } /** * Invoke the method after resolving its argument values in the context of the given message. *

Argument values are commonly resolved through * {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}. * The {@code providedArgs} parameter however may supply argument values to be used directly, * i.e. without argument resolution. *

Delegates to {@link #getMethodArgumentValues} and calls {@link #doInvoke} with the * resolved arguments. * @param message the current message being processed * @param providedArgs "given" arguments matched by type, not resolved * @return the raw value returned by the invoked method * @throws Exception raised if no suitable argument resolver can be found, * or if the method raised an exception * @see #getMethodArgumentValues * @see #doInvoke */ @Nullable public Object invoke(Message message, Object... providedArgs) throws Exception { Object[] args = getMethodArgumentValues(message, providedArgs); if (logger.isTraceEnabled()) { logger.trace("Arguments: " + Arrays.toString(args)); } // 注意:如下是本类的改动点!!! // 情况一:无租户编号的情况 Long tenantId= parseTenantId(message); if (tenantId == null) { return doInvoke(args); } // 情况二:有租户的情况下 return TenantUtils.execute(tenantId, () -> doInvoke(args)); } private Long parseTenantId(Message message) { Object tenantId = message.getHeaders().get(HEADER_TENANT_ID); if (tenantId == null) { return null; } if (tenantId instanceof Long) { return (Long) tenantId; } if (tenantId instanceof Number) { return ((Number) tenantId).longValue(); } if (tenantId instanceof String) { return Long.parseLong((String) tenantId); } if (tenantId instanceof byte[]) { return Long.parseLong(new String((byte[]) tenantId)); } throw new IllegalArgumentException("未知的数据类型:" + tenantId); } /** * Get the method argument values for the current message, checking the provided * argument values and falling back to the configured argument resolvers. *

The resulting array will be passed into {@link #doInvoke}. * @since 5.1.2 */ protected Object[] getMethodArgumentValues(Message message, Object... providedArgs) throws Exception { MethodParameter[] parameters = getMethodParameters(); if (ObjectUtils.isEmpty(parameters)) { return EMPTY_ARGS; } Object[] args = new Object[parameters.length]; for (int i = 0; i < parameters.length; i++) { MethodParameter parameter = parameters[i]; parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); args[i] = findProvidedArgument(parameter, providedArgs); if (args[i] != null) { continue; } if (!this.resolvers.supportsParameter(parameter)) { throw new MethodArgumentResolutionException( message, parameter, formatArgumentError(parameter, "No suitable resolver")); } try { args[i] = this.resolvers.resolveArgument(parameter, message); } catch (Exception ex) { // Leave stack trace for later, exception may actually be resolved and handled... if (logger.isDebugEnabled()) { String exMsg = ex.getMessage(); if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) { logger.debug(formatArgumentError(parameter, exMsg)); } } throw ex; } } return args; } /** * Invoke the handler method with the given argument values. */ @Nullable protected Object doInvoke(Object... args) throws Exception { try { return getBridgedMethod().invoke(getBean(), args); } catch (IllegalArgumentException ex) { assertTargetBean(getBridgedMethod(), getBean(), args); String text = (ex.getMessage() == null || ex.getCause() instanceof NullPointerException) ? "Illegal argument": ex.getMessage(); throw new IllegalStateException(formatInvokeError(text, args), ex); } catch (InvocationTargetException ex) { // Unwrap for HandlerExceptionResolvers ... Throwable targetException = ex.getTargetException(); if (targetException instanceof RuntimeException runtimeException) { throw runtimeException; } else if (targetException instanceof Error error) { throw error; } else if (targetException instanceof Exception exception) { throw exception; } else { throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException); } } } MethodParameter getAsyncReturnValueType(@Nullable Object returnValue) { return new AsyncResultMethodParameter(returnValue); } private class AsyncResultMethodParameter extends AnnotatedMethodParameter { @Nullable private final Object returnValue; private final ResolvableType returnType; public AsyncResultMethodParameter(@Nullable Object returnValue) { super(-1); this.returnValue = returnValue; this.returnType = ResolvableType.forType(super.getGenericParameterType()).getGeneric(); } protected AsyncResultMethodParameter(AsyncResultMethodParameter original) { super(original); this.returnValue = original.returnValue; this.returnType = original.returnType; } @Override public Class getParameterType() { if (this.returnValue != null) { return this.returnValue.getClass(); } if (!ResolvableType.NONE.equals(this.returnType)) { return this.returnType.toClass(); } return super.getParameterType(); } @Override public Type getGenericParameterType() { return this.returnType.getType(); } @Override public AsyncResultMethodParameter clone() { return new AsyncResultMethodParameter(this); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ co.yixiang.yshop.framework.tenant.config.YshopTenantAutoConfiguration ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring.factories ================================================ org.springframework.boot.env.EnvironmentPostProcessor=\ co.yixiang.yshop.framework.tenant.core.mq.kafka.TenantKafkaEnvironmentPostProcessor ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-excel/pom.xml ================================================ co.yixiang.boot yshop-framework ${revision} 4.0.0 yshop-spring-boot-starter-excel jar ${project.artifactId} Excel 拓展 https://gitee.com/guchengwuyue/yshop-drink co.yixiang.boot yshop-common org.springframework.boot spring-boot-starter co.yixiang.boot yshop-module-system-api ${revision} org.springframework spring-web provided jakarta.servlet jakarta.servlet-api provided com.alibaba easyexcel com.google.guava guava co.yixiang.boot yshop-spring-boot-starter-biz-ip true co.yixiang.boot yshop-spring-boot-starter-test test ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-excel/src/main/java/co/yixiang/yshop/framework/dict/config/YshopDictAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.dict.config; import co.yixiang.yshop.framework.dict.core.DictFrameworkUtils; import co.yixiang.yshop.module.system.api.dict.DictDataApi; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.context.annotation.Bean; @AutoConfiguration public class YshopDictAutoConfiguration { @Bean @SuppressWarnings("InstantiationOfUtilityClass") public DictFrameworkUtils dictUtils(DictDataApi dictDataApi) { DictFrameworkUtils.init(dictDataApi); return new DictFrameworkUtils(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-excel/src/main/java/co/yixiang/yshop/framework/dict/core/DictFrameworkUtils.java ================================================ package co.yixiang.yshop.framework.dict.core; import cn.hutool.core.util.ObjectUtil; import co.yixiang.yshop.framework.common.core.KeyValue; import co.yixiang.yshop.framework.common.util.cache.CacheUtils; import co.yixiang.yshop.module.system.api.dict.DictDataApi; import co.yixiang.yshop.module.system.api.dict.dto.DictDataRespDTO; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import java.time.Duration; import java.util.List; /** * 字典工具类 * * @author yshop */ @Slf4j public class DictFrameworkUtils { private static DictDataApi dictDataApi; private static final DictDataRespDTO DICT_DATA_NULL = new DictDataRespDTO(); // TODO @puhui999:GET_DICT_DATA_CACHE、GET_DICT_DATA_LIST_CACHE、PARSE_DICT_DATA_CACHE 这 3 个缓存是有点重叠,可以思考下,有没可能减少 1 个。微信讨论好私聊,再具体改哈 /** * 针对 {@link #getDictDataLabel(String, String)} 的缓存 */ private static final LoadingCache, DictDataRespDTO> GET_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache( Duration.ofMinutes(1L), // 过期时间 1 分钟 new CacheLoader, DictDataRespDTO>() { @Override public DictDataRespDTO load(KeyValue key) { return ObjectUtil.defaultIfNull(dictDataApi.getDictData(key.getKey(), key.getValue()), DICT_DATA_NULL); } }); /** * 针对 {@link #getDictDataLabelList(String)} 的缓存 */ private static final LoadingCache> GET_DICT_DATA_LIST_CACHE = CacheUtils.buildAsyncReloadingCache( Duration.ofMinutes(1L), // 过期时间 1 分钟 new CacheLoader>() { @Override public List load(String dictType) { return dictDataApi.getDictDataLabelList(dictType); } }); /** * 针对 {@link #parseDictDataValue(String, String)} 的缓存 */ private static final LoadingCache, DictDataRespDTO> PARSE_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache( Duration.ofMinutes(1L), // 过期时间 1 分钟 new CacheLoader, DictDataRespDTO>() { @Override public DictDataRespDTO load(KeyValue key) { return ObjectUtil.defaultIfNull(dictDataApi.parseDictData(key.getKey(), key.getValue()), DICT_DATA_NULL); } }); public static void init(DictDataApi dictDataApi) { DictFrameworkUtils.dictDataApi = dictDataApi; log.info("[init][初始化 DictFrameworkUtils 成功]"); } @SneakyThrows public static String getDictDataLabel(String dictType, Integer value) { return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, String.valueOf(value))).getLabel(); } @SneakyThrows public static String getDictDataLabel(String dictType, String value) { return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, value)).getLabel(); } @SneakyThrows public static List getDictDataLabelList(String dictType) { return GET_DICT_DATA_LIST_CACHE.get(dictType); } @SneakyThrows public static String parseDictDataValue(String dictType, String label) { return PARSE_DICT_DATA_CACHE.get(new KeyValue<>(dictType, label)).getValue(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-excel/src/main/java/co/yixiang/yshop/framework/excel/core/annotations/DictFormat.java ================================================ package co.yixiang.yshop.framework.excel.core.annotations; import java.lang.annotation.*; /** * 字典格式化 * * 实现将字典数据的值,格式化成字典数据的标签 */ @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface DictFormat { /** * 例如说,SysDictTypeConstants、InfDictTypeConstants * * @return 字典类型 */ String value(); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-excel/src/main/java/co/yixiang/yshop/framework/excel/core/annotations/ExcelColumnSelect.java ================================================ package co.yixiang.yshop.framework.excel.core.annotations; import java.lang.annotation.*; /** * 给 Excel 列添加下拉选择数据 * * 其中 {@link #dictType()} 和 {@link #functionName()} 二选一 * * @author HUIHUI */ @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface ExcelColumnSelect { /** * @return 字典类型 */ String dictType() default ""; /** * @return 获取下拉数据源的方法名称 */ String functionName() default ""; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-excel/src/main/java/co/yixiang/yshop/framework/excel/core/convert/AreaConvert.java ================================================ package co.yixiang.yshop.framework.excel.core.convert; import cn.hutool.core.convert.Convert; import co.yixiang.yshop.framework.ip.core.Area; import co.yixiang.yshop.framework.ip.core.utils.AreaUtils; import com.alibaba.excel.converters.Converter; import com.alibaba.excel.enums.CellDataTypeEnum; import com.alibaba.excel.metadata.GlobalConfiguration; import com.alibaba.excel.metadata.data.ReadCellData; import com.alibaba.excel.metadata.property.ExcelContentProperty; import lombok.extern.slf4j.Slf4j; /** * Excel 数据地区转换器 * * @author HUIHUI */ @Slf4j public class AreaConvert implements Converter { @Override public Class supportJavaTypeKey() { throw new UnsupportedOperationException("暂不支持,也不需要"); } @Override public CellDataTypeEnum supportExcelTypeKey() { throw new UnsupportedOperationException("暂不支持,也不需要"); } @Override public Object convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { // 解析地区编号 String label = readCellData.getStringValue(); Area area = AreaUtils.parseArea(label); if (area == null) { log.error("[convertToJavaData][label({}) 解析不掉]", label); return null; } // 将 value 转换成对应的属性 Class fieldClazz = contentProperty.getField().getType(); return Convert.convert(fieldClazz, area.getId()); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-excel/src/main/java/co/yixiang/yshop/framework/excel/core/convert/DictConvert.java ================================================ package co.yixiang.yshop.framework.excel.core.convert; import cn.hutool.core.convert.Convert; import co.yixiang.yshop.framework.dict.core.DictFrameworkUtils; import co.yixiang.yshop.framework.excel.core.annotations.DictFormat; import com.alibaba.excel.converters.Converter; import com.alibaba.excel.enums.CellDataTypeEnum; import com.alibaba.excel.metadata.GlobalConfiguration; import com.alibaba.excel.metadata.data.ReadCellData; import com.alibaba.excel.metadata.data.WriteCellData; import com.alibaba.excel.metadata.property.ExcelContentProperty; import lombok.extern.slf4j.Slf4j; /** * Excel 数据字典转换器 * * @author yshop */ @Slf4j public class DictConvert implements Converter { @Override public Class supportJavaTypeKey() { throw new UnsupportedOperationException("暂不支持,也不需要"); } @Override public CellDataTypeEnum supportExcelTypeKey() { throw new UnsupportedOperationException("暂不支持,也不需要"); } @Override public Object convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { // 使用字典解析 String type = getType(contentProperty); String label = readCellData.getStringValue(); String value = DictFrameworkUtils.parseDictDataValue(type, label); if (value == null) { log.error("[convertToJavaData][type({}) 解析不掉 label({})]", type, label); return null; } // 将 String 的 value 转换成对应的属性 Class fieldClazz = contentProperty.getField().getType(); return Convert.convert(fieldClazz, value); } @Override public WriteCellData convertToExcelData(Object object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { // 空时,返回空 if (object == null) { return new WriteCellData<>(""); } // 使用字典格式化 String type = getType(contentProperty); String value = String.valueOf(object); String label = DictFrameworkUtils.getDictDataLabel(type, value); if (label == null) { log.error("[convertToExcelData][type({}) 转换不了 label({})]", type, value); return new WriteCellData<>(""); } // 生成 Excel 小表格 return new WriteCellData<>(label); } private static String getType(ExcelContentProperty contentProperty) { return contentProperty.getField().getAnnotation(DictFormat.class).value(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-excel/src/main/java/co/yixiang/yshop/framework/excel/core/convert/JsonConvert.java ================================================ package co.yixiang.yshop.framework.excel.core.convert; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import com.alibaba.excel.converters.Converter; import com.alibaba.excel.enums.CellDataTypeEnum; import com.alibaba.excel.metadata.GlobalConfiguration; import com.alibaba.excel.metadata.data.WriteCellData; import com.alibaba.excel.metadata.property.ExcelContentProperty; /** * Excel Json 转换器 * * @author yshop */ public class JsonConvert implements Converter { @Override public Class supportJavaTypeKey() { throw new UnsupportedOperationException("暂不支持,也不需要"); } @Override public CellDataTypeEnum supportExcelTypeKey() { throw new UnsupportedOperationException("暂不支持,也不需要"); } @Override public WriteCellData convertToExcelData(Object value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { // 生成 Excel 小表格 return new WriteCellData<>(JsonUtils.toJsonString(value)); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-excel/src/main/java/co/yixiang/yshop/framework/excel/core/convert/MoneyConvert.java ================================================ package co.yixiang.yshop.framework.excel.core.convert; import com.alibaba.excel.converters.Converter; import com.alibaba.excel.enums.CellDataTypeEnum; import com.alibaba.excel.metadata.GlobalConfiguration; import com.alibaba.excel.metadata.data.WriteCellData; import com.alibaba.excel.metadata.property.ExcelContentProperty; import java.math.BigDecimal; import java.math.RoundingMode; /** * 金额转换器 * * 金额单位:分 * * @author yshop */ public class MoneyConvert implements Converter { @Override public Class supportJavaTypeKey() { throw new UnsupportedOperationException("暂不支持,也不需要"); } @Override public CellDataTypeEnum supportExcelTypeKey() { throw new UnsupportedOperationException("暂不支持,也不需要"); } @Override public WriteCellData convertToExcelData(Integer value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { BigDecimal result = BigDecimal.valueOf(value) .divide(new BigDecimal(100), 2, RoundingMode.HALF_UP); return new WriteCellData<>(result.toString()); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-excel/src/main/java/co/yixiang/yshop/framework/excel/core/function/ExcelColumnSelectFunction.java ================================================ package co.yixiang.yshop.framework.excel.core.function; import java.util.List; /** * Excel 列下拉数据源获取接口 * * 为什么不直接解析字典还搞个接口?考虑到有的下拉数据不是从字典中获取的所有需要做一个兼容 * @author HUIHUI */ public interface ExcelColumnSelectFunction { /** * 获得方法名称 * * @return 方法名称 */ String getName(); /** * 获得列下拉数据源 * * @return 下拉数据源 */ List getOptions(); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-excel/src/main/java/co/yixiang/yshop/framework/excel/core/handler/SelectSheetWriteHandler.java ================================================ package co.yixiang.yshop.framework.excel.core.handler; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.hutool.poi.excel.ExcelUtil; import co.yixiang.yshop.framework.common.core.KeyValue; import co.yixiang.yshop.framework.dict.core.DictFrameworkUtils; import co.yixiang.yshop.framework.excel.core.annotations.ExcelColumnSelect; import co.yixiang.yshop.framework.excel.core.function.ExcelColumnSelectFunction; import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.write.handler.SheetWriteHandler; import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder; import lombok.extern.slf4j.Slf4j; import org.apache.poi.hssf.usermodel.HSSFDataValidation; import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.util.CellRangeAddressList; import java.lang.reflect.Field; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertList; /** * 基于固定 sheet 实现下拉框 * * @author HUIHUI */ @Slf4j public class SelectSheetWriteHandler implements SheetWriteHandler { /** * 数据起始行从 0 开始 * * 约定:本项目第一行有标题所以从 1 开始如果您的 Excel 有多行标题请自行更改 */ public static final int FIRST_ROW = 1; /** * 下拉列需要创建下拉框的行数,默认两千行如需更多请自行调整 */ public static final int LAST_ROW = 2000; private static final String DICT_SHEET_NAME = "字典sheet"; /** * key: 列 value: 下拉数据源 */ private final Map> selectMap = new HashMap<>(); public SelectSheetWriteHandler(Class head) { // 加载下拉数据获取接口 Map beansMap = SpringUtil.getBeanFactory().getBeansOfType(ExcelColumnSelectFunction.class); if (MapUtil.isEmpty(beansMap)) { return; } // 解析下拉数据 int colIndex = 0; for (Field field : head.getDeclaredFields()) { if (field.isAnnotationPresent(ExcelColumnSelect.class)) { ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); if (excelProperty != null && excelProperty.index() != -1) { colIndex = excelProperty.index(); } getSelectDataList(colIndex, field); } colIndex++; } } /** * 获得下拉数据,并添加到 {@link #selectMap} 中 * * @param colIndex 列索引 * @param field 字段 */ private void getSelectDataList(int colIndex, Field field) { ExcelColumnSelect columnSelect = field.getAnnotation(ExcelColumnSelect.class); String dictType = columnSelect.dictType(); String functionName = columnSelect.functionName(); Assert.isTrue(ObjectUtil.isNotEmpty(dictType) || ObjectUtil.isNotEmpty(functionName), "Field({}) 的 @ExcelColumnSelect 注解,dictType 和 functionName 不能同时为空", field.getName()); // 情况一:使用 dictType 获得下拉数据 if (StrUtil.isNotEmpty(dictType)) { // 情况一: 字典数据 (默认) selectMap.put(colIndex, DictFrameworkUtils.getDictDataLabelList(dictType)); return; } // 情况二:使用 functionName 获得下拉数据 Map functionMap = SpringUtil.getApplicationContext().getBeansOfType(ExcelColumnSelectFunction.class); ExcelColumnSelectFunction function = CollUtil.findOne(functionMap.values(), item -> item.getName().equals(functionName)); Assert.notNull(function, "未找到对应的 function({})", functionName); selectMap.put(colIndex, function.getOptions()); } @Override public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { if (CollUtil.isEmpty(selectMap)) { return; } // 1. 获取相应操作对象 DataValidationHelper helper = writeSheetHolder.getSheet().getDataValidationHelper(); // 需要设置下拉框的 sheet 页的数据验证助手 Workbook workbook = writeWorkbookHolder.getWorkbook(); // 获得工作簿 List>> keyValues = convertList(selectMap.entrySet(), entry -> new KeyValue<>(entry.getKey(), entry.getValue())); keyValues.sort(Comparator.comparing(item -> item.getValue().size())); // 升序不然创建下拉会报错 // 2. 创建数据字典的 sheet 页 Sheet dictSheet = workbook.createSheet(DICT_SHEET_NAME); for (KeyValue> keyValue : keyValues) { int rowLength = keyValue.getValue().size(); // 2.1 设置字典 sheet 页的值,每一列一个字典项 for (int i = 0; i < rowLength; i++) { Row row = dictSheet.getRow(i); if (row == null) { row = dictSheet.createRow(i); } row.createCell(keyValue.getKey()).setCellValue(keyValue.getValue().get(i)); } // 2.2 设置单元格下拉选择 setColumnSelect(writeSheetHolder, workbook, helper, keyValue); } } /** * 设置单元格下拉选择 */ private static void setColumnSelect(WriteSheetHolder writeSheetHolder, Workbook workbook, DataValidationHelper helper, KeyValue> keyValue) { // 1.1 创建可被其他单元格引用的名称 Name name = workbook.createName(); String excelColumn = ExcelUtil.indexToColName(keyValue.getKey()); // 1.2 下拉框数据来源 eg:字典sheet!$B1:$B2 String refers = DICT_SHEET_NAME + "!$" + excelColumn + "$1:$" + excelColumn + "$" + keyValue.getValue().size(); name.setNameName("dict" + keyValue.getKey()); // 设置名称的名字 name.setRefersToFormula(refers); // 设置公式 // 2.1 设置约束 DataValidationConstraint constraint = helper.createFormulaListConstraint("dict" + keyValue.getKey()); // 设置引用约束 // 设置下拉单元格的首行、末行、首列、末列 CellRangeAddressList rangeAddressList = new CellRangeAddressList(FIRST_ROW, LAST_ROW, keyValue.getKey(), keyValue.getKey()); DataValidation validation = helper.createValidation(constraint, rangeAddressList); if (validation instanceof HSSFDataValidation) { validation.setSuppressDropDownArrow(false); } else { validation.setSuppressDropDownArrow(true); validation.setShowErrorBox(true); } // 2.2 阻止输入非下拉框的值 validation.setErrorStyle(DataValidation.ErrorStyle.STOP); validation.createErrorBox("提示", "此值不存在于下拉选择中!"); // 2.3 添加下拉框约束 writeSheetHolder.getSheet().addValidationData(validation); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-excel/src/main/java/co/yixiang/yshop/framework/excel/core/util/ExcelUtils.java ================================================ package co.yixiang.yshop.framework.excel.core.util; import co.yixiang.yshop.framework.excel.core.handler.SelectSheetWriteHandler; import com.alibaba.excel.EasyExcel; import com.alibaba.excel.converters.longconverter.LongStringConverter; import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.List; /** * Excel 工具类 * * @author yshop */ public class ExcelUtils { /** * 将列表以 Excel 响应给前端 * * @param response 响应 * @param filename 文件名 * @param sheetName Excel sheet 名 * @param head Excel head 头 * @param data 数据列表哦 * @param 泛型,保证 head 和 data 类型的一致性 * @throws IOException 写入失败的情况 */ public static void write(HttpServletResponse response, String filename, String sheetName, Class head, List data) throws IOException { // 输出 Excel EasyExcel.write(response.getOutputStream(), head) .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度 .registerWriteHandler(new SelectSheetWriteHandler(head)) // 基于固定 sheet 实现下拉框 .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度 .sheet(sheetName).doWrite(data); // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了 response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name())); response.setContentType("application/vnd.ms-excel;charset=UTF-8"); } public static List read(MultipartFile file, Class head) throws IOException { return EasyExcel.read(file.getInputStream(), head, null) .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 .doReadAllSync(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-excel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ co.yixiang.yshop.framework.dict.config.YshopDictAutoConfiguration ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-excel/src/test/java/co/yixiang/yshop/framework/dict/core/util/DictFrameworkUtilsTest.java ================================================ package co.yixiang.yshop.framework.dict.core.util; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.dict.core.DictFrameworkUtils; import co.yixiang.yshop.framework.test.core.ut.BaseMockitoUnitTest; import co.yixiang.yshop.module.system.api.dict.DictDataApi; import co.yixiang.yshop.module.system.api.dict.dto.DictDataRespDTO; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomPojo; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; /** * {@link DictFrameworkUtils} 的单元测试 */ public class DictFrameworkUtilsTest extends BaseMockitoUnitTest { @Mock private DictDataApi dictDataApi; @BeforeEach public void setUp() { DictFrameworkUtils.init(dictDataApi); } @Test public void testGetDictDataLabel() { // mock 数据 DictDataRespDTO dataRespDTO = randomPojo(DictDataRespDTO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); // mock 方法 when(dictDataApi.getDictData(dataRespDTO.getDictType(), dataRespDTO.getValue())).thenReturn(dataRespDTO); // 断言返回值 assertEquals(dataRespDTO.getLabel(), DictFrameworkUtils.getDictDataLabel(dataRespDTO.getDictType(), dataRespDTO.getValue())); } @Test public void testParseDictDataValue() { // mock 数据 DictDataRespDTO resp = randomPojo(DictDataRespDTO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); // mock 方法 when(dictDataApi.parseDictData(resp.getDictType(), resp.getLabel())).thenReturn(resp); // 断言返回值 assertEquals(resp.getValue(), DictFrameworkUtils.parseDictDataValue(resp.getDictType(), resp.getLabel())); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-job/pom.xml ================================================ co.yixiang.boot yshop-framework ${revision} 4.0.0 yshop-spring-boot-starter-job jar ${project.artifactId} 任务拓展 1. 定时任务,基于 Quartz 拓展 2. 异步任务,基于 Spring Async 拓展 https://gitee.com/guchengwuyue/yshop-drink co.yixiang.boot yshop-common org.springframework.boot spring-boot-starter-quartz jakarta.validation jakarta.validation-api ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-job/src/main/java/co/yixiang/yshop/framework/quartz/config/YshopAsyncAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.quartz.config; import com.alibaba.ttl.TtlRunnable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; /** * 异步任务 Configuration */ @AutoConfiguration @EnableAsync public class YshopAsyncAutoConfiguration { @Bean public BeanPostProcessor threadPoolTaskExecutorBeanPostProcessor() { return new BeanPostProcessor() { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (!(bean instanceof ThreadPoolTaskExecutor)) { return bean; } // 修改提交的任务,接入 TransmittableThreadLocal ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) bean; executor.setTaskDecorator(TtlRunnable::get); return executor; } }; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-job/src/main/java/co/yixiang/yshop/framework/quartz/config/YshopQuartzAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.quartz.config; import co.yixiang.yshop.framework.quartz.core.scheduler.SchedulerManager; import lombok.extern.slf4j.Slf4j; import org.quartz.Scheduler; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.EnableScheduling; import java.util.Optional; /** * 定时任务 Configuration */ @AutoConfiguration @EnableScheduling // 开启 Spring 自带的定时任务 @Slf4j public class YshopQuartzAutoConfiguration { @Bean public SchedulerManager schedulerManager(Optional scheduler) { if (!scheduler.isPresent()) { log.info("[定时任务 - 已禁用]"); return new SchedulerManager(null); } return new SchedulerManager(scheduler.get()); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-job/src/main/java/co/yixiang/yshop/framework/quartz/core/enums/JobDataKeyEnum.java ================================================ package co.yixiang.yshop.framework.quartz.core.enums; /** * Quartz Job Data 的 key 枚举 */ public enum JobDataKeyEnum { JOB_ID, JOB_HANDLER_NAME, JOB_HANDLER_PARAM, JOB_RETRY_COUNT, // 最大重试次数 JOB_RETRY_INTERVAL, // 每次重试间隔 } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-job/src/main/java/co/yixiang/yshop/framework/quartz/core/handler/JobHandler.java ================================================ package co.yixiang.yshop.framework.quartz.core.handler; /** * 任务处理器 * * @author yshop */ public interface JobHandler { /** * 执行任务 * * @param param 参数 * @return 结果 * @throws Exception 异常 */ String execute(String param) throws Exception; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-job/src/main/java/co/yixiang/yshop/framework/quartz/core/handler/JobHandlerInvoker.java ================================================ package co.yixiang.yshop.framework.quartz.core.handler; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.thread.ThreadUtil; import co.yixiang.yshop.framework.quartz.core.enums.JobDataKeyEnum; import co.yixiang.yshop.framework.quartz.core.service.JobLogFrameworkService; import lombok.extern.slf4j.Slf4j; import org.quartz.DisallowConcurrentExecution; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.PersistJobDataAfterExecution; import org.springframework.context.ApplicationContext; import org.springframework.scheduling.quartz.QuartzJobBean; import jakarta.annotation.Resource; import java.time.LocalDateTime; import static cn.hutool.core.exceptions.ExceptionUtil.getRootCauseMessage; /** * 基础 Job 调用者,负责调用 {@link JobHandler#execute(String)} 执行任务 * * @author yshop */ @DisallowConcurrentExecution @PersistJobDataAfterExecution @Slf4j public class JobHandlerInvoker extends QuartzJobBean { @Resource private ApplicationContext applicationContext; @Resource private JobLogFrameworkService jobLogFrameworkService; @Override protected void executeInternal(JobExecutionContext executionContext) throws JobExecutionException { // 第一步,获得 Job 数据 Long jobId = executionContext.getMergedJobDataMap().getLong(JobDataKeyEnum.JOB_ID.name()); String jobHandlerName = executionContext.getMergedJobDataMap().getString(JobDataKeyEnum.JOB_HANDLER_NAME.name()); String jobHandlerParam = executionContext.getMergedJobDataMap().getString(JobDataKeyEnum.JOB_HANDLER_PARAM.name()); int refireCount = executionContext.getRefireCount(); int retryCount = (Integer) executionContext.getMergedJobDataMap().getOrDefault(JobDataKeyEnum.JOB_RETRY_COUNT.name(), 0); int retryInterval = (Integer) executionContext.getMergedJobDataMap().getOrDefault(JobDataKeyEnum.JOB_RETRY_INTERVAL.name(), 0); // 第二步,执行任务 Long jobLogId = null; LocalDateTime startTime = LocalDateTime.now(); String data = null; Throwable exception = null; try { // 记录 Job 日志(初始) jobLogId = jobLogFrameworkService.createJobLog(jobId, startTime, jobHandlerName, jobHandlerParam, refireCount + 1); // 执行任务 data = this.executeInternal(jobHandlerName, jobHandlerParam); } catch (Throwable ex) { exception = ex; } // 第三步,记录执行日志 this.updateJobLogResultAsync(jobLogId, startTime, data, exception, executionContext); // 第四步,处理有异常的情况 handleException(exception, refireCount, retryCount, retryInterval); } private String executeInternal(String jobHandlerName, String jobHandlerParam) throws Exception { // 获得 JobHandler 对象 JobHandler jobHandler = applicationContext.getBean(jobHandlerName, JobHandler.class); Assert.notNull(jobHandler, "JobHandler 不会为空"); // 执行任务 return jobHandler.execute(jobHandlerParam); } private void updateJobLogResultAsync(Long jobLogId, LocalDateTime startTime, String data, Throwable exception, JobExecutionContext executionContext) { LocalDateTime endTime = LocalDateTime.now(); // 处理是否成功 boolean success = exception == null; if (!success) { data = getRootCauseMessage(exception); } // 更新日志 try { jobLogFrameworkService.updateJobLogResultAsync(jobLogId, endTime, (int) LocalDateTimeUtil.between(startTime, endTime).toMillis(), success, data); } catch (Exception ex) { log.error("[executeInternal][Job({}) logId({}) 记录执行日志失败({}/{})]", executionContext.getJobDetail().getKey(), jobLogId, success, data); } } private void handleException(Throwable exception, int refireCount, int retryCount, int retryInterval) throws JobExecutionException { // 如果有异常,则进行重试 if (exception == null) { return; } // 情况一:如果到达重试上限,则直接抛出异常即可 if (refireCount >= retryCount) { throw new JobExecutionException(exception); } // 情况二:如果未到达重试上限,则 sleep 一定间隔时间,然后重试 // 这里使用 sleep 来实现,主要还是希望实现比较简单。因为,同一时间,不会存在大量失败的 Job。 if (retryInterval > 0) { ThreadUtil.sleep(retryInterval); } // 第二个参数,refireImmediately = true,表示立即重试 throw new JobExecutionException(exception, true); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-job/src/main/java/co/yixiang/yshop/framework/quartz/core/scheduler/SchedulerManager.java ================================================ package co.yixiang.yshop.framework.quartz.core.scheduler; import co.yixiang.yshop.framework.quartz.core.enums.JobDataKeyEnum; import co.yixiang.yshop.framework.quartz.core.handler.JobHandlerInvoker; import org.quartz.*; import static co.yixiang.yshop.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception0; /** * {@link org.quartz.Scheduler} 的管理器,负责创建任务 * * 考虑到实现的简洁性,我们使用 jobHandlerName 作为唯一标识,即: * 1. Job 的 {@link JobDetail#getKey()} * 2. Trigger 的 {@link Trigger#getKey()} * * 另外,jobHandlerName 对应到 Spring Bean 的名字,直接调用 * * @author yshop */ public class SchedulerManager { private final Scheduler scheduler; public SchedulerManager(Scheduler scheduler) { this.scheduler = scheduler; } /** * 添加 Job 到 Quartz 中 * * @param jobId 任务编号 * @param jobHandlerName 任务处理器的名字 * @param jobHandlerParam 任务处理器的参数 * @param cronExpression CRON 表达式 * @param retryCount 重试次数 * @param retryInterval 重试间隔 * @throws SchedulerException 添加异常 */ public void addJob(Long jobId, String jobHandlerName, String jobHandlerParam, String cronExpression, Integer retryCount, Integer retryInterval) throws SchedulerException { validateScheduler(); // 创建 JobDetail 对象 JobDetail jobDetail = JobBuilder.newJob(JobHandlerInvoker.class) .usingJobData(JobDataKeyEnum.JOB_ID.name(), jobId) .usingJobData(JobDataKeyEnum.JOB_HANDLER_NAME.name(), jobHandlerName) .withIdentity(jobHandlerName).build(); // 创建 Trigger 对象 Trigger trigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval); // 新增 Job 调度 scheduler.scheduleJob(jobDetail, trigger); } /** * 更新 Job 到 Quartz * * @param jobHandlerName 任务处理器的名字 * @param jobHandlerParam 任务处理器的参数 * @param cronExpression CRON 表达式 * @param retryCount 重试次数 * @param retryInterval 重试间隔 * @throws SchedulerException 更新异常 */ public void updateJob(String jobHandlerName, String jobHandlerParam, String cronExpression, Integer retryCount, Integer retryInterval) throws SchedulerException { validateScheduler(); // 创建新 Trigger 对象 Trigger newTrigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval); // 修改调度 scheduler.rescheduleJob(new TriggerKey(jobHandlerName), newTrigger); } /** * 删除 Quartz 中的 Job * * @param jobHandlerName 任务处理器的名字 * @throws SchedulerException 删除异常 */ public void deleteJob(String jobHandlerName) throws SchedulerException { validateScheduler(); // 暂停 Trigger 对象 scheduler.pauseTrigger(new TriggerKey(jobHandlerName)); // 取消并删除 Job 调度 scheduler.unscheduleJob(new TriggerKey(jobHandlerName)); scheduler.deleteJob(new JobKey(jobHandlerName)); } /** * 暂停 Quartz 中的 Job * * @param jobHandlerName 任务处理器的名字 * @throws SchedulerException 暂停异常 */ public void pauseJob(String jobHandlerName) throws SchedulerException { validateScheduler(); scheduler.pauseJob(new JobKey(jobHandlerName)); } /** * 启动 Quartz 中的 Job * * @param jobHandlerName 任务处理器的名字 * @throws SchedulerException 启动异常 */ public void resumeJob(String jobHandlerName) throws SchedulerException { validateScheduler(); scheduler.resumeJob(new JobKey(jobHandlerName)); scheduler.resumeTrigger(new TriggerKey(jobHandlerName)); } /** * 立即触发一次 Quartz 中的 Job * * @param jobId 任务编号 * @param jobHandlerName 任务处理器的名字 * @param jobHandlerParam 任务处理器的参数 * @throws SchedulerException 触发异常 */ public void triggerJob(Long jobId, String jobHandlerName, String jobHandlerParam) throws SchedulerException { validateScheduler(); // 触发任务 JobDataMap data = new JobDataMap(); // 无需重试,所以不设置 retryCount 和 retryInterval data.put(JobDataKeyEnum.JOB_ID.name(), jobId); data.put(JobDataKeyEnum.JOB_HANDLER_NAME.name(), jobHandlerName); data.put(JobDataKeyEnum.JOB_HANDLER_PARAM.name(), jobHandlerParam); scheduler.triggerJob(new JobKey(jobHandlerName), data); } private Trigger buildTrigger(String jobHandlerName, String jobHandlerParam, String cronExpression, Integer retryCount, Integer retryInterval) { return TriggerBuilder.newTrigger() .withIdentity(jobHandlerName) .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)) .usingJobData(JobDataKeyEnum.JOB_HANDLER_PARAM.name(), jobHandlerParam) .usingJobData(JobDataKeyEnum.JOB_RETRY_COUNT.name(), retryCount) .usingJobData(JobDataKeyEnum.JOB_RETRY_INTERVAL.name(), retryInterval) .build(); } private void validateScheduler() { if (scheduler == null) { throw exception0(NOT_IMPLEMENTED.getCode(), "[定时任务 - 已禁用]"); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-job/src/main/java/co/yixiang/yshop/framework/quartz/core/service/JobLogFrameworkService.java ================================================ package co.yixiang.yshop.framework.quartz.core.service; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; /** * Job 日志 Framework Service 接口 * * @author yshop */ public interface JobLogFrameworkService { /** * 创建 Job 日志 * * @param jobId 任务编号 * @param beginTime 开始时间 * @param jobHandlerName Job 处理器的名字 * @param jobHandlerParam Job 处理器的参数 * @param executeIndex 第几次执行 * @return Job 日志的编号 */ Long createJobLog(@NotNull(message = "任务编号不能为空") Long jobId, @NotNull(message = "开始时间") LocalDateTime beginTime, @NotEmpty(message = "Job 处理器的名字不能为空") String jobHandlerName, String jobHandlerParam, @NotNull(message = "第几次执行不能为空") Integer executeIndex); /** * 更新 Job 日志的执行结果 * * @param logId 日志编号 * @param endTime 结束时间。因为是异步,避免记录时间不准去 * @param duration 运行时长,单位:毫秒 * @param success 是否成功 * @param result 成功数据 */ void updateJobLogResultAsync(@NotNull(message = "日志编号不能为空") Long logId, @NotNull(message = "结束时间不能为空") LocalDateTime endTime, @NotNull(message = "运行时长不能为空") Integer duration, boolean success, String result); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-job/src/main/java/co/yixiang/yshop/framework/quartz/core/util/CronUtils.java ================================================ package co.yixiang.yshop.framework.quartz.core.util; import cn.hutool.core.date.LocalDateTimeUtil; import org.quartz.CronExpression; import java.text.ParseException; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * Quartz Cron 表达式的工具类 * * @author yshop */ public class CronUtils { /** * 校验 CRON 表达式是否有效 * * @param cronExpression CRON 表达式 * @return 是否有效 */ public static boolean isValid(String cronExpression) { return CronExpression.isValidExpression(cronExpression); } /** * 基于 CRON 表达式,获得下 n 个满足执行的时间 * * @param cronExpression CRON 表达式 * @param n 数量 * @return 满足条件的执行时间 */ public static List getNextTimes(String cronExpression, int n) { // 获得 CronExpression 对象 CronExpression cron; try { cron = new CronExpression(cronExpression); } catch (ParseException e) { throw new IllegalArgumentException(e.getMessage()); } // 从当前开始计算,n 个满足条件的 Date now = new Date(); List nextTimes = new ArrayList<>(n); for (int i = 0; i < n; i++) { Date nextTime = cron.getNextValidTimeAfter(now); nextTimes.add(LocalDateTimeUtil.of(nextTime)); // 切换现在,为下一个触发时间; now = nextTime; } return nextTimes; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ co.yixiang.yshop.framework.quartz.config.YshopQuartzAutoConfiguration co.yixiang.yshop.framework.quartz.config.YshopAsyncAutoConfiguration ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-monitor/pom.xml ================================================ co.yixiang.boot yshop-framework ${revision} 4.0.0 yshop-spring-boot-starter-monitor jar ${project.artifactId} 服务监控,提供链路追踪、日志服务、指标收集等等功能 https://gitee.com/guchengwuyue/yshop-drink co.yixiang.boot yshop-common org.springframework.boot spring-boot-starter-aop org.springframework spring-web provided jakarta.servlet jakarta.servlet-api provided io.opentracing opentracing-util org.apache.skywalking apm-toolkit-trace org.apache.skywalking apm-toolkit-logback-1.x org.apache.skywalking apm-toolkit-opentracing io.micrometer micrometer-registry-prometheus de.codecentric spring-boot-admin-starter-client ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-monitor/src/main/java/co/yixiang/yshop/framework/tracer/config/TracerProperties.java ================================================ package co.yixiang.yshop.framework.tracer.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; /** * BizTracer配置类 * * @author 麻薯 */ @ConfigurationProperties("yshop.tracer") @Data public class TracerProperties { } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-monitor/src/main/java/co/yixiang/yshop/framework/tracer/config/YshopMetricsAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.tracer.config; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; /** * Metrics 配置类 * * @author yshop */ @AutoConfiguration @ConditionalOnClass({MeterRegistryCustomizer.class}) @ConditionalOnProperty(prefix = "yshop.metrics", value = "enable", matchIfMissing = true) // 允许使用 yshop.metrics.enable=false 禁用 Metrics public class YshopMetricsAutoConfiguration { @Bean public MeterRegistryCustomizer metricsCommonTags( @Value("${spring.application.name}") String applicationName) { return registry -> registry.config().commonTags("application", applicationName); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-monitor/src/main/java/co/yixiang/yshop/framework/tracer/config/YshopTracerAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.tracer.config; import co.yixiang.yshop.framework.common.enums.WebFilterOrderEnum; import co.yixiang.yshop.framework.tracer.core.aop.BizTraceAspect; import co.yixiang.yshop.framework.tracer.core.filter.TraceFilter; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; /** * Tracer 配置类 * * @author mashu */ @AutoConfiguration @ConditionalOnClass({BizTraceAspect.class}) @EnableConfigurationProperties(TracerProperties.class) @ConditionalOnProperty(prefix = "yshop.tracer", value = "enable", matchIfMissing = true) public class YshopTracerAutoConfiguration { // TODO @yshop:重要。目前 opentracing 版本存在冲突,要么保证 skywalking,要么保证阿里云短信 sdk // @Bean // public TracerProperties bizTracerProperties() { // return new TracerProperties(); // } // // @Bean // public BizTraceAspect bizTracingAop() { // return new BizTraceAspect(tracer()); // } // // @Bean // public Tracer tracer() { // // 创建 SkywalkingTracer 对象 // SkywalkingTracer tracer = new SkywalkingTracer(); // // 设置为 GlobalTracer 的追踪器 // GlobalTracer.register(tracer); // return tracer; // } /** * 创建 TraceFilter 过滤器,响应 header 设置 traceId */ @Bean public FilterRegistrationBean traceFilter() { FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new TraceFilter()); registrationBean.setOrder(WebFilterOrderEnum.TRACE_FILTER); return registrationBean; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-monitor/src/main/java/co/yixiang/yshop/framework/tracer/core/annotation/BizTrace.java ================================================ package co.yixiang.yshop.framework.tracer.core.annotation; import java.lang.annotation.*; /** * 打印业务编号 / 业务类型注解 * * 使用时,需要设置 SkyWalking OAP Server 的 application.yaml 配置文件,修改 SW_SEARCHABLE_TAG_KEYS 配置项, * 增加 biz.type 和 biz.id 两值,然后重启 SkyWalking OAP Server 服务器。 * * @author 麻薯 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface BizTrace { /** * 业务编号 tag 名 */ String ID_TAG = "biz.id"; /** * 业务类型 tag 名 */ String TYPE_TAG = "biz.type"; /** * @return 操作名 */ String operationName() default ""; /** * @return 业务编号 */ String id(); /** * @return 业务类型 */ String type(); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-monitor/src/main/java/co/yixiang/yshop/framework/tracer/core/aop/BizTraceAspect.java ================================================ package co.yixiang.yshop.framework.tracer.core.aop; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.tracer.core.annotation.BizTrace; import co.yixiang.yshop.framework.common.util.spring.SpringExpressionUtils; import co.yixiang.yshop.framework.tracer.core.util.TracerFrameworkUtils; import io.opentracing.Span; import io.opentracing.Tracer; import io.opentracing.tag.Tags; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import java.util.Map; import static java.util.Arrays.asList; /** * {@link BizTrace} 切面,记录业务链路 * * @author mashu */ @Aspect @AllArgsConstructor @Slf4j public class BizTraceAspect { private static final String BIZ_OPERATION_NAME_PREFIX = "Biz/"; private final Tracer tracer; @Around(value = "@annotation(trace)") public Object around(ProceedingJoinPoint joinPoint, BizTrace trace) throws Throwable { // 创建 span String operationName = getOperationName(joinPoint, trace); Span span = tracer.buildSpan(operationName) .withTag(Tags.COMPONENT.getKey(), "biz") .start(); try { // 执行原有方法 return joinPoint.proceed(); } catch (Throwable throwable) { TracerFrameworkUtils.onError(throwable, span); throw throwable; } finally { // 设置 Span 的 biz 属性 setBizTag(span, joinPoint, trace); // 完成 Span span.finish(); } } private String getOperationName(ProceedingJoinPoint joinPoint, BizTrace trace) { // 自定义操作名 if (StrUtil.isNotEmpty(trace.operationName())) { return BIZ_OPERATION_NAME_PREFIX + trace.operationName(); } // 默认操作名,使用方法名 return BIZ_OPERATION_NAME_PREFIX + joinPoint.getSignature().getDeclaringType().getSimpleName() + "/" + joinPoint.getSignature().getName(); } private void setBizTag(Span span, ProceedingJoinPoint joinPoint, BizTrace trace) { try { Map result = SpringExpressionUtils.parseExpressions(joinPoint, asList(trace.type(), trace.id())); span.setTag(BizTrace.TYPE_TAG, MapUtil.getStr(result, trace.type())); span.setTag(BizTrace.ID_TAG, MapUtil.getStr(result, trace.id())); } catch (Exception ex) { log.error("[setBizTag][解析 bizType 与 bizId 发生异常]", ex); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-monitor/src/main/java/co/yixiang/yshop/framework/tracer/core/filter/TraceFilter.java ================================================ package co.yixiang.yshop.framework.tracer.core.filter; import co.yixiang.yshop.framework.common.util.monitor.TracerUtils; import org.springframework.web.filter.OncePerRequestFilter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; /** * Trace 过滤器,打印 traceId 到 header 中返回 * * @author yshop */ public class TraceFilter extends OncePerRequestFilter { /** * Header 名 - 链路追踪编号 */ private static final String HEADER_NAME_TRACE_ID = "trace-id"; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { // 设置响应 traceId response.addHeader(HEADER_NAME_TRACE_ID, TracerUtils.getTraceId()); // 继续过滤 chain.doFilter(request, response); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-monitor/src/main/java/co/yixiang/yshop/framework/tracer/core/util/TracerFrameworkUtils.java ================================================ package co.yixiang.yshop.framework.tracer.core.util; import io.opentracing.Span; import io.opentracing.tag.Tags; import java.io.PrintWriter; import java.io.StringWriter; import java.util.HashMap; import java.util.Map; /** * 链路追踪 Util * * @author yshop */ public class TracerFrameworkUtils { /** * 将异常记录到 Span 中,参考自 com.aliyuncs.utils.TraceUtils * * @param throwable 异常 * @param span Span */ public static void onError(Throwable throwable, Span span) { Tags.ERROR.set(span, Boolean.TRUE); if (throwable != null) { span.log(errorLogs(throwable)); } } private static Map errorLogs(Throwable throwable) { Map errorLogs = new HashMap(10); errorLogs.put("event", Tags.ERROR.getKey()); errorLogs.put("error.object", throwable); errorLogs.put("error.kind", throwable.getClass().getName()); String message = throwable.getCause() != null ? throwable.getCause().getMessage() : throwable.getMessage(); if (message != null) { errorLogs.put("message", message); } StringWriter sw = new StringWriter(); throwable.printStackTrace(new PrintWriter(sw)); errorLogs.put("stack", sw.toString()); return errorLogs; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-monitor/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ co.yixiang.yshop.framework.tracer.config.YshopTracerAutoConfiguration co.yixiang.yshop.framework.tracer.config.YshopMetricsAutoConfiguration ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mq/pom.xml ================================================ co.yixiang.boot yshop-framework ${revision} 4.0.0 yshop-spring-boot-starter-mq jar ${project.artifactId} 消息队列,支持 Redis、RocketMQ、RabbitMQ、Kafka 四种 https://gitee.com/guchengwuyue/yshop-drink co.yixiang.boot yshop-spring-boot-starter-redis org.springframework.kafka spring-kafka true org.springframework.amqp spring-rabbit true org.apache.rocketmq rocketmq-spring-boot-starter true ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mq/src/main/java/co/yixiang/yshop/framework/mq/rabbitmq/config/YshopRabbitMQAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.mq.rabbitmq.config; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.context.annotation.Bean; /** * RabbitMQ 消息队列配置类 * * @author yshop */ @AutoConfiguration @Slf4j @ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate") public class YshopRabbitMQAutoConfiguration { /** * Jackson2JsonMessageConverter Bean:使用 jackson 序列化消息 */ @Bean public MessageConverter createMessageConverter() { return new Jackson2JsonMessageConverter(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mq/src/main/java/co/yixiang/yshop/framework/mq/redis/config/YshopRedisMQConsumerAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.mq.redis.config; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.system.SystemUtil; import co.yixiang.yshop.framework.common.enums.DocumentEnum; import co.yixiang.yshop.framework.mq.redis.core.RedisMQTemplate; import co.yixiang.yshop.framework.mq.redis.core.job.RedisPendingMessageResendJob; import co.yixiang.yshop.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener; import co.yixiang.yshop.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener; import co.yixiang.yshop.framework.redis.config.YshopRedisAutoConfiguration; import lombok.extern.slf4j.Slf4j; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.context.annotation.Bean; import org.springframework.data.redis.connection.RedisServerCommands; import org.springframework.data.redis.connection.stream.Consumer; import org.springframework.data.redis.connection.stream.ObjectRecord; import org.springframework.data.redis.connection.stream.ReadOffset; import org.springframework.data.redis.connection.stream.StreamOffset; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.listener.ChannelTopic; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.stream.StreamMessageListenerContainer; import org.springframework.scheduling.annotation.EnableScheduling; import java.util.List; import java.util.Properties; /** * Redis 消息队列 Consumer 配置类 * * @author yshop */ @Slf4j @EnableScheduling // 启用定时任务,用于 RedisPendingMessageResendJob 重发消息 @AutoConfiguration(after = YshopRedisAutoConfiguration.class) public class YshopRedisMQConsumerAutoConfiguration { /** * 创建 Redis Pub/Sub 广播消费的容器 */ @Bean @ConditionalOnBean(AbstractRedisChannelMessageListener.class) // 只有 AbstractChannelMessageListener 存在的时候,才需要注册 Redis pubsub 监听 public RedisMessageListenerContainer redisMessageListenerContainer( RedisMQTemplate redisMQTemplate, List> listeners) { // 创建 RedisMessageListenerContainer 对象 RedisMessageListenerContainer container = new RedisMessageListenerContainer(); // 设置 RedisConnection 工厂。 container.setConnectionFactory(redisMQTemplate.getRedisTemplate().getRequiredConnectionFactory()); // 添加监听器 listeners.forEach(listener -> { listener.setRedisMQTemplate(redisMQTemplate); container.addMessageListener(listener, new ChannelTopic(listener.getChannel())); log.info("[redisMessageListenerContainer][注册 Channel({}) 对应的监听器({})]", listener.getChannel(), listener.getClass().getName()); }); return container; } /** * 创建 Redis Stream 重新消费的任务 */ @Bean @ConditionalOnBean(AbstractRedisStreamMessageListener.class) // 只有 AbstractStreamMessageListener 存在的时候,才需要注册 Redis pubsub 监听 public RedisPendingMessageResendJob redisPendingMessageResendJob(List> listeners, RedisMQTemplate redisTemplate, @Value("${spring.application.name}") String groupName, RedissonClient redissonClient) { return new RedisPendingMessageResendJob(listeners, redisTemplate, groupName, redissonClient); } /** * 创建 Redis Stream 集群消费的容器 * * 基础知识:Redis Stream 的 xreadgroup 命令 */ @Bean(initMethod = "start", destroyMethod = "stop") @ConditionalOnBean(AbstractRedisStreamMessageListener.class) // 只有 AbstractStreamMessageListener 存在的时候,才需要注册 Redis pubsub 监听 public StreamMessageListenerContainer> redisStreamMessageListenerContainer( RedisMQTemplate redisMQTemplate, List> listeners) { RedisTemplate redisTemplate = redisMQTemplate.getRedisTemplate(); checkRedisVersion(redisTemplate); // 第一步,创建 StreamMessageListenerContainer 容器 // 创建 options 配置 StreamMessageListenerContainer.StreamMessageListenerContainerOptions> containerOptions = StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder() .batchSize(10) // 一次性最多拉取多少条消息 .targetType(String.class) // 目标类型。统一使用 String,通过自己封装的 AbstractStreamMessageListener 去反序列化 .build(); // 创建 container 对象 StreamMessageListenerContainer> container = StreamMessageListenerContainer.create(redisMQTemplate.getRedisTemplate().getRequiredConnectionFactory(), containerOptions); // 第二步,注册监听器,消费对应的 Stream 主题 String consumerName = buildConsumerName(); listeners.parallelStream().forEach(listener -> { log.info("[redisStreamMessageListenerContainer][开始注册 StreamKey({}) 对应的监听器({})]", listener.getStreamKey(), listener.getClass().getName()); // 创建 listener 对应的消费者分组 try { redisTemplate.opsForStream().createGroup(listener.getStreamKey(), listener.getGroup()); } catch (Exception ignore) { } // 设置 listener 对应的 redisTemplate listener.setRedisMQTemplate(redisMQTemplate); // 创建 Consumer 对象 Consumer consumer = Consumer.from(listener.getGroup(), consumerName); // 设置 Consumer 消费进度,以最小消费进度为准 StreamOffset streamOffset = StreamOffset.create(listener.getStreamKey(), ReadOffset.lastConsumed()); // 设置 Consumer 监听 StreamMessageListenerContainer.StreamReadRequestBuilder builder = StreamMessageListenerContainer.StreamReadRequest .builder(streamOffset).consumer(consumer) .autoAcknowledge(false) // 不自动 ack .cancelOnError(throwable -> false); // 默认配置,发生异常就取消消费,显然不符合预期;因此,我们设置为 false container.register(builder.build(), listener); log.info("[redisStreamMessageListenerContainer][完成注册 StreamKey({}) 对应的监听器({})]", listener.getStreamKey(), listener.getClass().getName()); }); return container; } /** * 构建消费者名字,使用本地 IP + 进程编号的方式。 * 参考自 RocketMQ clientId 的实现 * * @return 消费者名字 */ private static String buildConsumerName() { return String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID()); } /** * 校验 Redis 版本号,是否满足最低的版本号要求! */ private static void checkRedisVersion(RedisTemplate redisTemplate) { // 获得 Redis 版本 Properties info = redisTemplate.execute((RedisCallback) RedisServerCommands::info); String version = MapUtil.getStr(info, "redis_version"); // 校验最低版本必须大于等于 5.0.0 int majorVersion = Integer.parseInt(StrUtil.subBefore(version, '.', false)); if (majorVersion < 5) { throw new IllegalStateException(StrUtil.format("您当前的 Redis 版本为 {},小于最低要求的 5.0.0 版本!" + "请参考 {} 文档进行安装。", version, DocumentEnum.REDIS_INSTALL.getUrl())); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mq/src/main/java/co/yixiang/yshop/framework/mq/redis/config/YshopRedisMQProducerAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.mq.redis.config; import co.yixiang.yshop.framework.mq.redis.core.RedisMQTemplate; import co.yixiang.yshop.framework.mq.redis.core.interceptor.RedisMessageInterceptor; import co.yixiang.yshop.framework.redis.config.YshopRedisAutoConfiguration; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.data.redis.core.StringRedisTemplate; import java.util.List; /** * Redis 消息队列 Producer 配置类 * * @author yshop */ @Slf4j @AutoConfiguration(after = YshopRedisAutoConfiguration.class) public class YshopRedisMQProducerAutoConfiguration { @Bean public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate, List interceptors) { RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate); // 添加拦截器 interceptors.forEach(redisMQTemplate::addInterceptor); return redisMQTemplate; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mq/src/main/java/co/yixiang/yshop/framework/mq/redis/core/RedisMQTemplate.java ================================================ package co.yixiang.yshop.framework.mq.redis.core; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.framework.mq.redis.core.interceptor.RedisMessageInterceptor; import co.yixiang.yshop.framework.mq.redis.core.message.AbstractRedisMessage; import co.yixiang.yshop.framework.mq.redis.core.pubsub.AbstractRedisChannelMessage; import co.yixiang.yshop.framework.mq.redis.core.stream.AbstractRedisStreamMessage; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.data.redis.connection.stream.RecordId; import org.springframework.data.redis.connection.stream.StreamRecords; import org.springframework.data.redis.core.RedisTemplate; import java.util.ArrayList; import java.util.List; /** * Redis MQ 操作模板类 * * @author yshop */ @AllArgsConstructor public class RedisMQTemplate { @Getter private final RedisTemplate redisTemplate; /** * 拦截器数组 */ @Getter private final List interceptors = new ArrayList<>(); /** * 发送 Redis 消息,基于 Redis pub/sub 实现 * * @param message 消息 */ public void send(T message) { try { sendMessageBefore(message); // 发送消息 redisTemplate.convertAndSend(message.getChannel(), JsonUtils.toJsonString(message)); } finally { sendMessageAfter(message); } } /** * 发送 Redis 消息,基于 Redis Stream 实现 * * @param message 消息 * @return 消息记录的编号对象 */ public RecordId send(T message) { try { sendMessageBefore(message); // 发送消息 return redisTemplate.opsForStream().add(StreamRecords.newRecord() .ofObject(JsonUtils.toJsonString(message)) // 设置内容 .withStreamKey(message.getStreamKey())); // 设置 stream key } finally { sendMessageAfter(message); } } /** * 添加拦截器 * * @param interceptor 拦截器 */ public void addInterceptor(RedisMessageInterceptor interceptor) { interceptors.add(interceptor); } private void sendMessageBefore(AbstractRedisMessage message) { // 正序 interceptors.forEach(interceptor -> interceptor.sendMessageBefore(message)); } private void sendMessageAfter(AbstractRedisMessage message) { // 倒序 for (int i = interceptors.size() - 1; i >= 0; i--) { interceptors.get(i).sendMessageAfter(message); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mq/src/main/java/co/yixiang/yshop/framework/mq/redis/core/interceptor/RedisMessageInterceptor.java ================================================ package co.yixiang.yshop.framework.mq.redis.core.interceptor; import co.yixiang.yshop.framework.mq.redis.core.message.AbstractRedisMessage; /** * {@link AbstractRedisMessage} 消息拦截器 * 通过拦截器,作为插件机制,实现拓展。 * 例如说,多租户场景下的 MQ 消息处理 * * @author yshop */ public interface RedisMessageInterceptor { default void sendMessageBefore(AbstractRedisMessage message) { } default void sendMessageAfter(AbstractRedisMessage message) { } default void consumeMessageBefore(AbstractRedisMessage message) { } default void consumeMessageAfter(AbstractRedisMessage message) { } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mq/src/main/java/co/yixiang/yshop/framework/mq/redis/core/job/RedisPendingMessageResendJob.java ================================================ package co.yixiang.yshop.framework.mq.redis.core.job; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.mq.redis.core.RedisMQTemplate; import co.yixiang.yshop.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.data.domain.Range; import org.springframework.data.redis.connection.stream.*; import org.springframework.data.redis.core.StreamOperations; import org.springframework.scheduling.annotation.Scheduled; import java.util.List; import java.util.Map; import java.util.Objects; /** * 这个任务用于处理,crash 之后的消费者未消费完的消息 */ @Slf4j @AllArgsConstructor public class RedisPendingMessageResendJob { private static final String LOCK_KEY = "redis:pending:msg:lock"; /** * 消息超时时间,默认 5 分钟 * * 1. 超时的消息才会被重新投递 * 2. 由于定时任务 1 分钟一次,消息超时后不会被立即重投,极端情况下消息5分钟过期后,再等 1 分钟才会被扫瞄到 */ private static final int EXPIRE_TIME = 5 * 60; private final List> listeners; private final RedisMQTemplate redisTemplate; private final String groupName; private final RedissonClient redissonClient; /** * 一分钟执行一次,这里选择每分钟的35秒执行,是为了避免整点任务过多的问题 */ @Scheduled(cron = "35 * * * * ?") public void messageResend() { RLock lock = redissonClient.getLock(LOCK_KEY); // 尝试加锁 if (lock.tryLock()) { try { execute(); } catch (Exception ex) { log.error("[messageResend][执行异常]", ex); } finally { lock.unlock(); } } } /** * 执行清理逻辑 * */ private void execute() { StreamOperations ops = redisTemplate.getRedisTemplate().opsForStream(); listeners.forEach(listener -> { PendingMessagesSummary pendingMessagesSummary = Objects.requireNonNull(ops.pending(listener.getStreamKey(), groupName)); // 每个消费者的 pending 队列消息数量 Map pendingMessagesPerConsumer = pendingMessagesSummary.getPendingMessagesPerConsumer(); pendingMessagesPerConsumer.forEach((consumerName, pendingMessageCount) -> { log.info("[processPendingMessage][消费者({}) 消息数量({})]", consumerName, pendingMessageCount); // 每个消费者的 pending消息的详情信息 PendingMessages pendingMessages = ops.pending(listener.getStreamKey(), Consumer.from(groupName, consumerName), Range.unbounded(), pendingMessageCount); if (pendingMessages.isEmpty()) { return; } pendingMessages.forEach(pendingMessage -> { // 获取消息上一次传递到 consumer 的时间, long lastDelivery = pendingMessage.getElapsedTimeSinceLastDelivery().getSeconds(); if (lastDelivery < EXPIRE_TIME){ return; } // 获取指定 id 的消息体 List> records = ops.range(listener.getStreamKey(), Range.of(Range.Bound.inclusive(pendingMessage.getIdAsString()), Range.Bound.inclusive(pendingMessage.getIdAsString()))); if (CollUtil.isEmpty(records)) { return; } // 重新投递消息 redisTemplate.getRedisTemplate().opsForStream().add(StreamRecords.newRecord() .ofObject(records.get(0).getValue()) // 设置内容 .withStreamKey(listener.getStreamKey())); // ack 消息消费完成 redisTemplate.getRedisTemplate().opsForStream().acknowledge(groupName, records.get(0)); log.info("[processPendingMessage][消息({})重新投递成功]", records.get(0).getId()); }); }); }); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mq/src/main/java/co/yixiang/yshop/framework/mq/redis/core/message/AbstractRedisMessage.java ================================================ package co.yixiang.yshop.framework.mq.redis.core.message; import lombok.Data; import java.util.HashMap; import java.util.Map; /** * Redis 消息抽象基类 * * @author yshop */ @Data public abstract class AbstractRedisMessage { /** * 头 */ private Map headers = new HashMap<>(); public String getHeader(String key) { return headers.get(key); } public void addHeader(String key, String value) { headers.put(key, value); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mq/src/main/java/co/yixiang/yshop/framework/mq/redis/core/pubsub/AbstractRedisChannelMessage.java ================================================ package co.yixiang.yshop.framework.mq.redis.core.pubsub; import co.yixiang.yshop.framework.mq.redis.core.message.AbstractRedisMessage; import com.fasterxml.jackson.annotation.JsonIgnore; /** * Redis Channel Message 抽象类 * * @author yshop */ public abstract class AbstractRedisChannelMessage extends AbstractRedisMessage { /** * 获得 Redis Channel,默认使用类名 * * @return Channel */ @JsonIgnore // 避免序列化。原因是,Redis 发布 Channel 消息的时候,已经会指定。 public String getChannel() { return getClass().getSimpleName(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mq/src/main/java/co/yixiang/yshop/framework/mq/redis/core/pubsub/AbstractRedisChannelMessageListener.java ================================================ package co.yixiang.yshop.framework.mq.redis.core.pubsub; import cn.hutool.core.util.TypeUtil; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.framework.mq.redis.core.RedisMQTemplate; import co.yixiang.yshop.framework.mq.redis.core.interceptor.RedisMessageInterceptor; import co.yixiang.yshop.framework.mq.redis.core.message.AbstractRedisMessage; import lombok.Setter; import lombok.SneakyThrows; import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.connection.MessageListener; import java.lang.reflect.Type; import java.util.List; /** * Redis Pub/Sub 监听器抽象类,用于实现广播消费 * * @param 消息类型。一定要填写噢,不然会报错 * * @author yshop */ public abstract class AbstractRedisChannelMessageListener implements MessageListener { /** * 消息类型 */ private final Class messageType; /** * Redis Channel */ private final String channel; /** * RedisMQTemplate */ @Setter private RedisMQTemplate redisMQTemplate; @SneakyThrows protected AbstractRedisChannelMessageListener() { this.messageType = getMessageClass(); this.channel = messageType.getDeclaredConstructor().newInstance().getChannel(); } /** * 获得 Sub 订阅的 Redis Channel 通道 * * @return channel */ public final String getChannel() { return channel; } @Override public final void onMessage(Message message, byte[] bytes) { T messageObj = JsonUtils.parseObject(message.getBody(), messageType); try { consumeMessageBefore(messageObj); // 消费消息 this.onMessage(messageObj); } finally { consumeMessageAfter(messageObj); } } /** * 处理消息 * * @param message 消息 */ public abstract void onMessage(T message); /** * 通过解析类上的泛型,获得消息类型 * * @return 消息类型 */ @SuppressWarnings("unchecked") private Class getMessageClass() { Type type = TypeUtil.getTypeArgument(getClass(), 0); if (type == null) { throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName())); } return (Class) type; } private void consumeMessageBefore(AbstractRedisMessage message) { assert redisMQTemplate != null; List interceptors = redisMQTemplate.getInterceptors(); // 正序 interceptors.forEach(interceptor -> interceptor.consumeMessageBefore(message)); } private void consumeMessageAfter(AbstractRedisMessage message) { assert redisMQTemplate != null; List interceptors = redisMQTemplate.getInterceptors(); // 倒序 for (int i = interceptors.size() - 1; i >= 0; i--) { interceptors.get(i).consumeMessageAfter(message); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mq/src/main/java/co/yixiang/yshop/framework/mq/redis/core/stream/AbstractRedisStreamMessage.java ================================================ package co.yixiang.yshop.framework.mq.redis.core.stream; import co.yixiang.yshop.framework.mq.redis.core.message.AbstractRedisMessage; import com.fasterxml.jackson.annotation.JsonIgnore; /** * Redis Stream Message 抽象类 * * @author yshop */ public abstract class AbstractRedisStreamMessage extends AbstractRedisMessage { /** * 获得 Redis Stream Key,默认使用类名 * * @return Channel */ @JsonIgnore // 避免序列化 public String getStreamKey() { return getClass().getSimpleName(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mq/src/main/java/co/yixiang/yshop/framework/mq/redis/core/stream/AbstractRedisStreamMessageListener.java ================================================ package co.yixiang.yshop.framework.mq.redis.core.stream; import cn.hutool.core.util.TypeUtil; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.framework.mq.redis.core.RedisMQTemplate; import co.yixiang.yshop.framework.mq.redis.core.interceptor.RedisMessageInterceptor; import co.yixiang.yshop.framework.mq.redis.core.message.AbstractRedisMessage; import lombok.Getter; import lombok.Setter; import lombok.SneakyThrows; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.connection.stream.ObjectRecord; import org.springframework.data.redis.stream.StreamListener; import java.lang.reflect.Type; import java.util.List; /** * Redis Stream 监听器抽象类,用于实现集群消费 * * @param 消息类型。一定要填写噢,不然会报错 * * @author yshop */ public abstract class AbstractRedisStreamMessageListener implements StreamListener> { /** * 消息类型 */ private final Class messageType; /** * Redis Channel */ @Getter private final String streamKey; /** * Redis 消费者分组,默认使用 spring.application.name 名字 */ @Value("${spring.application.name}") @Getter private String group; /** * RedisMQTemplate */ @Setter private RedisMQTemplate redisMQTemplate; @SneakyThrows protected AbstractRedisStreamMessageListener() { this.messageType = getMessageClass(); this.streamKey = messageType.getDeclaredConstructor().newInstance().getStreamKey(); } @Override public void onMessage(ObjectRecord message) { // 消费消息 T messageObj = JsonUtils.parseObject(message.getValue(), messageType); try { consumeMessageBefore(messageObj); // 消费消息 this.onMessage(messageObj); // ack 消息消费完成 redisMQTemplate.getRedisTemplate().opsForStream().acknowledge(group, message); // TODO yshop:需要额外考虑以下几个点: // 1. 处理异常的情况 // 2. 发送日志;以及事务的结合 // 3. 消费日志;以及通用的幂等性 // 4. 消费失败的重试,https://zhuanlan.zhihu.com/p/60501638 } finally { consumeMessageAfter(messageObj); } } /** * 处理消息 * * @param message 消息 */ public abstract void onMessage(T message); /** * 通过解析类上的泛型,获得消息类型 * * @return 消息类型 */ @SuppressWarnings("unchecked") private Class getMessageClass() { Type type = TypeUtil.getTypeArgument(getClass(), 0); if (type == null) { throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName())); } return (Class) type; } private void consumeMessageBefore(AbstractRedisMessage message) { assert redisMQTemplate != null; List interceptors = redisMQTemplate.getInterceptors(); // 正序 interceptors.forEach(interceptor -> interceptor.consumeMessageBefore(message)); } private void consumeMessageAfter(AbstractRedisMessage message) { assert redisMQTemplate != null; List interceptors = redisMQTemplate.getInterceptors(); // 倒序 for (int i = interceptors.size() - 1; i >= 0; i--) { interceptors.get(i).consumeMessageAfter(message); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ co.yixiang.yshop.framework.mq.redis.config.YshopRedisMQProducerAutoConfiguration co.yixiang.yshop.framework.mq.redis.config.YshopRedisMQConsumerAutoConfiguration co.yixiang.yshop.framework.mq.rabbitmq.config.YshopRabbitMQAutoConfiguration ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/pom.xml ================================================ co.yixiang.boot yshop-framework ${revision} 4.0.0 yshop-spring-boot-starter-mybatis jar ${project.artifactId} 数据库连接池、多数据源、事务、MyBatis 拓展 https://gitee.com/guchengwuyue/yshop-drink co.yixiang.boot yshop-common co.yixiang.boot yshop-spring-boot-starter-web provided com.mysql mysql-connector-j com.oracle.database.jdbc ojdbc8 true org.postgresql postgresql true com.microsoft.sqlserver mssql-jdbc true com.dameng DmJdbcDriver18 true com.alibaba druid-spring-boot-3-starter com.baomidou mybatis-plus-spring-boot3-starter com.baomidou dynamic-datasource-spring-boot3-starter org.springframework.boot spring-boot-starter-undertow com.github.yulichang mybatis-plus-join-boot-starter com.fhs-opensource easy-trans-spring-boot-starter com.fhs-opensource easy-trans-mybatis-plus-extend ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/java/co/yixiang/yshop/framework/datasource/config/YshopDataSourceAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.datasource.config; import co.yixiang.yshop.framework.datasource.core.filter.DruidAdRemoveFilter; import com.alibaba.druid.spring.boot3.autoconfigure.properties.DruidStatProperties; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.transaction.annotation.EnableTransactionManagement; /** * 数据库配置类 * * @author yshop */ @AutoConfiguration @EnableTransactionManagement(proxyTargetClass = true) // 启动事务管理 @EnableConfigurationProperties(DruidStatProperties.class) public class YshopDataSourceAutoConfiguration { /** * 创建 DruidAdRemoveFilter 过滤器,过滤 common.js 的广告 */ @Bean @ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled", havingValue = "true") public FilterRegistrationBean druidAdRemoveFilterFilter(DruidStatProperties properties) { // 获取 druid web 监控页面的参数 DruidStatProperties.StatViewServlet config = properties.getStatViewServlet(); // 提取 common.js 的配置路径 String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*"; String commonJsPattern = pattern.replaceAll("\\*", "js/common.js"); // 创建 DruidAdRemoveFilter Bean FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new DruidAdRemoveFilter()); registrationBean.addUrlPatterns(commonJsPattern); return registrationBean; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/java/co/yixiang/yshop/framework/datasource/core/enums/DataSourceEnum.java ================================================ package co.yixiang.yshop.framework.datasource.core.enums; /** * 对应于多数据源中不同数据源配置 * * 通过在方法上,使用 {@link com.baomidou.dynamic.datasource.annotation.DS} 注解,设置使用的数据源。 * 注意,默认是 {@link #MASTER} 数据源 * * 对应官方文档为 http://dynamic-datasource.com/guide/customize/Annotation.html */ public interface DataSourceEnum { /** * 主库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Master} 注解 */ String MASTER = "master"; /** * 从库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Slave} 注解 */ String SLAVE = "slave"; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/java/co/yixiang/yshop/framework/datasource/core/filter/DruidAdRemoveFilter.java ================================================ package co.yixiang.yshop.framework.datasource.core.filter; import com.alibaba.druid.util.Utils; import org.springframework.web.filter.OncePerRequestFilter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; /** * Druid 底部广告过滤器 * * @author yshop */ public class DruidAdRemoveFilter extends OncePerRequestFilter { /** * common.js 的路径 */ private static final String COMMON_JS_ILE_PATH = "support/http/resources/js/common.js"; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { chain.doFilter(request, response); // 重置缓冲区,响应头不会被重置 response.resetBuffer(); // 获取 common.js String text = Utils.readFromResource(COMMON_JS_ILE_PATH); // 正则替换 banner, 除去底部的广告信息 text = text.replaceAll("
", ""); text = text.replaceAll("powered.*?shrek.wang", ""); response.getWriter().write(text); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/java/co/yixiang/yshop/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java ================================================ package co.yixiang.yshop.framework.mybatis.config; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.util.collection.SetUtils; import co.yixiang.yshop.framework.mybatis.core.enums.SqlConstants; import co.yixiang.yshop.framework.mybatis.core.util.JdbcUtils; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.annotation.IdType; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.env.EnvironmentPostProcessor; import org.springframework.core.env.ConfigurableEnvironment; import java.util.Set; /** * 当 IdType 为 {@link IdType#NONE} 时,根据 PRIMARY 数据源所使用的数据库,自动设置 * * @author yshop */ @Slf4j public class IdTypeEnvironmentPostProcessor implements EnvironmentPostProcessor { private static final String ID_TYPE_KEY = "mybatis-plus.global-config.db-config.id-type"; private static final String DATASOURCE_DYNAMIC_KEY = "spring.datasource.dynamic"; private static final String QUARTZ_JOB_STORE_DRIVER_KEY = "spring.quartz.properties.org.quartz.jobStore.driverDelegateClass"; private static final Set INPUT_ID_TYPES = SetUtils.asSet(DbType.ORACLE, DbType.ORACLE_12C, DbType.POSTGRE_SQL, DbType.KINGBASE_ES, DbType.DB2, DbType.H2); @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { // 如果获取不到 DbType,则不进行处理 DbType dbType = getDbType(environment); if (dbType == null) { return; } // 设置 Quartz JobStore 对应的 Driver // TODO yshop:暂时没有找到特别合适的地方,先放在这里 setJobStoreDriverIfPresent(environment, dbType); // 初始化 SQL 静态变量 SqlConstants.init(dbType); // 如果非 NONE,则不进行处理 IdType idType = getIdType(environment); if (idType != IdType.NONE) { return; } // 情况一,用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库 if (INPUT_ID_TYPES.contains(dbType)) { setIdType(environment, IdType.INPUT); return; } // 情况二,自增 ID,适合 MySQL 等直接自增的数据库 setIdType(environment, IdType.AUTO); } public IdType getIdType(ConfigurableEnvironment environment) { return environment.getProperty(ID_TYPE_KEY, IdType.class); } public void setIdType(ConfigurableEnvironment environment, IdType idType) { environment.getSystemProperties().put(ID_TYPE_KEY, idType); log.info("[setIdType][修改 MyBatis Plus 的 idType 为({})]", idType); } public void setJobStoreDriverIfPresent(ConfigurableEnvironment environment, DbType dbType) { String driverClass = environment.getProperty(QUARTZ_JOB_STORE_DRIVER_KEY); if (StrUtil.isNotEmpty(driverClass)) { return; } // 根据 dbType 类型,获取对应的 driverClass switch (dbType) { case POSTGRE_SQL: driverClass = "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate"; break; case ORACLE: case ORACLE_12C: driverClass = "org.quartz.impl.jdbcjobstore.oracle.OracleDelegate"; break; case SQL_SERVER: case SQL_SERVER2005: driverClass = "org.quartz.impl.jdbcjobstore.MSSQLDelegate"; break; } // 设置 driverClass 变量 if (StrUtil.isNotEmpty(driverClass)) { environment.getSystemProperties().put(QUARTZ_JOB_STORE_DRIVER_KEY, driverClass); } } public static DbType getDbType(ConfigurableEnvironment environment) { String primary = environment.getProperty(DATASOURCE_DYNAMIC_KEY + "." + "primary"); if (StrUtil.isEmpty(primary)) { return null; } String url = environment.getProperty(DATASOURCE_DYNAMIC_KEY + ".datasource." + primary + ".url"); if (StrUtil.isEmpty(url)) { return null; } return JdbcUtils.getDbType(url); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/java/co/yixiang/yshop/framework/mybatis/config/YshopMybatisAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.mybatis.config; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.mybatis.core.handler.DefaultDBFieldHandler; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator; import com.baomidou.mybatisplus.extension.incrementer.*; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.apache.ibatis.annotations.Mapper; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.core.env.ConfigurableEnvironment; /** * MyBaits 配置类 * * @author yshop */ @AutoConfiguration(before = MybatisPlusAutoConfiguration.class) // 目的:先于 MyBatis Plus 自动配置,避免 @MapperScan 可能扫描不到 Mapper 打印 warn 日志 @MapperScan(value = "${yshop.info.base-package}", annotationClass = Mapper.class, lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试 public class YshopMybatisAutoConfiguration { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件 return mybatisPlusInterceptor; } @Bean public MetaObjectHandler defaultMetaObjectHandler(){ return new DefaultDBFieldHandler(); // 自动填充参数类 } @Bean @ConditionalOnProperty(prefix = "mybatis-plus.global-config.db-config", name = "id-type", havingValue = "INPUT") public IKeyGenerator keyGenerator(ConfigurableEnvironment environment) { DbType dbType = IdTypeEnvironmentPostProcessor.getDbType(environment); if (dbType != null) { switch (dbType) { case POSTGRE_SQL: return new PostgreKeyGenerator(); case ORACLE: case ORACLE_12C: return new OracleKeyGenerator(); case H2: return new H2KeyGenerator(); case KINGBASE_ES: return new KingbaseKeyGenerator(); case DM: return new DmKeyGenerator(); } } // 找不到合适的 IKeyGenerator 实现类 throw new IllegalArgumentException(StrUtil.format("DbType{} 找不到合适的 IKeyGenerator 实现类", dbType)); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/java/co/yixiang/yshop/framework/mybatis/core/dataobject/BaseDO.java ================================================ package co.yixiang.yshop.framework.mybatis.core.dataobject; import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableLogic; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fhs.core.trans.vo.TransPojo; import lombok.Data; import org.apache.ibatis.type.JdbcType; import java.io.Serializable; import java.time.LocalDateTime; /** * 基础实体对象 * * 为什么实现 {@link TransPojo} 接口? * 因为使用 Easy-Trans TransType.SIMPLE 模式,集成 MyBatis Plus 查询 * * @author yshop */ @Data @JsonIgnoreProperties(value = "transMap") // 由于 Easy-Trans 会添加 transMap 属性,避免 Jackson 在 Spring Cache 反序列化报错 public abstract class BaseDO implements Serializable, TransPojo { /** * 创建时间 */ @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; /** * 最后更新时间 */ @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; /** * 创建者,目前使用 SysUser 的 id 编号 * * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。 */ @TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR) private String creator; /** * 更新者,目前使用 SysUser 的 id 编号 * * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。 */ @TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.VARCHAR) private String updater; /** * 是否删除 */ @TableLogic private Boolean deleted; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/java/co/yixiang/yshop/framework/mybatis/core/enums/SqlConstants.java ================================================ package co.yixiang.yshop.framework.mybatis.core.enums; import com.baomidou.mybatisplus.annotation.DbType; /** * SQL相关常量类 * * @author yshop */ public class SqlConstants { /** * 数据库的类型 */ public static DbType DB_TYPE; public static void init(DbType dbType) { DB_TYPE = dbType; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/java/co/yixiang/yshop/framework/mybatis/core/handler/DefaultDBFieldHandler.java ================================================ package co.yixiang.yshop.framework.mybatis.core.handler; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.framework.web.core.util.WebFrameworkUtils; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import org.apache.ibatis.reflection.MetaObject; import java.time.LocalDateTime; import java.util.Objects; /** * 通用参数填充实现类 * * 如果没有显式的对通用参数进行赋值,这里会对通用参数进行填充、赋值 * * @author hexiaowu */ public class DefaultDBFieldHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) { BaseDO baseDO = (BaseDO) metaObject.getOriginalObject(); LocalDateTime current = LocalDateTime.now(); // 创建时间为空,则以当前时间为插入时间 if (Objects.isNull(baseDO.getCreateTime())) { baseDO.setCreateTime(current); } // 更新时间为空,则以当前时间为更新时间 if (Objects.isNull(baseDO.getUpdateTime())) { baseDO.setUpdateTime(current); } Long userId = WebFrameworkUtils.getLoginUserId(); // 当前登录用户不为空,创建人为空,则当前登录用户为创建人 if (Objects.nonNull(userId) && Objects.isNull(baseDO.getCreator())) { baseDO.setCreator(userId.toString()); } // 当前登录用户不为空,更新人为空,则当前登录用户为更新人 if (Objects.nonNull(userId) && Objects.isNull(baseDO.getUpdater())) { baseDO.setUpdater(userId.toString()); } } } @Override public void updateFill(MetaObject metaObject) { // 更新时间为空,则以当前时间为更新时间 Object modifyTime = getFieldValByName("updateTime", metaObject); if (Objects.isNull(modifyTime)) { setFieldValByName("updateTime", LocalDateTime.now(), metaObject); } // 当前登录用户不为空,更新人为空,则当前登录用户为更新人 Object modifier = getFieldValByName("updater", metaObject); Long userId = WebFrameworkUtils.getLoginUserId(); if (Objects.nonNull(userId) && Objects.isNull(modifier)) { setFieldValByName("updater", userId.toString(), metaObject); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/java/co/yixiang/yshop/framework/mybatis/core/mapper/BaseMapperX.java ================================================ package co.yixiang.yshop.framework.mybatis.core.mapper; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.pojo.SortablePageParam; import co.yixiang.yshop.framework.common.pojo.SortingField; import co.yixiang.yshop.framework.mybatis.core.enums.SqlConstants; import co.yixiang.yshop.framework.mybatis.core.util.MyBatisUtils; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.support.SFunction; import com.baomidou.mybatisplus.extension.toolkit.Db; import com.github.yulichang.base.MPJBaseMapper; import com.github.yulichang.interfaces.MPJBaseJoin; import com.github.yulichang.wrapper.MPJLambdaWrapper; import org.apache.ibatis.annotations.Param; import java.util.Collection; import java.util.List; import java.util.Objects; /** * 在 MyBatis Plus 的 BaseMapper 的基础上拓展,提供更多的能力 * * 1. {@link BaseMapper} 为 MyBatis Plus 的基础接口,提供基础的 CRUD 能力 * 2. {@link MPJBaseMapper} 为 MyBatis Plus Join 的基础接口,提供连表 Join 能力 */ public interface BaseMapperX extends MPJBaseMapper { default PageResult selectPage(SortablePageParam pageParam, @Param("ew") Wrapper queryWrapper) { return selectPage(pageParam, pageParam.getSortingFields(), queryWrapper); } default PageResult selectPage(PageParam pageParam, @Param("ew") Wrapper queryWrapper) { return selectPage(pageParam, null, queryWrapper); } default PageResult selectPage(PageParam pageParam, Collection sortingFields, @Param("ew") Wrapper queryWrapper) { // 特殊:不分页,直接查询全部 if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) { List list = selectList(queryWrapper); return new PageResult<>(list, (long) list.size()); } // MyBatis Plus 查询 IPage mpPage = MyBatisUtils.buildPage(pageParam, sortingFields); selectPage(mpPage, queryWrapper); // 转换返回 return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); } default PageResult selectJoinPage(PageParam pageParam, Class clazz, MPJLambdaWrapper lambdaWrapper) { // 特殊:不分页,直接查询全部 if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageNo())) { List list = selectJoinList(clazz, lambdaWrapper); return new PageResult<>(list, (long) list.size()); } // MyBatis Plus Join 查询 IPage mpPage = MyBatisUtils.buildPage(pageParam); mpPage = selectJoinPage(mpPage, clazz, lambdaWrapper); // 转换返回 return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); } default PageResult selectJoinPage(PageParam pageParam, Class resultTypeClass, MPJBaseJoin joinQueryWrapper) { IPage mpPage = MyBatisUtils.buildPage(pageParam); selectJoinPage(mpPage, resultTypeClass, joinQueryWrapper); // 转换返回 return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); } default T selectOne(String field, Object value) { return selectOne(new QueryWrapper().eq(field, value)); } default T selectOne(SFunction field, Object value) { return selectOne(new LambdaQueryWrapper().eq(field, value)); } default T selectOne(String field1, Object value1, String field2, Object value2) { return selectOne(new QueryWrapper().eq(field1, value1).eq(field2, value2)); } default T selectOne(SFunction field1, Object value1, SFunction field2, Object value2) { return selectOne(new LambdaQueryWrapper().eq(field1, value1).eq(field2, value2)); } default T selectOne(SFunction field1, Object value1, SFunction field2, Object value2, SFunction field3, Object value3) { return selectOne(new LambdaQueryWrapper().eq(field1, value1).eq(field2, value2) .eq(field3, value3)); } default Long selectCount() { return selectCount(new QueryWrapper<>()); } default Long selectCount(String field, Object value) { return selectCount(new QueryWrapper().eq(field, value)); } default Long selectCount(SFunction field, Object value) { return selectCount(new LambdaQueryWrapper().eq(field, value)); } default List selectList() { return selectList(new QueryWrapper<>()); } default List selectList(String field, Object value) { return selectList(new QueryWrapper().eq(field, value)); } default List selectList(SFunction field, Object value) { return selectList(new LambdaQueryWrapper().eq(field, value)); } default List selectList(String field, Collection values) { if (CollUtil.isEmpty(values)) { return CollUtil.newArrayList(); } return selectList(new QueryWrapper().in(field, values)); } default List selectList(SFunction field, Collection values) { if (CollUtil.isEmpty(values)) { return CollUtil.newArrayList(); } return selectList(new LambdaQueryWrapper().in(field, values)); } @Deprecated default List selectList(SFunction leField, SFunction geField, Object value) { return selectList(new LambdaQueryWrapper().le(leField, value).ge(geField, value)); } default List selectList(SFunction field1, Object value1, SFunction field2, Object value2) { return selectList(new LambdaQueryWrapper().eq(field1, value1).eq(field2, value2)); } /** * 批量插入,适合大量数据插入 * * @param entities 实体们 */ default Boolean insertBatch(Collection entities) { // 特殊:SQL Server 批量插入后,获取 id 会报错,因此通过循环处理 if (Objects.equals(SqlConstants.DB_TYPE, DbType.SQL_SERVER)) { entities.forEach(this::insert); return CollUtil.isNotEmpty(entities); } return Db.saveBatch(entities); } /** * 批量插入,适合大量数据插入 * * @param entities 实体们 * @param size 插入数量 Db.saveBatch 默认为 1000 */ default Boolean insertBatch(Collection entities, int size) { // 特殊:SQL Server 批量插入后,获取 id 会报错,因此通过循环处理 if (Objects.equals(SqlConstants.DB_TYPE, DbType.SQL_SERVER)) { entities.forEach(this::insert); return CollUtil.isNotEmpty(entities); } return Db.saveBatch(entities, size); } default int updateBatch(T update) { return update(update, new QueryWrapper<>()); } default Boolean updateBatch(Collection entities) { return Db.updateBatchById(entities); } default Boolean updateBatch(Collection entities, int size) { return Db.updateBatchById(entities, size); } default Boolean insertOrUpdate(T entity) { return Db.saveOrUpdate(entity); } default Boolean insertOrUpdateBatch(Collection collection) { return Db.saveOrUpdateBatch(collection); } default int delete(String field, String value) { return delete(new QueryWrapper().eq(field, value)); } default int delete(SFunction field, Object value) { return delete(new LambdaQueryWrapper().eq(field, value)); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/java/co/yixiang/yshop/framework/mybatis/core/query/LambdaQueryWrapperX.java ================================================ package co.yixiang.yshop.framework.mybatis.core.query; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjectUtil; import co.yixiang.yshop.framework.common.util.collection.ArrayUtils; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.support.SFunction; import org.springframework.util.StringUtils; import java.util.Collection; /** * 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能: *

* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。 * * @param 数据类型 */ public class LambdaQueryWrapperX extends LambdaQueryWrapper { public LambdaQueryWrapperX likeIfPresent(SFunction column, String val) { if (StringUtils.hasText(val)) { return (LambdaQueryWrapperX) super.like(column, val); } return this; } public LambdaQueryWrapperX inIfPresent(SFunction column, Collection values) { if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) { return (LambdaQueryWrapperX) super.in(column, values); } return this; } public LambdaQueryWrapperX inIfPresent(SFunction column, Object... values) { if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) { return (LambdaQueryWrapperX) super.in(column, values); } return this; } public LambdaQueryWrapperX eqIfPresent(SFunction column, Object val) { if (ObjectUtil.isNotEmpty(val)) { return (LambdaQueryWrapperX) super.eq(column, val); } return this; } public LambdaQueryWrapperX neIfPresent(SFunction column, Object val) { if (ObjectUtil.isNotEmpty(val)) { return (LambdaQueryWrapperX) super.ne(column, val); } return this; } public LambdaQueryWrapperX gtIfPresent(SFunction column, Object val) { if (val != null) { return (LambdaQueryWrapperX) super.gt(column, val); } return this; } public LambdaQueryWrapperX geIfPresent(SFunction column, Object val) { if (val != null) { return (LambdaQueryWrapperX) super.ge(column, val); } return this; } public LambdaQueryWrapperX ltIfPresent(SFunction column, Object val) { if (val != null) { return (LambdaQueryWrapperX) super.lt(column, val); } return this; } public LambdaQueryWrapperX leIfPresent(SFunction column, Object val) { if (val != null) { return (LambdaQueryWrapperX) super.le(column, val); } return this; } public LambdaQueryWrapperX betweenIfPresent(SFunction column, Object val1, Object val2) { if (val1 != null && val2 != null) { return (LambdaQueryWrapperX) super.between(column, val1, val2); } if (val1 != null) { return (LambdaQueryWrapperX) ge(column, val1); } if (val2 != null) { return (LambdaQueryWrapperX) le(column, val2); } return this; } public LambdaQueryWrapperX betweenIfPresent(SFunction column, Object[] values) { Object val1 = ArrayUtils.get(values, 0); Object val2 = ArrayUtils.get(values, 1); return betweenIfPresent(column, val1, val2); } // ========== 重写父类方法,方便链式调用 ========== @Override public LambdaQueryWrapperX eq(boolean condition, SFunction column, Object val) { super.eq(condition, column, val); return this; } @Override public LambdaQueryWrapperX eq(SFunction column, Object val) { super.eq(column, val); return this; } @Override public LambdaQueryWrapperX orderByDesc(SFunction column) { super.orderByDesc(true, column); return this; } @Override public LambdaQueryWrapperX last(String lastSql) { super.last(lastSql); return this; } @Override public LambdaQueryWrapperX in(SFunction column, Collection coll) { super.in(column, coll); return this; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/java/co/yixiang/yshop/framework/mybatis/core/query/MPJLambdaWrapperX.java ================================================ package co.yixiang.yshop.framework.mybatis.core.query; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjectUtil; import co.yixiang.yshop.framework.common.util.collection.ArrayUtils; import com.baomidou.mybatisplus.core.toolkit.support.SFunction; import com.github.yulichang.toolkit.MPJWrappers; import com.github.yulichang.wrapper.MPJLambdaWrapper; import org.springframework.util.StringUtils; import java.util.Collection; import java.util.function.Consumer; /** * 拓展 MyBatis Plus Join QueryWrapper 类,主要增加如下功能: *

* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。 * * @param 数据类型 */ public class MPJLambdaWrapperX extends MPJLambdaWrapper { public MPJLambdaWrapperX likeIfPresent(SFunction column, String val) { MPJWrappers.lambdaJoin().like(column, val); if (StringUtils.hasText(val)) { return (MPJLambdaWrapperX) super.like(column, val); } return this; } public MPJLambdaWrapperX inIfPresent(SFunction column, Collection values) { if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) { return (MPJLambdaWrapperX) super.in(column, values); } return this; } public MPJLambdaWrapperX inIfPresent(SFunction column, Object... values) { if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) { return (MPJLambdaWrapperX) super.in(column, values); } return this; } public MPJLambdaWrapperX eqIfPresent(SFunction column, Object val) { if (ObjectUtil.isNotEmpty(val)) { return (MPJLambdaWrapperX) super.eq(column, val); } return this; } public MPJLambdaWrapperX neIfPresent(SFunction column, Object val) { if (ObjectUtil.isNotEmpty(val)) { return (MPJLambdaWrapperX) super.ne(column, val); } return this; } public MPJLambdaWrapperX gtIfPresent(SFunction column, Object val) { if (val != null) { return (MPJLambdaWrapperX) super.gt(column, val); } return this; } public MPJLambdaWrapperX geIfPresent(SFunction column, Object val) { if (val != null) { return (MPJLambdaWrapperX) super.ge(column, val); } return this; } public MPJLambdaWrapperX ltIfPresent(SFunction column, Object val) { if (val != null) { return (MPJLambdaWrapperX) super.lt(column, val); } return this; } public MPJLambdaWrapperX leIfPresent(SFunction column, Object val) { if (val != null) { return (MPJLambdaWrapperX) super.le(column, val); } return this; } public MPJLambdaWrapperX betweenIfPresent(SFunction column, Object val1, Object val2) { if (val1 != null && val2 != null) { return (MPJLambdaWrapperX) super.between(column, val1, val2); } if (val1 != null) { return (MPJLambdaWrapperX) ge(column, val1); } if (val2 != null) { return (MPJLambdaWrapperX) le(column, val2); } return this; } public MPJLambdaWrapperX betweenIfPresent(SFunction column, Object[] values) { Object val1 = ArrayUtils.get(values, 0); Object val2 = ArrayUtils.get(values, 1); return betweenIfPresent(column, val1, val2); } // ========== 重写父类方法,方便链式调用 ========== @Override public MPJLambdaWrapperX eq(boolean condition, SFunction column, Object val) { super.eq(condition, column, val); return this; } @Override public MPJLambdaWrapperX eq(SFunction column, Object val) { super.eq(column, val); return this; } @Override public MPJLambdaWrapperX orderByDesc(SFunction column) { //noinspection unchecked super.orderByDesc(true, column); return this; } @Override public MPJLambdaWrapperX last(String lastSql) { super.last(lastSql); return this; } @Override public MPJLambdaWrapperX in(SFunction column, Collection coll) { super.in(column, coll); return this; } @Override public MPJLambdaWrapperX selectAll(Class clazz) { super.selectAll(clazz); return this; } @Override public MPJLambdaWrapperX selectAll(Class clazz, String prefix) { super.selectAll(clazz, prefix); return this; } @Override public MPJLambdaWrapperX selectAs(SFunction column, String alias) { super.selectAs(column, alias); return this; } @Override public MPJLambdaWrapperX selectAs(String column, SFunction alias) { super.selectAs(column, alias); return this; } @Override public MPJLambdaWrapperX selectAs(SFunction column, SFunction alias) { super.selectAs(column, alias); return this; } @Override public MPJLambdaWrapperX selectAs(String index, SFunction column, SFunction alias) { super.selectAs(index, column, alias); return this; } @Override public MPJLambdaWrapperX selectAsClass(Class source, Class tag) { super.selectAsClass(source, tag); return this; } @Override public MPJLambdaWrapperX selectSub(Class clazz, Consumer> consumer, SFunction alias) { super.selectSub(clazz, consumer, alias); return this; } @Override public MPJLambdaWrapperX selectSub(Class clazz, String st, Consumer> consumer, SFunction alias) { super.selectSub(clazz, st, consumer, alias); return this; } @Override public MPJLambdaWrapperX selectCount(SFunction column) { super.selectCount(column); return this; } @Override public MPJLambdaWrapperX selectCount(Object column, String alias) { super.selectCount(column, alias); return this; } @Override public MPJLambdaWrapperX selectCount(Object column, SFunction alias) { super.selectCount(column, alias); return this; } @Override public MPJLambdaWrapperX selectCount(SFunction column, String alias) { super.selectCount(column, alias); return this; } @Override public MPJLambdaWrapperX selectCount(SFunction column, SFunction alias) { super.selectCount(column, alias); return this; } @Override public MPJLambdaWrapperX selectSum(SFunction column) { super.selectSum(column); return this; } @Override public MPJLambdaWrapperX selectSum(SFunction column, String alias) { super.selectSum(column, alias); return this; } @Override public MPJLambdaWrapperX selectSum(SFunction column, SFunction alias) { super.selectSum(column, alias); return this; } @Override public MPJLambdaWrapperX selectMax(SFunction column) { super.selectMax(column); return this; } @Override public MPJLambdaWrapperX selectMax(SFunction column, String alias) { super.selectMax(column, alias); return this; } @Override public MPJLambdaWrapperX selectMax(SFunction column, SFunction alias) { super.selectMax(column, alias); return this; } @Override public MPJLambdaWrapperX selectMin(SFunction column) { super.selectMin(column); return this; } @Override public MPJLambdaWrapperX selectMin(SFunction column, String alias) { super.selectMin(column, alias); return this; } @Override public MPJLambdaWrapperX selectMin(SFunction column, SFunction alias) { super.selectMin(column, alias); return this; } @Override public MPJLambdaWrapperX selectAvg(SFunction column) { super.selectAvg(column); return this; } @Override public MPJLambdaWrapperX selectAvg(SFunction column, String alias) { super.selectAvg(column, alias); return this; } @Override public MPJLambdaWrapperX selectAvg(SFunction column, SFunction alias) { super.selectAvg(column, alias); return this; } @Override public MPJLambdaWrapperX selectLen(SFunction column) { super.selectLen(column); return this; } @Override public MPJLambdaWrapperX selectLen(SFunction column, String alias) { super.selectLen(column, alias); return this; } @Override public MPJLambdaWrapperX selectLen(SFunction column, SFunction alias) { super.selectLen(column, alias); return this; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/java/co/yixiang/yshop/framework/mybatis/core/query/QueryWrapperX.java ================================================ package co.yixiang.yshop.framework.mybatis.core.query; import cn.hutool.core.lang.Assert; import co.yixiang.yshop.framework.mybatis.core.enums.SqlConstants; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.toolkit.ArrayUtils; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import org.springframework.util.StringUtils; import java.util.Collection; /** * 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能: * * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。 * * @param 数据类型 */ public class QueryWrapperX extends QueryWrapper { public QueryWrapperX likeIfPresent(String column, String val) { if (StringUtils.hasText(val)) { return (QueryWrapperX) super.like(column, val); } return this; } public QueryWrapperX inIfPresent(String column, Collection values) { if (!CollectionUtils.isEmpty(values)) { return (QueryWrapperX) super.in(column, values); } return this; } public QueryWrapperX inIfPresent(String column, Object... values) { if (!ArrayUtils.isEmpty(values)) { return (QueryWrapperX) super.in(column, values); } return this; } public QueryWrapperX eqIfPresent(String column, Object val) { if (val != null) { return (QueryWrapperX) super.eq(column, val); } return this; } public QueryWrapperX neIfPresent(String column, Object val) { if (val != null) { return (QueryWrapperX) super.ne(column, val); } return this; } public QueryWrapperX gtIfPresent(String column, Object val) { if (val != null) { return (QueryWrapperX) super.gt(column, val); } return this; } public QueryWrapperX geIfPresent(String column, Object val) { if (val != null) { return (QueryWrapperX) super.ge(column, val); } return this; } public QueryWrapperX ltIfPresent(String column, Object val) { if (val != null) { return (QueryWrapperX) super.lt(column, val); } return this; } public QueryWrapperX leIfPresent(String column, Object val) { if (val != null) { return (QueryWrapperX) super.le(column, val); } return this; } public QueryWrapperX betweenIfPresent(String column, Object val1, Object val2) { if (val1 != null && val2 != null) { return (QueryWrapperX) super.between(column, val1, val2); } if (val1 != null) { return (QueryWrapperX) ge(column, val1); } if (val2 != null) { return (QueryWrapperX) le(column, val2); } return this; } public QueryWrapperX betweenIfPresent(String column, Object[] values) { if (values!= null && values.length != 0 && values[0] != null && values[1] != null) { return (QueryWrapperX) super.between(column, values[0], values[1]); } if (values!= null && values.length != 0 && values[0] != null) { return (QueryWrapperX) ge(column, values[0]); } if (values!= null && values.length != 0 && values[1] != null) { return (QueryWrapperX) le(column, values[1]); } return this; } // ========== 重写父类方法,方便链式调用 ========== @Override public QueryWrapperX eq(boolean condition, String column, Object val) { super.eq(condition, column, val); return this; } @Override public QueryWrapperX eq(String column, Object val) { super.eq(column, val); return this; } @Override public QueryWrapperX orderByDesc(String column) { super.orderByDesc(true, column); return this; } @Override public QueryWrapperX last(String lastSql) { super.last(lastSql); return this; } @Override public QueryWrapperX in(String column, Collection coll) { super.in(column, coll); return this; } /** * 设置只返回最后一条 * * TODO yshop:不是完美解,需要在思考下。如果使用多数据源,并且数据源是多种类型时,可能会存在问题:实现之返回一条的语法不同 * * @return this */ public QueryWrapperX limitN(int n) { Assert.notNull(SqlConstants.DB_TYPE, "获取不到数据库的类型"); switch (SqlConstants.DB_TYPE) { case ORACLE: case ORACLE_12C: super.le("ROWNUM", n); break; case SQL_SERVER: case SQL_SERVER2005: super.select("TOP " + n + " *"); // 由于 SQL Server 是通过 SELECT TOP 1 实现限制一条,所以只好使用 * 查询剩余字段 break; default: super.last("LIMIT " + n); } return this; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/java/co/yixiang/yshop/framework/mybatis/core/type/EncryptTypeHandler.java ================================================ package co.yixiang.yshop.framework.mybatis.core.type; import cn.hutool.core.lang.Assert; import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.symmetric.AES; import cn.hutool.extra.spring.SpringUtil; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; /** * 字段字段的 TypeHandler 实现类,基于 {@link cn.hutool.crypto.symmetric.AES} 实现 * 可通过 jasypt.encryptor.password 配置项,设置密钥 * * @author yshop */ public class EncryptTypeHandler extends BaseTypeHandler { private static final String ENCRYPTOR_PROPERTY_NAME = "mybatis-plus.encryptor.password"; private static AES aes; @Override public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, encrypt(parameter)); } @Override public String getNullableResult(ResultSet rs, String columnName) throws SQLException { String value = rs.getString(columnName); return decrypt(value); } @Override public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { String value = rs.getString(columnIndex); return decrypt(value); } @Override public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { String value = cs.getString(columnIndex); return decrypt(value); } private static String decrypt(String value) { if (value == null) { return null; } return getEncryptor().decryptStr(value); } public static String encrypt(String rawValue) { if (rawValue == null) { return null; } return getEncryptor().encryptBase64(rawValue); } private static AES getEncryptor() { if (aes != null) { return aes; } // 构建 AES String password = SpringUtil.getProperty(ENCRYPTOR_PROPERTY_NAME); Assert.notEmpty(password, "配置项({}) 不能为空", ENCRYPTOR_PROPERTY_NAME); aes = SecureUtil.aes(password.getBytes()); return aes; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/java/co/yixiang/yshop/framework/mybatis/core/type/IntegerListTypeHandler.java ================================================ package co.yixiang.yshop.framework.mybatis.core.type; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.common.util.string.StrUtils; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedJdbcTypes; import org.apache.ibatis.type.MappedTypes; import org.apache.ibatis.type.TypeHandler; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; /** * List 的类型转换器实现类,对应数据库的 varchar 类型 * * @author jason */ @MappedJdbcTypes(JdbcType.VARCHAR) @MappedTypes(List.class) public class IntegerListTypeHandler implements TypeHandler> { private static final String COMMA = ","; @Override public void setParameter(PreparedStatement ps, int i, List strings, JdbcType jdbcType) throws SQLException { ps.setString(i, CollUtil.join(strings, COMMA)); } @Override public List getResult(ResultSet rs, String columnName) throws SQLException { String value = rs.getString(columnName); return getResult(value); } @Override public List getResult(ResultSet rs, int columnIndex) throws SQLException { String value = rs.getString(columnIndex); return getResult(value); } @Override public List getResult(CallableStatement cs, int columnIndex) throws SQLException { String value = cs.getString(columnIndex); return getResult(value); } private List getResult(String value) { if (value == null) { return null; } return StrUtils.splitToInteger(value, COMMA); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/java/co/yixiang/yshop/framework/mybatis/core/type/JsonLongSetTypeHandler.java ================================================ package co.yixiang.yshop.framework.mybatis.core.type; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; import com.fasterxml.jackson.core.type.TypeReference; import java.util.Set; /** * 参考 {@link com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler} 实现 * 在我们将字符串反序列化为 Set 并且泛型为 Long 时,如果每个元素的数值太小,会被处理成 Integer 类型,导致可能存在隐性的 BUG。 * * 例如说哦,SysUserDO 的 postIds 属性 * * @author yshop */ public class JsonLongSetTypeHandler extends AbstractJsonTypeHandler { private static final TypeReference> TYPE_REFERENCE = new TypeReference>(){}; @Override protected Object parse(String json) { return JsonUtils.parseObject(json, TYPE_REFERENCE); } @Override protected String toJson(Object obj) { return JsonUtils.toJsonString(obj); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/java/co/yixiang/yshop/framework/mybatis/core/type/LongListTypeHandler.java ================================================ package co.yixiang.yshop.framework.mybatis.core.type; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.common.util.string.StrUtils; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedJdbcTypes; import org.apache.ibatis.type.MappedTypes; import org.apache.ibatis.type.TypeHandler; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; /** * List 的类型转换器实现类,对应数据库的 varchar 类型 * * @author yshop */ @MappedJdbcTypes(JdbcType.VARCHAR) @MappedTypes(List.class) public class LongListTypeHandler implements TypeHandler> { private static final String COMMA = ","; @Override public void setParameter(PreparedStatement ps, int i, List strings, JdbcType jdbcType) throws SQLException { // 设置占位符 ps.setString(i, CollUtil.join(strings, COMMA)); } @Override public List getResult(ResultSet rs, String columnName) throws SQLException { String value = rs.getString(columnName); return getResult(value); } @Override public List getResult(ResultSet rs, int columnIndex) throws SQLException { String value = rs.getString(columnIndex); return getResult(value); } @Override public List getResult(CallableStatement cs, int columnIndex) throws SQLException { String value = cs.getString(columnIndex); return getResult(value); } private List getResult(String value) { if (value == null) { return null; } return StrUtils.splitToLong(value, COMMA); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/java/co/yixiang/yshop/framework/mybatis/core/type/StringListTypeHandler.java ================================================ package co.yixiang.yshop.framework.mybatis.core.type; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedJdbcTypes; import org.apache.ibatis.type.MappedTypes; import org.apache.ibatis.type.TypeHandler; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; /** * List 的类型转换器实现类,对应数据库的 varchar 类型 * * @author 永不言败 * @since 2022 3/23 12:50:15 */ @MappedJdbcTypes(JdbcType.VARCHAR) @MappedTypes(List.class) public class StringListTypeHandler implements TypeHandler> { private static final String COMMA = ","; @Override public void setParameter(PreparedStatement ps, int i, List strings, JdbcType jdbcType) throws SQLException { // 设置占位符 ps.setString(i, CollUtil.join(strings, COMMA)); } @Override public List getResult(ResultSet rs, String columnName) throws SQLException { String value = rs.getString(columnName); return getResult(value); } @Override public List getResult(ResultSet rs, int columnIndex) throws SQLException { String value = rs.getString(columnIndex); return getResult(value); } @Override public List getResult(CallableStatement cs, int columnIndex) throws SQLException { String value = cs.getString(columnIndex); return getResult(value); } private List getResult(String value) { if (value == null) { return null; } return StrUtil.splitTrim(value, COMMA); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/java/co/yixiang/yshop/framework/mybatis/core/util/JdbcUtils.java ================================================ package co.yixiang.yshop.framework.mybatis.core.util; import com.baomidou.mybatisplus.annotation.DbType; import java.sql.Connection; import java.sql.DriverManager; /** * JDBC 工具类 * * @author yshop */ public class JdbcUtils { /** * 判断连接是否正确 * * @param url 数据源连接 * @param username 账号 * @param password 密码 * @return 是否正确 */ public static boolean isConnectionOK(String url, String username, String password) { try (Connection ignored = DriverManager.getConnection(url, username, password)) { return true; } catch (Exception ex) { return false; } } /** * 获得 URL 对应的 DB 类型 * * @param url URL * @return DB 类型 */ public static DbType getDbType(String url) { String name = com.alibaba.druid.util.JdbcUtils.getDbType(url, null); return DbType.getDbType(name); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/java/co/yixiang/yshop/framework/mybatis/core/util/MyBatisUtils.java ================================================ package co.yixiang.yshop.framework.mybatis.core.util; import cn.hutool.core.collection.CollectionUtil; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.SortingField; import com.baomidou.mybatisplus.core.metadata.OrderItem; import com.baomidou.mybatisplus.core.toolkit.StringPool; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; /** * MyBatis 工具类 */ public class MyBatisUtils { private static final String MYSQL_ESCAPE_CHARACTER = "`"; public static Page buildPage(PageParam pageParam) { return buildPage(pageParam, null); } public static Page buildPage(PageParam pageParam, Collection sortingFields) { // 页码 + 数量 Page page = new Page<>(pageParam.getPageNo(), pageParam.getPageSize()); // 排序字段 if (!CollectionUtil.isEmpty(sortingFields)) { page.addOrder(sortingFields.stream().map(sortingField -> SortingField.ORDER_ASC.equals(sortingField.getOrder()) ? OrderItem.asc(sortingField.getField()) : OrderItem.desc(sortingField.getField())) .collect(Collectors.toList())); } return page; } /** * 将拦截器添加到链中 * 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置 * * @param interceptor 链 * @param inner 拦截器 * @param index 位置 */ public static void addInterceptor(MybatisPlusInterceptor interceptor, InnerInterceptor inner, int index) { List inners = new ArrayList<>(interceptor.getInterceptors()); inners.add(index, inner); interceptor.setInterceptors(inners); } /** * 获得 Table 对应的表名 * * 兼容 MySQL 转义表名 `t_xxx` * * @param table 表 * @return 去除转移字符后的表名 */ public static String getTableName(Table table) { String tableName = table.getName(); if (tableName.startsWith(MYSQL_ESCAPE_CHARACTER) && tableName.endsWith(MYSQL_ESCAPE_CHARACTER)) { tableName = tableName.substring(1, tableName.length() - 1); } return tableName; } /** * 构建 Column 对象 * * @param tableName 表名 * @param tableAlias 别名 * @param column 字段名 * @return Column 对象 */ public static Column buildColumn(String tableName, Alias tableAlias, String column) { if (tableAlias != null) { tableName = tableAlias.getName(); } return new Column(tableName + StringPool.DOT + column); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/java/co/yixiang/yshop/framework/translate/config/YshopTranslateAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.translate.config; import co.yixiang.yshop.framework.translate.core.TranslateUtils; import com.fhs.trans.service.impl.TransService; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.context.annotation.Bean; @AutoConfiguration public class YshopTranslateAutoConfiguration { @Bean @SuppressWarnings({"InstantiationOfUtilityClass", "SpringJavaInjectionPointsAutowiringInspection"}) public TranslateUtils translateUtils(TransService transService) { TranslateUtils.init(transService); return new TranslateUtils(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/java/co/yixiang/yshop/framework/translate/core/TranslateUtils.java ================================================ package co.yixiang.yshop.framework.translate.core; import cn.hutool.core.collection.CollUtil; import com.fhs.core.trans.vo.VO; import com.fhs.trans.service.impl.TransService; import java.util.List; /** * VO 数据翻译 Utils * * @author yshop */ public class TranslateUtils { private static TransService transService; public static void init(TransService transService) { TranslateUtils.transService = transService; } /** * 数据翻译 * * 使用场景:无法使用 @TransMethodResult 注解的场景,只能通过手动触发翻译 * * @param data 数据 * @return 翻译结果 */ public static List translate(List data) { if (CollUtil.isNotEmpty((data))) { transService.transBatch(data); } return data; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ co.yixiang.yshop.framework.datasource.config.YshopDataSourceAutoConfiguration co.yixiang.yshop.framework.mybatis.config.YshopMybatisAutoConfiguration co.yixiang.yshop.framework.translate.config.YshopTranslateAutoConfiguration ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-mybatis/src/main/resources/META-INF/spring.factories ================================================ org.springframework.boot.env.EnvironmentPostProcessor=\ co.yixiang.yshop.framework.mybatis.config.IdTypeEnvironmentPostProcessor ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/pom.xml ================================================ co.yixiang.boot yshop-framework ${revision} 4.0.0 yshop-spring-boot-starter-protection jar ${project.artifactId} 服务保证,提供分布式锁、幂等、限流、熔断等等功能 https://gitee.com/guchengwuyue/yshop-drink co.yixiang.boot yshop-spring-boot-starter-web provided co.yixiang.boot yshop-spring-boot-starter-redis com.baomidou lock4j-redisson-spring-boot-starter true ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/java/co/yixiang/yshop/framework/idempotent/config/YshopIdempotentConfiguration.java ================================================ package co.yixiang.yshop.framework.idempotent.config; import co.yixiang.yshop.framework.idempotent.core.aop.IdempotentAspect; import co.yixiang.yshop.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; import co.yixiang.yshop.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver; import co.yixiang.yshop.framework.idempotent.core.keyresolver.IdempotentKeyResolver; import co.yixiang.yshop.framework.idempotent.core.keyresolver.impl.UserIdempotentKeyResolver; import co.yixiang.yshop.framework.idempotent.core.redis.IdempotentRedisDAO; import org.springframework.boot.autoconfigure.AutoConfiguration; import co.yixiang.yshop.framework.redis.config.YshopRedisAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.data.redis.core.StringRedisTemplate; import java.util.List; @AutoConfiguration(after = YshopRedisAutoConfiguration.class) public class YshopIdempotentConfiguration { @Bean public IdempotentAspect idempotentAspect(List keyResolvers, IdempotentRedisDAO idempotentRedisDAO) { return new IdempotentAspect(keyResolvers, idempotentRedisDAO); } @Bean public IdempotentRedisDAO idempotentRedisDAO(StringRedisTemplate stringRedisTemplate) { return new IdempotentRedisDAO(stringRedisTemplate); } // ========== 各种 IdempotentKeyResolver Bean ========== @Bean public DefaultIdempotentKeyResolver defaultIdempotentKeyResolver() { return new DefaultIdempotentKeyResolver(); } @Bean public UserIdempotentKeyResolver userIdempotentKeyResolver() { return new UserIdempotentKeyResolver(); } @Bean public ExpressionIdempotentKeyResolver expressionIdempotentKeyResolver() { return new ExpressionIdempotentKeyResolver(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/java/co/yixiang/yshop/framework/idempotent/core/annotation/Idempotent.java ================================================ package co.yixiang.yshop.framework.idempotent.core.annotation; import co.yixiang.yshop.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; import co.yixiang.yshop.framework.idempotent.core.keyresolver.IdempotentKeyResolver; import co.yixiang.yshop.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver; import co.yixiang.yshop.framework.idempotent.core.keyresolver.impl.UserIdempotentKeyResolver; 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; /** * 幂等注解 * * @author yshop */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Idempotent { /** * 幂等的超时时间,默认为 1 秒 * * 注意,如果执行时间超过它,请求还是会进来 */ int timeout() default 1; /** * 时间单位,默认为 SECONDS 秒 */ TimeUnit timeUnit() default TimeUnit.SECONDS; /** * 提示信息,正在执行中的提示 */ String message() default "重复请求,请稍后重试"; /** * 使用的 Key 解析器 * * @see DefaultIdempotentKeyResolver 全局级别 * @see UserIdempotentKeyResolver 用户级别 * @see ExpressionIdempotentKeyResolver 自定义表达式,通过 {@link #keyArg()} 计算 */ Class keyResolver() default DefaultIdempotentKeyResolver.class; /** * 使用的 Key 参数 */ String keyArg() default ""; /** * 删除 Key,当发生异常时候 * * 问题:为什么发生异常时,需要删除 Key 呢? * 回答:发生异常时,说明业务发生错误,此时需要删除 Key,避免下次请求无法正常执行。 * * 问题:为什么不搞 deleteWhenSuccess 执行成功时,需要删除 Key 呢? * 回答:这种情况下,本质上是分布式锁,推荐使用 @Lock4j 注解 */ boolean deleteKeyWhenException() default true; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/java/co/yixiang/yshop/framework/idempotent/core/aop/IdempotentAspect.java ================================================ package co.yixiang.yshop.framework.idempotent.core.aop; import co.yixiang.yshop.framework.common.exception.ServiceException; import co.yixiang.yshop.framework.common.exception.enums.GlobalErrorCodeConstants; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.framework.idempotent.core.annotation.Idempotent; import co.yixiang.yshop.framework.idempotent.core.keyresolver.IdempotentKeyResolver; import co.yixiang.yshop.framework.idempotent.core.redis.IdempotentRedisDAO; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.util.Assert; import java.util.List; import java.util.Map; /** * 拦截声明了 {@link Idempotent} 注解的方法,实现幂等操作 * * @author yshop */ @Aspect @Slf4j public class IdempotentAspect { /** * IdempotentKeyResolver 集合 */ private final Map, IdempotentKeyResolver> keyResolvers; private final IdempotentRedisDAO idempotentRedisDAO; public IdempotentAspect(List keyResolvers, IdempotentRedisDAO idempotentRedisDAO) { this.keyResolvers = CollectionUtils.convertMap(keyResolvers, IdempotentKeyResolver::getClass); this.idempotentRedisDAO = idempotentRedisDAO; } @Around(value = "@annotation(idempotent)") public Object aroundPointCut(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable { // 获得 IdempotentKeyResolver IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver()); Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver"); // 解析 Key String key = keyResolver.resolver(joinPoint, idempotent); // 1. 锁定 Key boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit()); // 锁定失败,抛出异常 if (!success) { log.info("[aroundPointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs()); throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), idempotent.message()); } // 2. 执行逻辑 try { return joinPoint.proceed(); } catch (Throwable throwable) { // 3. 异常时,删除 Key // 参考美团 GTIS 思路:https://tech.meituan.com/2016/09/29/distributed-system-mutually-exclusive-idempotence-cerberus-gtis.html if (idempotent.deleteKeyWhenException()) { idempotentRedisDAO.delete(key); } throw throwable; } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/java/co/yixiang/yshop/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java ================================================ package co.yixiang.yshop.framework.idempotent.core.keyresolver; import co.yixiang.yshop.framework.idempotent.core.annotation.Idempotent; import org.aspectj.lang.JoinPoint; /** * 幂等 Key 解析器接口 * * @author yshop */ public interface IdempotentKeyResolver { /** * 解析一个 Key * * @param idempotent 幂等注解 * @param joinPoint AOP 切面 * @return Key */ String resolver(JoinPoint joinPoint, Idempotent idempotent); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/java/co/yixiang/yshop/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java ================================================ package co.yixiang.yshop.framework.idempotent.core.keyresolver.impl; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.SecureUtil; import co.yixiang.yshop.framework.idempotent.core.annotation.Idempotent; import co.yixiang.yshop.framework.idempotent.core.keyresolver.IdempotentKeyResolver; import org.aspectj.lang.JoinPoint; /** * 默认(全局级别)幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key * * 为了避免 Key 过长,使用 MD5 进行“压缩” * * @author yshop */ public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver { @Override public String resolver(JoinPoint joinPoint, Idempotent idempotent) { String methodName = joinPoint.getSignature().toString(); String argsStr = StrUtil.join(",", joinPoint.getArgs()); return SecureUtil.md5(methodName + argsStr); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/java/co/yixiang/yshop/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java ================================================ package co.yixiang.yshop.framework.idempotent.core.keyresolver.impl; import cn.hutool.core.util.ArrayUtil; import co.yixiang.yshop.framework.idempotent.core.annotation.Idempotent; import co.yixiang.yshop.framework.idempotent.core.keyresolver.IdempotentKeyResolver; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import java.lang.reflect.Method; /** * 基于 Spring EL 表达式, * * @author yshop */ public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver { private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); private final ExpressionParser expressionParser = new SpelExpressionParser(); @Override public String resolver(JoinPoint joinPoint, Idempotent idempotent) { // 获得被拦截方法参数名列表 Method method = getMethod(joinPoint); Object[] args = joinPoint.getArgs(); String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method); // 准备 Spring EL 表达式解析的上下文 StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); if (ArrayUtil.isNotEmpty(parameterNames)) { for (int i = 0; i < parameterNames.length; i++) { evaluationContext.setVariable(parameterNames[i], args[i]); } } // 解析参数 Expression expression = expressionParser.parseExpression(idempotent.keyArg()); return expression.getValue(evaluationContext, String.class); } private static Method getMethod(JoinPoint point) { // 处理,声明在类上的情况 MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); if (!method.getDeclaringClass().isInterface()) { return method; } // 处理,声明在接口上的情况 try { return point.getTarget().getClass().getDeclaredMethod( point.getSignature().getName(), method.getParameterTypes()); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/java/co/yixiang/yshop/framework/idempotent/core/keyresolver/impl/UserIdempotentKeyResolver.java ================================================ package co.yixiang.yshop.framework.idempotent.core.keyresolver.impl; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.SecureUtil; import co.yixiang.yshop.framework.idempotent.core.annotation.Idempotent; import co.yixiang.yshop.framework.idempotent.core.keyresolver.IdempotentKeyResolver; import co.yixiang.yshop.framework.web.core.util.WebFrameworkUtils; import org.aspectj.lang.JoinPoint; /** * 用户级别的幂等 Key 解析器,使用方法名 + 方法参数 + userId + userType,组装成一个 Key * * 为了避免 Key 过长,使用 MD5 进行“压缩” * * @author yshop */ public class UserIdempotentKeyResolver implements IdempotentKeyResolver { @Override public String resolver(JoinPoint joinPoint, Idempotent idempotent) { String methodName = joinPoint.getSignature().toString(); String argsStr = StrUtil.join(",", joinPoint.getArgs()); Long userId = WebFrameworkUtils.getLoginUserId(); Integer userType = WebFrameworkUtils.getLoginUserType(); return SecureUtil.md5(methodName + argsStr + userId + userType); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/java/co/yixiang/yshop/framework/idempotent/core/redis/IdempotentRedisDAO.java ================================================ package co.yixiang.yshop.framework.idempotent.core.redis; import lombok.AllArgsConstructor; import org.springframework.data.redis.core.StringRedisTemplate; import java.util.concurrent.TimeUnit; /** * 幂等 Redis DAO * * @author yshop */ @AllArgsConstructor public class IdempotentRedisDAO { /** * 幂等操作 * * KEY 格式:idempotent:%s // 参数为 uuid * VALUE 格式:String * 过期时间:不固定 */ private static final String IDEMPOTENT = "idempotent:%s"; private final StringRedisTemplate redisTemplate; public Boolean setIfAbsent(String key, long timeout, TimeUnit timeUnit) { String redisKey = formatKey(key); return redisTemplate.opsForValue().setIfAbsent(redisKey, "", timeout, timeUnit); } public void delete(String key) { String redisKey = formatKey(key); redisTemplate.delete(redisKey); } private static String formatKey(String key) { return String.format(IDEMPOTENT, key); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/java/co/yixiang/yshop/framework/lock4j/config/YshopLock4jConfiguration.java ================================================ package co.yixiang.yshop.framework.lock4j.config; import co.yixiang.yshop.framework.lock4j.core.DefaultLockFailureStrategy; import com.baomidou.lock.spring.boot.autoconfigure.LockAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.context.annotation.Bean; @AutoConfiguration(before = LockAutoConfiguration.class) @ConditionalOnClass(name = "com.baomidou.lock.annotation.Lock4j") public class YshopLock4jConfiguration { @Bean public DefaultLockFailureStrategy lockFailureStrategy() { return new DefaultLockFailureStrategy(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/java/co/yixiang/yshop/framework/lock4j/core/DefaultLockFailureStrategy.java ================================================ package co.yixiang.yshop.framework.lock4j.core; import co.yixiang.yshop.framework.common.exception.ServiceException; import co.yixiang.yshop.framework.common.exception.enums.GlobalErrorCodeConstants; import com.baomidou.lock.LockFailureStrategy; import lombok.extern.slf4j.Slf4j; import java.lang.reflect.Method; /** * 自定义获取锁失败策略,抛出 {@link ServiceException} 异常 */ @Slf4j public class DefaultLockFailureStrategy implements LockFailureStrategy { @Override public void onLockFailure(String key, Method method, Object[] arguments) { log.debug("[onLockFailure][线程:{} 获取锁失败,key:{} 获取失败:{} ]", Thread.currentThread().getName(), key, arguments); throw new ServiceException(GlobalErrorCodeConstants.LOCKED); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/java/co/yixiang/yshop/framework/lock4j/core/Lock4jRedisKeyConstants.java ================================================ package co.yixiang.yshop.framework.lock4j.core; /** * Lock4j Redis Key 枚举类 * * @author yshop */ public interface Lock4jRedisKeyConstants { /** * 分布式锁 * * KEY 格式:lock4j:%s // 参数来自 DefaultLockKeyBuilder 类 * VALUE 数据格式:HASH // RLock.class:Redisson 的 Lock 锁,使用 Hash 数据结构 * 过期时间:不固定 */ String LOCK4J = "lock4j:%s"; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/java/co/yixiang/yshop/framework/ratelimiter/config/YshopRateLimiterConfiguration.java ================================================ package co.yixiang.yshop.framework.ratelimiter.config; import co.yixiang.yshop.framework.ratelimiter.core.aop.RateLimiterAspect; import co.yixiang.yshop.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; import co.yixiang.yshop.framework.ratelimiter.core.keyresolver.impl.*; import co.yixiang.yshop.framework.ratelimiter.core.redis.RateLimiterRedisDAO; import co.yixiang.yshop.framework.redis.config.YshopRedisAutoConfiguration; import org.redisson.api.RedissonClient; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.context.annotation.Bean; import java.util.List; @AutoConfiguration(after = YshopRedisAutoConfiguration.class) public class YshopRateLimiterConfiguration { @Bean public RateLimiterAspect rateLimiterAspect(List keyResolvers, RateLimiterRedisDAO rateLimiterRedisDAO) { return new RateLimiterAspect(keyResolvers, rateLimiterRedisDAO); } @Bean @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") public RateLimiterRedisDAO rateLimiterRedisDAO(RedissonClient redissonClient) { return new RateLimiterRedisDAO(redissonClient); } // ========== 各种 RateLimiterRedisDAO Bean ========== @Bean public DefaultRateLimiterKeyResolver defaultRateLimiterKeyResolver() { return new DefaultRateLimiterKeyResolver(); } @Bean public UserRateLimiterKeyResolver userRateLimiterKeyResolver() { return new UserRateLimiterKeyResolver(); } @Bean public ClientIpRateLimiterKeyResolver clientIpRateLimiterKeyResolver() { return new ClientIpRateLimiterKeyResolver(); } @Bean public ServerNodeRateLimiterKeyResolver serverNodeRateLimiterKeyResolver() { return new ServerNodeRateLimiterKeyResolver(); } @Bean public ExpressionRateLimiterKeyResolver expressionRateLimiterKeyResolver() { return new ExpressionRateLimiterKeyResolver(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/java/co/yixiang/yshop/framework/ratelimiter/core/annotation/RateLimiter.java ================================================ package co.yixiang.yshop.framework.ratelimiter.core.annotation; import co.yixiang.yshop.framework.common.exception.enums.GlobalErrorCodeConstants; import co.yixiang.yshop.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver; import co.yixiang.yshop.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; import co.yixiang.yshop.framework.ratelimiter.core.keyresolver.impl.ClientIpRateLimiterKeyResolver; import co.yixiang.yshop.framework.ratelimiter.core.keyresolver.impl.DefaultRateLimiterKeyResolver; import co.yixiang.yshop.framework.ratelimiter.core.keyresolver.impl.ServerNodeRateLimiterKeyResolver; import co.yixiang.yshop.framework.ratelimiter.core.keyresolver.impl.UserRateLimiterKeyResolver; 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; /** * 限流注解 * * @author yshop */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimiter { /** * 限流的时间,默认为 1 秒 */ int time() default 1; /** * 时间单位,默认为 SECONDS 秒 */ TimeUnit timeUnit() default TimeUnit.SECONDS; /** * 限流次数 */ int count() default 100; /** * 提示信息,请求过快的提示 * * @see GlobalErrorCodeConstants#TOO_MANY_REQUESTS */ String message() default ""; // 为空时,使用 TOO_MANY_REQUESTS 错误提示 /** * 使用的 Key 解析器 * * @see DefaultRateLimiterKeyResolver 全局级别 * @see UserRateLimiterKeyResolver 用户 ID 级别 * @see ClientIpRateLimiterKeyResolver 用户 IP 级别 * @see ServerNodeRateLimiterKeyResolver 服务器 Node 级别 * @see ExpressionIdempotentKeyResolver 自定义表达式,通过 {@link #keyArg()} 计算 */ Class keyResolver() default DefaultRateLimiterKeyResolver.class; /** * 使用的 Key 参数 */ String keyArg() default ""; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/java/co/yixiang/yshop/framework/ratelimiter/core/aop/RateLimiterAspect.java ================================================ package co.yixiang.yshop.framework.ratelimiter.core.aop; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.exception.ServiceException; import co.yixiang.yshop.framework.common.exception.enums.GlobalErrorCodeConstants; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.framework.ratelimiter.core.annotation.RateLimiter; import co.yixiang.yshop.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; import co.yixiang.yshop.framework.ratelimiter.core.redis.RateLimiterRedisDAO; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.util.Assert; import java.util.List; import java.util.Map; /** * 拦截声明了 {@link RateLimiter} 注解的方法,实现限流操作 * * @author yshop */ @Aspect @Slf4j public class RateLimiterAspect { /** * RateLimiterKeyResolver 集合 */ private final Map, RateLimiterKeyResolver> keyResolvers; private final RateLimiterRedisDAO rateLimiterRedisDAO; public RateLimiterAspect(List keyResolvers, RateLimiterRedisDAO rateLimiterRedisDAO) { this.keyResolvers = CollectionUtils.convertMap(keyResolvers, RateLimiterKeyResolver::getClass); this.rateLimiterRedisDAO = rateLimiterRedisDAO; } @Before("@annotation(rateLimiter)") public void beforePointCut(JoinPoint joinPoint, RateLimiter rateLimiter) { // 获得 IdempotentKeyResolver 对象 RateLimiterKeyResolver keyResolver = keyResolvers.get(rateLimiter.keyResolver()); Assert.notNull(keyResolver, "找不到对应的 RateLimiterKeyResolver"); // 解析 Key String key = keyResolver.resolver(joinPoint, rateLimiter); // 获取 1 次限流 boolean success = rateLimiterRedisDAO.tryAcquire(key, rateLimiter.count(), rateLimiter.time(), rateLimiter.timeUnit()); if (!success) { log.info("[beforePointCut][方法({}) 参数({}) 请求过于频繁]", joinPoint.getSignature().toString(), joinPoint.getArgs()); String message = StrUtil.blankToDefault(rateLimiter.message(), GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getMsg()); throw new ServiceException(GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getCode(), message); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/java/co/yixiang/yshop/framework/ratelimiter/core/keyresolver/RateLimiterKeyResolver.java ================================================ package co.yixiang.yshop.framework.ratelimiter.core.keyresolver; import co.yixiang.yshop.framework.ratelimiter.core.annotation.RateLimiter; import org.aspectj.lang.JoinPoint; /** * 限流 Key 解析器接口 * * @author yshop */ public interface RateLimiterKeyResolver { /** * 解析一个 Key * * @param rateLimiter 限流注解 * @param joinPoint AOP 切面 * @return Key */ String resolver(JoinPoint joinPoint, RateLimiter rateLimiter); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/java/co/yixiang/yshop/framework/ratelimiter/core/keyresolver/impl/ClientIpRateLimiterKeyResolver.java ================================================ package co.yixiang.yshop.framework.ratelimiter.core.keyresolver.impl; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.SecureUtil; import co.yixiang.yshop.framework.common.util.servlet.ServletUtils; import co.yixiang.yshop.framework.ratelimiter.core.annotation.RateLimiter; import co.yixiang.yshop.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; import org.aspectj.lang.JoinPoint; /** * IP 级别的限流 Key 解析器,使用方法名 + 方法参数 + IP,组装成一个 Key * * 为了避免 Key 过长,使用 MD5 进行“压缩” * * @author yshop */ public class ClientIpRateLimiterKeyResolver implements RateLimiterKeyResolver { @Override public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) { String methodName = joinPoint.getSignature().toString(); String argsStr = StrUtil.join(",", joinPoint.getArgs()); String clientIp = ServletUtils.getClientIP(); return SecureUtil.md5(methodName + argsStr + clientIp); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/java/co/yixiang/yshop/framework/ratelimiter/core/keyresolver/impl/DefaultRateLimiterKeyResolver.java ================================================ package co.yixiang.yshop.framework.ratelimiter.core.keyresolver.impl; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.SecureUtil; import co.yixiang.yshop.framework.ratelimiter.core.annotation.RateLimiter; import co.yixiang.yshop.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; import org.aspectj.lang.JoinPoint; /** * 默认(全局级别)限流 Key 解析器,使用方法名 + 方法参数,组装成一个 Key * * 为了避免 Key 过长,使用 MD5 进行“压缩” * * @author yshop */ public class DefaultRateLimiterKeyResolver implements RateLimiterKeyResolver { @Override public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) { String methodName = joinPoint.getSignature().toString(); String argsStr = StrUtil.join(",", joinPoint.getArgs()); return SecureUtil.md5(methodName + argsStr); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/java/co/yixiang/yshop/framework/ratelimiter/core/keyresolver/impl/ExpressionRateLimiterKeyResolver.java ================================================ package co.yixiang.yshop.framework.ratelimiter.core.keyresolver.impl; import cn.hutool.core.util.ArrayUtil; import co.yixiang.yshop.framework.ratelimiter.core.annotation.RateLimiter; import co.yixiang.yshop.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import java.lang.reflect.Method; /** * 基于 Spring EL 表达式的 {@link RateLimiterKeyResolver} 实现类 * * @author yshop */ public class ExpressionRateLimiterKeyResolver implements RateLimiterKeyResolver { private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); private final ExpressionParser expressionParser = new SpelExpressionParser(); @Override public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) { // 获得被拦截方法参数名列表 Method method = getMethod(joinPoint); Object[] args = joinPoint.getArgs(); String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method); // 准备 Spring EL 表达式解析的上下文 StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); if (ArrayUtil.isNotEmpty(parameterNames)) { for (int i = 0; i < parameterNames.length; i++) { evaluationContext.setVariable(parameterNames[i], args[i]); } } // 解析参数 Expression expression = expressionParser.parseExpression(rateLimiter.keyArg()); return expression.getValue(evaluationContext, String.class); } private static Method getMethod(JoinPoint point) { // 处理,声明在类上的情况 MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); if (!method.getDeclaringClass().isInterface()) { return method; } // 处理,声明在接口上的情况 try { return point.getTarget().getClass().getDeclaredMethod( point.getSignature().getName(), method.getParameterTypes()); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/java/co/yixiang/yshop/framework/ratelimiter/core/keyresolver/impl/ServerNodeRateLimiterKeyResolver.java ================================================ package co.yixiang.yshop.framework.ratelimiter.core.keyresolver.impl; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.SecureUtil; import cn.hutool.system.SystemUtil; import co.yixiang.yshop.framework.ratelimiter.core.annotation.RateLimiter; import co.yixiang.yshop.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; import org.aspectj.lang.JoinPoint; /** * Server 节点级别的限流 Key 解析器,使用方法名 + 方法参数 + IP,组装成一个 Key * * 为了避免 Key 过长,使用 MD5 进行“压缩” * * @author yshop */ public class ServerNodeRateLimiterKeyResolver implements RateLimiterKeyResolver { @Override public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) { String methodName = joinPoint.getSignature().toString(); String argsStr = StrUtil.join(",", joinPoint.getArgs()); String serverNode = String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID()); return SecureUtil.md5(methodName + argsStr + serverNode); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/java/co/yixiang/yshop/framework/ratelimiter/core/keyresolver/impl/UserRateLimiterKeyResolver.java ================================================ package co.yixiang.yshop.framework.ratelimiter.core.keyresolver.impl; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.SecureUtil; import co.yixiang.yshop.framework.ratelimiter.core.annotation.RateLimiter; import co.yixiang.yshop.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; import co.yixiang.yshop.framework.web.core.util.WebFrameworkUtils; import org.aspectj.lang.JoinPoint; /** * 用户级别的限流 Key 解析器,使用方法名 + 方法参数 + userId + userType,组装成一个 Key * * 为了避免 Key 过长,使用 MD5 进行“压缩” * * @author yshop */ public class UserRateLimiterKeyResolver implements RateLimiterKeyResolver { @Override public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) { String methodName = joinPoint.getSignature().toString(); String argsStr = StrUtil.join(",", joinPoint.getArgs()); Long userId = WebFrameworkUtils.getLoginUserId(); Integer userType = WebFrameworkUtils.getLoginUserType(); return SecureUtil.md5(methodName + argsStr + userId + userType); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/java/co/yixiang/yshop/framework/ratelimiter/core/redis/RateLimiterRedisDAO.java ================================================ package co.yixiang.yshop.framework.ratelimiter.core.redis; import lombok.AllArgsConstructor; import org.redisson.api.*; import java.util.Objects; import java.util.concurrent.TimeUnit; /** * 限流 Redis DAO * * @author yshop */ @AllArgsConstructor public class RateLimiterRedisDAO { /** * 限流操作 * * KEY 格式:rate_limiter:%s // 参数为 uuid * VALUE 格式:String * 过期时间:不固定 */ private static final String RATE_LIMITER = "rate_limiter:%s"; private final RedissonClient redissonClient; public Boolean tryAcquire(String key, int count, int time, TimeUnit timeUnit) { // 1. 获得 RRateLimiter,并设置 rate 速率 RRateLimiter rateLimiter = getRRateLimiter(key, count, time, timeUnit); // 2. 尝试获取 1 个 return rateLimiter.tryAcquire(); } private static String formatKey(String key) { return String.format(RATE_LIMITER, key); } private RRateLimiter getRRateLimiter(String key, long count, int time, TimeUnit timeUnit) { String redisKey = formatKey(key); RRateLimiter rateLimiter = redissonClient.getRateLimiter(redisKey); long rateInterval = timeUnit.toSeconds(time); // 1. 如果不存在,设置 rate 速率 RateLimiterConfig config = rateLimiter.getConfig(); if (config == null) { rateLimiter.trySetRate(RateType.OVERALL, count, rateInterval, RateIntervalUnit.SECONDS); return rateLimiter; } // 2. 如果存在,并且配置相同,则直接返回 if (config.getRateType() == RateType.OVERALL && Objects.equals(config.getRate(), count) && Objects.equals(config.getRateInterval(), TimeUnit.SECONDS.toMillis(rateInterval))) { return rateLimiter; } // 3. 如果存在,并且配置不同,则进行新建 rateLimiter.setRate(RateType.OVERALL, count, rateInterval, RateIntervalUnit.SECONDS); return rateLimiter; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ co.yixiang.yshop.framework.idempotent.config.YshopIdempotentConfiguration co.yixiang.yshop.framework.lock4j.config.YshopLock4jConfiguration co.yixiang.yshop.framework.ratelimiter.config.YshopRateLimiterConfiguration ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-redis/pom.xml ================================================ co.yixiang.boot yshop-framework ${revision} 4.0.0 yshop-spring-boot-starter-redis jar ${project.artifactId} Redis 封装拓展 https://gitee.com/guchengwuyue/yshop-drink co.yixiang.boot yshop-common org.redisson redisson-spring-boot-starter org.springframework.boot spring-boot-starter-cache com.fasterxml.jackson.datatype jackson-datatype-jsr310 ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-redis/src/main/java/co/yixiang/yshop/framework/redis/config/RedissonConfig.java ================================================ //package co.yixiang.yshop.framework.redis.config; // // //import cn.hutool.core.util.StrUtil; //import org.redisson.Redisson; //import org.redisson.api.RedissonClient; //import org.redisson.config.Config; //import org.springframework.beans.factory.annotation.Value; //import org.springframework.context.annotation.Bean; //import org.springframework.context.annotation.Configuration; // //@Configuration //public class RedissonConfig { // @Value("${spring.data.redis.host}") // private String host; // // @Value("${spring.data.redis.port}") // private String port; // // @Value("${spring.data.redis.password}") // private String password; // // @Value("${spring.data.redis.database}") // private Integer database; // // // // @Bean(destroyMethod = "shutdown") // public RedissonClient redissonClient() { // Config config = new Config(); // config.useSingleServer() // .setDatabase(database) // .setAddress("redis://"+host+":"+port); // // //.setPassword(password); // if(StrUtil.isNotEmpty(password)){ // config.useSingleServer().setPassword(password); // } // // return Redisson.create(config); // } // // //} ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-redis/src/main/java/co/yixiang/yshop/framework/redis/config/YshopCacheAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.redis.config; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.redis.core.TimeoutRedisCacheManager; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.cache.CacheProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.data.redis.cache.BatchStrategies; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.cache.RedisCacheWriter; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.util.StringUtils; import java.util.Objects; import static co.yixiang.yshop.framework.redis.config.YshopRedisAutoConfiguration.buildRedisSerializer; /** * Cache 配置类,基于 Redis 实现 */ @AutoConfiguration @EnableConfigurationProperties({CacheProperties.class, YshopCacheProperties.class}) @EnableCaching public class YshopCacheAutoConfiguration { /** * RedisCacheConfiguration Bean *

* 参考 org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration 的 createConfiguration 方法 */ @Bean @Primary public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); // 设置使用 : 单冒号,而不是双 :: 冒号,避免 Redis Desktop Manager 多余空格 // 详细可见 https://blog.csdn.net/chuixue24/article/details/103928965 博客 // 再次修复单冒号,而不是双 :: 冒号问题,Issues 详情:https://gitee.com/zhijiantianya/yshop-cloud/issues/I86VY2 config = config.computePrefixWith(cacheName -> { String keyPrefix = cacheProperties.getRedis().getKeyPrefix(); if (StringUtils.hasText(keyPrefix)) { keyPrefix = keyPrefix.lastIndexOf(StrUtil.COLON) == -1 ? keyPrefix + StrUtil.COLON : keyPrefix; return keyPrefix + cacheName + StrUtil.COLON; } return cacheName + StrUtil.COLON; }); // 设置使用 JSON 序列化方式 config = config.serializeValuesWith( RedisSerializationContext.SerializationPair.fromSerializer(buildRedisSerializer())); // 设置 CacheProperties.Redis 的属性 CacheProperties.Redis redisProperties = cacheProperties.getRedis(); if (redisProperties.getTimeToLive() != null) { config = config.entryTtl(redisProperties.getTimeToLive()); } if (!redisProperties.isCacheNullValues()) { config = config.disableCachingNullValues(); } if (!redisProperties.isUseKeyPrefix()) { config = config.disableKeyPrefix(); } return config; } @Bean public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate, RedisCacheConfiguration redisCacheConfiguration, YshopCacheProperties yshopCacheProperties) { // 创建 RedisCacheWriter 对象 RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory()); RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.scan(yshopCacheProperties.getRedisScanBatchSize())); // 创建 TenantRedisCacheManager 对象 return new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-redis/src/main/java/co/yixiang/yshop/framework/redis/config/YshopCacheProperties.java ================================================ package co.yixiang.yshop.framework.redis.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; /** * Cache 配置项 * * @author Wanwan */ @ConfigurationProperties("yshop.cache") @Data @Validated public class YshopCacheProperties { /** * {@link #redisScanBatchSize} 默认值 */ private static final Integer REDIS_SCAN_BATCH_SIZE_DEFAULT = 30; /** * redis scan 一次返回数量 */ private Integer redisScanBatchSize = REDIS_SCAN_BATCH_SIZE_DEFAULT; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-redis/src/main/java/co/yixiang/yshop/framework/redis/config/YshopRedisAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.redis.config; import cn.hutool.core.util.ReflectUtil; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.redisson.spring.starter.RedissonAutoConfigurationV2; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializer; /** * Redis 配置类 */ @AutoConfiguration(before = RedissonAutoConfigurationV2.class) // 目的:使用自己定义的 RedisTemplate Bean public class YshopRedisAutoConfiguration { /** * 创建 RedisTemplate Bean,使用 JSON 序列化方式 */ @Bean public RedisTemplate redisTemplate(RedisConnectionFactory factory) { // 创建 RedisTemplate 对象 RedisTemplate template = new RedisTemplate<>(); // 设置 RedisConnection 工厂。😈 它就是实现多种 Java Redis 客户端接入的秘密工厂。感兴趣的胖友,可以自己去撸下。 template.setConnectionFactory(factory); // 使用 String 序列化方式,序列化 KEY 。 template.setKeySerializer(RedisSerializer.string()); template.setHashKeySerializer(RedisSerializer.string()); // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。 template.setValueSerializer(buildRedisSerializer()); template.setHashValueSerializer(buildRedisSerializer()); return template; } public static RedisSerializer buildRedisSerializer() { RedisSerializer json = RedisSerializer.json(); // 解决 LocalDateTime 的序列化 ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(json, "mapper"); objectMapper.registerModules(new JavaTimeModule()); return json; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-redis/src/main/java/co/yixiang/yshop/framework/redis/core/RedisKeyDefine.java ================================================ package co.yixiang.yshop.framework.redis.core; import com.fasterxml.jackson.annotation.JsonValue; import lombok.AllArgsConstructor; import lombok.Data; import lombok.Getter; import java.time.Duration; /** * Redis Key 定义类 * * @author yshop */ @Data public class RedisKeyDefine { @Getter @AllArgsConstructor public enum KeyTypeEnum { STRING("String"), LIST("List"), HASH("Hash"), SET("Set"), ZSET("Sorted Set"), STREAM("Stream"), PUBSUB("Pub/Sub"); /** * 类型 */ @JsonValue private final String type; } @Getter @AllArgsConstructor public enum TimeoutTypeEnum { FOREVER(1), // 永不超时 DYNAMIC(2), // 动态超时 FIXED(3); // 固定超时 /** * 类型 */ @JsonValue private final Integer type; } /** * Key 模板 */ private final String keyTemplate; /** * Key 类型的枚举 */ private final KeyTypeEnum keyType; /** * Value 类型 * * 如果是使用分布式锁,设置为 {@link java.util.concurrent.locks.Lock} 类型 */ private final Class valueType; /** * 超时类型 */ private final TimeoutTypeEnum timeoutType; /** * 过期时间 */ private final Duration timeout; /** * 备注 */ private final String memo; private RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class valueType, TimeoutTypeEnum timeoutType, Duration timeout) { this.memo = memo; this.keyTemplate = keyTemplate; this.keyType = keyType; this.valueType = valueType; this.timeout = timeout; this.timeoutType = timeoutType; // 添加注册表 RedisKeyRegistry.add(this); } public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class valueType, Duration timeout) { this(memo, keyTemplate, keyType, valueType, TimeoutTypeEnum.FIXED, timeout); } public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class valueType, TimeoutTypeEnum timeoutType) { this(memo, keyTemplate, keyType, valueType, timeoutType, Duration.ZERO); } /** * 格式化 Key * * 注意,内部采用 {@link String#format(String, Object...)} 实现 * * @param args 格式化的参数 * @return Key */ public String formatKey(Object... args) { return String.format(keyTemplate, args); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-redis/src/main/java/co/yixiang/yshop/framework/redis/core/RedisKeyRegistry.java ================================================ package co.yixiang.yshop.framework.redis.core; import java.util.ArrayList; import java.util.List; /** * {@link RedisKeyDefine} 注册表 */ public class RedisKeyRegistry { /** * Redis RedisKeyDefine 数组 */ private static final List DEFINES = new ArrayList<>(); public static void add(RedisKeyDefine define) { DEFINES.add(define); } public static List list() { return DEFINES; } public static int size() { return DEFINES.size(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-redis/src/main/java/co/yixiang/yshop/framework/redis/core/TimeoutRedisCacheManager.java ================================================ package co.yixiang.yshop.framework.redis.core; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.StrUtil; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.redis.cache.RedisCache; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.cache.RedisCacheWriter; import java.time.Duration; /** * 支持自定义过期时间的 {@link RedisCacheManager} 实现类 * * 在 {@link Cacheable#cacheNames()} 格式为 "key#ttl" 时,# 后面的 ttl 为过期时间。 * 单位为最后一个字母(支持的单位有:d 天,h 小时,m 分钟,s 秒),默认单位为 s 秒 * * @author yshop */ public class TimeoutRedisCacheManager extends RedisCacheManager { private static final String SPLIT = "#"; public TimeoutRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) { super(cacheWriter, defaultCacheConfiguration); } @Override protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) { if (StrUtil.isEmpty(name)) { return super.createRedisCache(name, cacheConfig); } // 如果使用 # 分隔,大小不为 2,则说明不使用自定义过期时间 String[] names = StrUtil.splitToArray(name, SPLIT); if (names.length != 2) { return super.createRedisCache(name, cacheConfig); } // 核心:通过修改 cacheConfig 的过期时间,实现自定义过期时间 if (cacheConfig != null) { // 移除 # 后面的 : 以及后面的内容,避免影响解析 String ttlStr = StrUtil.subBefore(names[1], StrUtil.COLON, false); // 获得 ttlStr 时间部分 names[1] = StrUtil.subAfter(names[1], ttlStr, false); // 移除掉 ttlStr 时间部分 // 解析时间 Duration duration = parseDuration(ttlStr); cacheConfig = cacheConfig.entryTtl(duration); } // 创建 RedisCache 对象,需要忽略掉 ttlStr return super.createRedisCache(names[0] + names[1], cacheConfig); } /** * 解析过期时间 Duration * * @param ttlStr 过期时间字符串 * @return 过期时间 Duration */ private Duration parseDuration(String ttlStr) { String timeUnit = StrUtil.subSuf(ttlStr, -1); switch (timeUnit) { case "d": return Duration.ofDays(removeDurationSuffix(ttlStr)); case "h": return Duration.ofHours(removeDurationSuffix(ttlStr)); case "m": return Duration.ofMinutes(removeDurationSuffix(ttlStr)); case "s": return Duration.ofSeconds(removeDurationSuffix(ttlStr)); default: return Duration.ofSeconds(Long.parseLong(ttlStr)); } } /** * 移除多余的后缀,返回具体的时间 * * @param ttlStr 过期时间字符串 * @return 时间 */ private Long removeDurationSuffix(String ttlStr) { return NumberUtil.parseLong(StrUtil.sub(ttlStr, 0, ttlStr.length() - 1)); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-redis/src/main/java/co/yixiang/yshop/framework/redis/util/redis/RedisUtil.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.framework.redis.util.redis; import cn.hutool.extra.spring.SpringUtil; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.util.CollectionUtils; import java.util.Collection; import java.util.concurrent.TimeUnit; @SuppressWarnings("unchecked") public class RedisUtil { // private static RedisTemplate redisTemplate = SpringContextUtils // .getBean("redisTemplate",RedisTemplate.class); private static RedisTemplate redisTemplate = SpringUtil .getBean("redisTemplate",RedisTemplate.class); /** * 指定缓存失效时间 * @param key 键 * @param time 时间(秒) * @return */ public static boolean expire(String key,long time){ try { if(time>0){ redisTemplate.expire(key, time, TimeUnit.SECONDS); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据key 获取过期时间 * @param key 键 不能为null * @return 时间(秒) 返回0代表为永久有效 失效时间为负数,说明该主键未设置失效时间(失效时间默认为-1) */ public static long getExpire(String key){ return redisTemplate.getExpire(key,TimeUnit.SECONDS); } /** * 判断key是否存在 * @param key 键 * @return true 存在 false 不存在 */ public static boolean hasKey(String key){ try { return redisTemplate.hasKey(key); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除缓存 * @param key 可以传一个值 或多个 */ @SuppressWarnings("unchecked") public static void del(String ... key){ if(key!=null&&key.length>0){ if(key.length==1){ redisTemplate.delete(key[0]); }else{ redisTemplate.delete((Collection) CollectionUtils.arrayToList(key)); } } } /** * 普通缓存获取 * @param key 键 * @return 值 */ @SuppressWarnings("unchecked") public static T get(String key){ return key==null?null:(T)redisTemplate.opsForValue().get(key); } /** * 普通缓存放入 * @param key 键 * @param value 值 * @return true成功 false失败 */ public static boolean set(String key,Object value) { try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 普通缓存放入并设置时间 * @param key 键 * @param value 值 * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 * @return true成功 false 失败 */ public static boolean set(String key,Object value,long time){ try { if(time>0){ redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); }else{ set(key, value); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 递增 此时value值必须为int类型 否则报错 * @param key 键 * @param delta 要增加几(大于0) * @return */ public static long incr(String key, long delta){ if(delta<0){ throw new RuntimeException("递增因子必须大于0"); } return redisTemplate.opsForValue().increment(key, delta); } /** * 递减 * @param key 键 * @param delta 要减少几(小于0) * @return */ public static long decr(String key, long delta){ if(delta<0){ throw new RuntimeException("递减因子必须大于0"); } return redisTemplate.opsForValue().increment(key, -delta); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ co.yixiang.yshop.framework.redis.config.YshopRedisAutoConfiguration co.yixiang.yshop.framework.redis.config.YshopCacheAutoConfiguration ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/pom.xml ================================================ co.yixiang.boot yshop-framework ${revision} 4.0.0 yshop-spring-boot-starter-security jar ${project.artifactId} 1. security:用户的认证、权限的校验,实现「谁」可以做「什么事」 2. operatelog:操作日志,实现「谁」在「什么时间」对「什么」做了「什么事」 https://gitee.com/guchengwuyue/yshop-drink co.yixiang.boot yshop-common org.springframework.boot spring-boot-starter-aop co.yixiang.boot yshop-spring-boot-starter-web org.springframework.boot spring-boot-configuration-processor true org.springframework.boot spring-boot-starter-security com.google.guava guava io.github.mouzt bizlog-sdk co.yixiang.boot yshop-module-system-api ${revision} ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/operatelog/config/YshopOperateLogConfiguration.java ================================================ package co.yixiang.yshop.framework.operatelog.config; import co.yixiang.yshop.framework.operatelog.core.service.LogRecordServiceImpl; import com.mzt.logapi.service.ILogRecordService; import com.mzt.logapi.starter.annotation.EnableLogRecord; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; /** * 操作日志配置类 * * @author HUIHUI */ @EnableLogRecord(tenant = "") // 貌似用不上 tenant 这玩意给个空好啦 @AutoConfiguration @Slf4j public class YshopOperateLogConfiguration { @Bean @Primary public ILogRecordService iLogRecordServiceImpl() { return new LogRecordServiceImpl(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/operatelog/core/service/LogRecordServiceImpl.java ================================================ package co.yixiang.yshop.framework.operatelog.core.service; import co.yixiang.yshop.framework.common.util.monitor.TracerUtils; import co.yixiang.yshop.framework.common.util.servlet.ServletUtils; import co.yixiang.yshop.framework.security.core.LoginUser; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import co.yixiang.yshop.module.system.api.logger.OperateLogApi; import co.yixiang.yshop.module.system.api.logger.dto.OperateLogCreateReqDTO; import com.mzt.logapi.beans.LogRecord; import com.mzt.logapi.service.ILogRecordService; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import java.util.List; /** * 操作日志 ILogRecordService 实现类 * * 基于 {@link OperateLogApi} 实现,记录操作日志 * * @author HUIHUI */ @Slf4j public class LogRecordServiceImpl implements ILogRecordService { @Resource private OperateLogApi operateLogApi; @Override public void record(LogRecord logRecord) { // 1. 补全通用字段 OperateLogCreateReqDTO reqDTO = new OperateLogCreateReqDTO(); reqDTO.setTraceId(TracerUtils.getTraceId()); // 补充用户信息 fillUserFields(reqDTO); // 补全模块信息 fillModuleFields(reqDTO, logRecord); // 补全请求信息 fillRequestFields(reqDTO); // 2. 异步记录日志 operateLogApi.createOperateLog(reqDTO); } private static void fillUserFields(OperateLogCreateReqDTO reqDTO) { // 使用 SecurityFrameworkUtils。因为要考虑,rpc、mq、job,它其实不是 web; LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); if (loginUser == null) { return; } reqDTO.setUserId(loginUser.getId()); reqDTO.setUserType(loginUser.getUserType()); } public static void fillModuleFields(OperateLogCreateReqDTO reqDTO, LogRecord logRecord) { reqDTO.setType(logRecord.getType()); // 大模块类型,例如:CRM 客户 reqDTO.setSubType(logRecord.getSubType());// 操作名称,例如:转移客户 reqDTO.setBizId(Long.parseLong(logRecord.getBizNo())); // 业务编号,例如:客户编号 reqDTO.setAction(logRecord.getAction());// 操作内容,例如:修改编号为 1 的用户信息,将性别从男改成女,将姓名从yshop改成源码。 reqDTO.setExtra(logRecord.getExtra()); // 拓展字段,有些复杂的业务,需要记录一些字段 ( JSON 格式 ),例如说,记录订单编号,{ orderId: "1"} } private static void fillRequestFields(OperateLogCreateReqDTO reqDTO) { // 获得 Request 对象 HttpServletRequest request = ServletUtils.getRequest(); if (request == null) { return; } // 补全请求信息 reqDTO.setRequestMethod(request.getMethod()); reqDTO.setRequestUrl(request.getRequestURI()); reqDTO.setUserIp(ServletUtils.getClientIP(request)); reqDTO.setUserAgent(ServletUtils.getUserAgent(request)); } @Override public List queryLog(String bizNo, String type) { throw new UnsupportedOperationException("使用 OperateLogApi 进行操作日志的查询"); } @Override public List queryLogByBizNo(String bizNo, String type, String subType) { throw new UnsupportedOperationException("使用 OperateLogApi 进行操作日志的查询"); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/config/AuthorizeRequestsCustomizer.java ================================================ package co.yixiang.yshop.framework.security.config; import co.yixiang.yshop.framework.web.config.WebProperties; import jakarta.annotation.Resource; import org.springframework.core.Ordered; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; /** * 自定义的 URL 的安全配置 * 目的:每个 Maven Module 可以自定义规则! * * @author yshop */ public abstract class AuthorizeRequestsCustomizer implements Customizer.AuthorizationManagerRequestMatcherRegistry>, Ordered { @Resource private WebProperties webProperties; protected String buildAdminApi(String url) { return webProperties.getAdminApi().getPrefix() + url; } protected String buildAppApi(String url) { return webProperties.getAppApi().getPrefix() + url; } @Override public int getOrder() { return 0; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/config/SecurityProperties.java ================================================ package co.yixiang.yshop.framework.security.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.util.Collections; import java.util.List; @ConfigurationProperties(prefix = "yshop.security") @Validated @Data public class SecurityProperties { /** * HTTP 请求时,访问令牌的请求 Header */ @NotEmpty(message = "Token Header 不能为空") private String tokenHeader = "Authorization"; /** * HTTP 请求时,访问令牌的请求参数 * * 初始目的:解决 WebSocket 无法通过 header 传参,只能通过 token 参数拼接 */ @NotEmpty(message = "Token Parameter 不能为空") private String tokenParameter = "token"; /** * mock 模式的开关 */ @NotNull(message = "mock 模式的开关不能为空") private Boolean mockEnable = false; /** * mock 模式的密钥 * 一定要配置密钥,保证安全性 */ @NotEmpty(message = "mock 模式的密钥不能为空") // 这里设置了一个默认值,因为实际上只有 mockEnable 为 true 时才需要配置。 private String mockSecret = "test"; /** * 免登录的 URL 列表 */ private List permitAllUrls = Collections.emptyList(); /** * PasswordEncoder 加密复杂度,越高开销越大 */ private Integer passwordEncoderLength = 4; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/config/YshopSecurityAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.security.config; import co.yixiang.yshop.framework.security.core.aop.PreAuthenticatedAspect; import co.yixiang.yshop.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy; import co.yixiang.yshop.framework.security.core.filter.TokenAuthenticationFilter; import co.yixiang.yshop.framework.security.core.handler.AccessDeniedHandlerImpl; import co.yixiang.yshop.framework.security.core.handler.AuthenticationEntryPointImpl; import co.yixiang.yshop.framework.security.core.service.SecurityFrameworkService; import co.yixiang.yshop.framework.security.core.service.SecurityFrameworkServiceImpl; import co.yixiang.yshop.framework.web.core.handler.GlobalExceptionHandler; import co.yixiang.yshop.module.system.api.oauth2.OAuth2TokenApi; import co.yixiang.yshop.module.system.api.permission.PermissionApi; import jakarta.annotation.Resource; import org.springframework.beans.factory.config.MethodInvokingFactoryBean; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; /** * Spring Security 自动配置类,主要用于相关组件的配置 * * 注意,不能和 {@link YshopWebSecurityConfigurerAdapter} 用一个,原因是会导致初始化报错。 * 参见 https://stackoverflow.com/questions/53847050/spring-boot-delegatebuilder-cannot-be-null-on-autowiring-authenticationmanager 文档。 * * @author yshop */ @AutoConfiguration @AutoConfigureOrder(-1) // 目的:先于 Spring Security 自动配置,避免一键改包后,org.* 基础包无法生效 @EnableConfigurationProperties(SecurityProperties.class) public class YshopSecurityAutoConfiguration { @Resource private SecurityProperties securityProperties; /** * 处理用户未登录拦截的切面的 Bean */ @Bean public PreAuthenticatedAspect preAuthenticatedAspect() { return new PreAuthenticatedAspect(); } /** * 认证失败处理类 Bean */ @Bean public AuthenticationEntryPoint authenticationEntryPoint() { return new AuthenticationEntryPointImpl(); } /** * 权限不够处理器 Bean */ @Bean public AccessDeniedHandler accessDeniedHandler() { return new AccessDeniedHandlerImpl(); } /** * Spring Security 加密器 * 考虑到安全性,这里采用 BCryptPasswordEncoder 加密器 * * @see Password Encoding with Spring Security */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(securityProperties.getPasswordEncoderLength()); } /** * Token 认证过滤器 Bean */ @Bean public TokenAuthenticationFilter authenticationTokenFilter(GlobalExceptionHandler globalExceptionHandler, OAuth2TokenApi oauth2TokenApi) { return new TokenAuthenticationFilter(securityProperties, globalExceptionHandler, oauth2TokenApi); } @Bean("ss") // 使用 Spring Security 的缩写,方便使用 public SecurityFrameworkService securityFrameworkService(PermissionApi permissionApi) { return new SecurityFrameworkServiceImpl(permissionApi); } /** * 声明调用 {@link SecurityContextHolder#setStrategyName(String)} 方法, * 设置使用 {@link TransmittableThreadLocalSecurityContextHolderStrategy} 作为 Security 的上下文策略 */ @Bean public MethodInvokingFactoryBean securityContextHolderMethodInvokingFactoryBean() { MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean(); methodInvokingFactoryBean.setTargetClass(SecurityContextHolder.class); methodInvokingFactoryBean.setTargetMethod("setStrategyName"); methodInvokingFactoryBean.setArguments(TransmittableThreadLocalSecurityContextHolderStrategy.class.getName()); return methodInvokingFactoryBean; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/config/YshopWebSecurityConfigurerAdapter.java ================================================ package co.yixiang.yshop.framework.security.config; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.security.core.filter.TokenAuthenticationFilter; import co.yixiang.yshop.framework.web.config.WebProperties; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import jakarta.annotation.Resource; import jakarta.annotation.security.PermitAll; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.util.pattern.PathPattern; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertList; /** * 自定义的 Spring Security 配置适配器实现 * * @author yshop */ @AutoConfiguration @AutoConfigureOrder(-1) // 目的:先于 Spring Security 自动配置,避免一键改包后,org.* 基础包无法生效 @EnableMethodSecurity(securedEnabled = true) public class YshopWebSecurityConfigurerAdapter { @Resource private WebProperties webProperties; @Resource private SecurityProperties securityProperties; /** * 认证失败处理类 Bean */ @Resource private AuthenticationEntryPoint authenticationEntryPoint; /** * 权限不够处理器 Bean */ @Resource private AccessDeniedHandler accessDeniedHandler; /** * Token 认证过滤器 Bean */ @Resource private TokenAuthenticationFilter authenticationTokenFilter; /** * 自定义的权限映射 Bean 们 * * @see #filterChain(HttpSecurity) */ @Resource private List authorizeRequestsCustomizers; @Resource private ApplicationContext applicationContext; /** * 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入 * 通过覆写父类的该方法,添加 @Bean 注解,解决该问题 */ @Bean public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } /** * 配置 URL 的安全配置 * * anyRequest | 匹配所有请求路径 * access | SpringEl表达式结果为true时可以访问 * anonymous | 匿名可以访问 * denyAll | 用户不能访问 * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 * hasRole | 如果有参数,参数表示角色,则其角色可以访问 * permitAll | 用户可以任意访问 * rememberMe | 允许通过remember-me登录的用户访问 * authenticated | 用户登录后可访问 */ @Bean protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { // 登出 httpSecurity // 开启跨域 .cors(Customizer.withDefaults()) // CSRF 禁用,因为不使用 Session .csrf(AbstractHttpConfigurer::disable) // 基于 token 机制,所以不需要 Session .sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .headers(c -> c.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) // 一堆自定义的 Spring Security 处理器 .exceptionHandling(c -> c.authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler)); // 登录、登录暂时不使用 Spring Security 的拓展点,主要考虑一方面拓展多用户、多种登录方式相对复杂,一方面用户的学习成本较高 // 获得 @PermitAll 带来的 URL 列表,免登录 Multimap permitAllUrls = getPermitAllUrlsFromAnnotations(); // 设置每个请求的权限 httpSecurity // ①:全局共享规则 .authorizeHttpRequests(c -> c // 1.1 静态资源,可匿名访问 .requestMatchers(HttpMethod.GET, "/*.html", "/*.html", "/*.css", "/*.js").permitAll() // 1.1 设置 @PermitAll 无需认证 .requestMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll() .requestMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll() .requestMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll() .requestMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll() .requestMatchers(HttpMethod.HEAD, permitAllUrls.get(HttpMethod.HEAD).toArray(new String[0])).permitAll() .requestMatchers(HttpMethod.PATCH, permitAllUrls.get(HttpMethod.PATCH).toArray(new String[0])).permitAll() // 1.2 基于 yshop.security.permit-all-urls 无需认证 .requestMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll() // 1.3 设置 App API 无需认证 .requestMatchers(buildAppApi("/**")).permitAll() ) // ②:每个项目的自定义规则 .authorizeHttpRequests(c -> authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(c))) // ③:兜底规则,必须认证 .authorizeHttpRequests(c -> c.anyRequest().authenticated()); // 添加 Token Filter httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); return httpSecurity.build(); } private String buildAppApi(String url) { return webProperties.getAppApi().getPrefix() + url; } private Multimap getPermitAllUrlsFromAnnotations() { Multimap result = HashMultimap.create(); // 获得接口对应的 HandlerMethod 集合 RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping"); Map handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods(); // 获得有 @PermitAll 注解的接口 for (Map.Entry entry : handlerMethodMap.entrySet()) { HandlerMethod handlerMethod = entry.getValue(); if (!handlerMethod.hasMethodAnnotation(PermitAll.class)) { continue; } Set urls = new HashSet<>(); if (entry.getKey().getPatternsCondition() != null) { urls.addAll(entry.getKey().getPatternsCondition().getPatterns()); } if (entry.getKey().getPathPatternsCondition() != null) { urls.addAll(convertList(entry.getKey().getPathPatternsCondition().getPatterns(), PathPattern::getPatternString)); } if (urls.isEmpty()) { continue; } // 特殊:使用 @RequestMapping 注解,并且未写 method 属性,此时认为都需要免登录 Set methods = entry.getKey().getMethodsCondition().getMethods(); if (CollUtil.isEmpty(methods)) { result.putAll(HttpMethod.GET, urls); result.putAll(HttpMethod.POST, urls); result.putAll(HttpMethod.PUT, urls); result.putAll(HttpMethod.DELETE, urls); result.putAll(HttpMethod.HEAD, urls); result.putAll(HttpMethod.PATCH, urls); continue; } // 根据请求方法,添加到 result 结果 entry.getKey().getMethodsCondition().getMethods().forEach(requestMethod -> { switch (requestMethod) { case GET: result.putAll(HttpMethod.GET, urls); break; case POST: result.putAll(HttpMethod.POST, urls); break; case PUT: result.putAll(HttpMethod.PUT, urls); break; case DELETE: result.putAll(HttpMethod.DELETE, urls); break; case HEAD: result.putAll(HttpMethod.HEAD, urls); break; case PATCH: result.putAll(HttpMethod.PATCH, urls); break; } }); } return result; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/core/LoginUser.java ================================================ package co.yixiang.yshop.framework.security.core; import cn.hutool.core.map.MapUtil; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 登录用户信息 * * @author yshop */ @Data public class LoginUser { public static final String INFO_KEY_NICKNAME = "nickname"; public static final String INFO_KEY_DEPT_ID = "deptId"; /** * 用户编号 */ private Long id; /** * 用户类型 * * 关联 {@link UserTypeEnum} */ private Integer userType; /** * 额外的用户信息 */ private Map info; /** * 租户编号 */ private Long tenantId; /** * 授权范围 */ private List scopes; private Long shopId; // ========== 上下文 ========== /** * 上下文字段,不进行持久化 * * 1. 用于基于 LoginUser 维度的临时缓存 */ @JsonIgnore private Map context; public void setContext(String key, Object value) { if (context == null) { context = new HashMap<>(); } context.put(key, value); } public T getContext(String key, Class type) { return MapUtil.get(context, key, type); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/core/annotations/PreAuthenticated.java ================================================ package co.yixiang.yshop.framework.security.core.annotations; import java.lang.annotation.*; /** * 声明用户需要登录 * * 为什么不使用 {@link org.springframework.security.access.prepost.PreAuthorize} 注解,原因是不通过时,抛出的是认证不通过,而不是未登录 * * @author yshop */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface PreAuthenticated { } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/core/aop/PreAuthenticatedAspect.java ================================================ package co.yixiang.yshop.framework.security.core.aop; import co.yixiang.yshop.framework.security.core.annotations.PreAuthenticated; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import static co.yixiang.yshop.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; @Aspect @Slf4j public class PreAuthenticatedAspect { @Around("@annotation(preAuthenticated)") public Object around(ProceedingJoinPoint joinPoint, PreAuthenticated preAuthenticated) throws Throwable { if (SecurityFrameworkUtils.getLoginUser() == null) { throw exception(UNAUTHORIZED); } return joinPoint.proceed(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java ================================================ package co.yixiang.yshop.framework.security.core.context; import com.alibaba.ttl.TransmittableThreadLocal; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.core.context.SecurityContextImpl; import org.springframework.util.Assert; /** * 基于 TransmittableThreadLocal 实现的 Security Context 持有者策略 * 目的是,避免 @Async 等异步执行时,原生 ThreadLocal 的丢失问题 * * @author yshop */ public class TransmittableThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { /** * 使用 TransmittableThreadLocal 作为上下文 */ private static final ThreadLocal CONTEXT_HOLDER = new TransmittableThreadLocal<>(); @Override public void clearContext() { CONTEXT_HOLDER.remove(); } @Override public SecurityContext getContext() { SecurityContext ctx = CONTEXT_HOLDER.get(); if (ctx == null) { ctx = createEmptyContext(); CONTEXT_HOLDER.set(ctx); } return ctx; } @Override public void setContext(SecurityContext context) { Assert.notNull(context, "Only non-null SecurityContext instances are permitted"); CONTEXT_HOLDER.set(context); } @Override public SecurityContext createEmptyContext() { return new SecurityContextImpl(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/core/filter/TokenAuthenticationFilter.java ================================================ package co.yixiang.yshop.framework.security.core.filter; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.exception.ServiceException; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.util.servlet.ServletUtils; import co.yixiang.yshop.framework.security.config.SecurityProperties; import co.yixiang.yshop.framework.security.core.LoginUser; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import co.yixiang.yshop.framework.web.core.handler.GlobalExceptionHandler; import co.yixiang.yshop.framework.web.core.util.WebFrameworkUtils; import co.yixiang.yshop.module.system.api.oauth2.OAuth2TokenApi; import co.yixiang.yshop.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO; import lombok.RequiredArgsConstructor; import org.springframework.security.access.AccessDeniedException; import org.springframework.web.filter.OncePerRequestFilter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; /** * Token 过滤器,验证 token 的有效性 * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文 * * @author yshop */ @RequiredArgsConstructor public class TokenAuthenticationFilter extends OncePerRequestFilter { private final SecurityProperties securityProperties; private final GlobalExceptionHandler globalExceptionHandler; private final OAuth2TokenApi oauth2TokenApi; @Override @SuppressWarnings("NullableProblems") protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader(), securityProperties.getTokenParameter()); if (StrUtil.isNotEmpty(token)) { Integer userType = WebFrameworkUtils.getLoginUserType(request); try { // 1.1 基于 token 构建登录用户 LoginUser loginUser = buildLoginUserByToken(token, userType); // 1.2 模拟 Login 功能,方便日常开发调试 if (loginUser == null) { loginUser = mockLoginUser(request, token, userType); } // 2. 设置当前用户 if (loginUser != null) { SecurityFrameworkUtils.setLoginUser(loginUser, request); } } catch (Throwable ex) { CommonResult result = globalExceptionHandler.allExceptionHandler(request, ex); ServletUtils.writeJSON(response, result); return; } } // 继续过滤链 chain.doFilter(request, response); } private LoginUser buildLoginUserByToken(String token, Integer userType) { try { OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token); if (accessToken == null) { return null; } // 用户类型不匹配,无权限 // 注意:只有 /admin-api/* 和 /app-api/* 有 userType,才需要比对用户类型 // 类似 WebSocket 的 /ws/* 连接地址,是不需要比对用户类型的 if (userType != null && ObjectUtil.notEqual(accessToken.getUserType(), userType)) { throw new AccessDeniedException("错误的用户类型"); } // 构建登录用户 return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) .setShopId(accessToken.getShopId()) .setInfo(accessToken.getUserInfo()) // 额外的用户信息 .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes()); } catch (ServiceException serviceException) { // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可 return null; } } /** * 模拟登录用户,方便日常开发调试 * * 注意,在线上环境下,一定要关闭该功能!!! * * @param request 请求 * @param token 模拟的 token,格式为 {@link SecurityProperties#getMockSecret()} + 用户编号 * @param userType 用户类型 * @return 模拟的 LoginUser */ private LoginUser mockLoginUser(HttpServletRequest request, String token, Integer userType) { if (!securityProperties.getMockEnable()) { return null; } // 必须以 mockSecret 开头 if (!token.startsWith(securityProperties.getMockSecret())) { return null; } // 构建模拟用户 Long userId = Long.valueOf(token.substring(securityProperties.getMockSecret().length())); return new LoginUser().setId(userId).setUserType(userType) .setTenantId(WebFrameworkUtils.getTenantId(request)); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/core/handler/AccessDeniedHandlerImpl.java ================================================ package co.yixiang.yshop.framework.security.core.handler; import co.yixiang.yshop.framework.common.exception.enums.GlobalErrorCodeConstants; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import co.yixiang.yshop.framework.common.util.servlet.ServletUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.access.ExceptionTranslationFilter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import static co.yixiang.yshop.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN; /** * 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。 * * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类 * * @author yshop */ @Slf4j @SuppressWarnings("JavadocReference") public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { // 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏 log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(), SecurityFrameworkUtils.getLoginUserId(), e); // 返回 403 ServletUtils.writeJSON(response, CommonResult.error(FORBIDDEN)); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/core/handler/AuthenticationEntryPointImpl.java ================================================ package co.yixiang.yshop.framework.security.core.handler; import co.yixiang.yshop.framework.common.exception.enums.GlobalErrorCodeConstants; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.util.servlet.ServletUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.ExceptionTranslationFilter; import jakarta.servlet.FilterChain; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import static co.yixiang.yshop.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; /** * 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页 * * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类 * * @author ruoyi */ @Slf4j @SuppressWarnings("JavadocReference") // 忽略文档引用报错 public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e); // 返回 401 ServletUtils.writeJSON(response, CommonResult.error(UNAUTHORIZED)); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/core/service/SecurityFrameworkService.java ================================================ package co.yixiang.yshop.framework.security.core.service; /** * Security 框架 Service 接口,定义权限相关的校验操作 * * @author yshop */ public interface SecurityFrameworkService { /** * 判断是否有权限 * * @param permission 权限 * @return 是否 */ boolean hasPermission(String permission); /** * 判断是否有权限,任一一个即可 * * @param permissions 权限 * @return 是否 */ boolean hasAnyPermissions(String... permissions); /** * 判断是否有角色 * * 注意,角色使用的是 SysRoleDO 的 code 标识 * * @param role 角色 * @return 是否 */ boolean hasRole(String role); /** * 判断是否有角色,任一一个即可 * * @param roles 角色数组 * @return 是否 */ boolean hasAnyRoles(String... roles); /** * 判断是否有授权 * * @param scope 授权 * @return 是否 */ boolean hasScope(String scope); /** * 判断是否有授权范围,任一一个即可 * * @param scope 授权范围数组 * @return 是否 */ boolean hasAnyScopes(String... scope); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/core/service/SecurityFrameworkServiceImpl.java ================================================ package co.yixiang.yshop.framework.security.core.service; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.security.core.LoginUser; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import co.yixiang.yshop.module.system.api.permission.PermissionApi; import lombok.AllArgsConstructor; import java.util.Arrays; import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; /** * 默认的 {@link SecurityFrameworkService} 实现类 * * @author yshop */ @AllArgsConstructor public class SecurityFrameworkServiceImpl implements SecurityFrameworkService { private final PermissionApi permissionApi; @Override public boolean hasPermission(String permission) { return hasAnyPermissions(permission); } @Override public boolean hasAnyPermissions(String... permissions) { return permissionApi.hasAnyPermissions(getLoginUserId(), permissions); } @Override public boolean hasRole(String role) { return hasAnyRoles(role); } @Override public boolean hasAnyRoles(String... roles) { return permissionApi.hasAnyRoles(getLoginUserId(), roles); } @Override public boolean hasScope(String scope) { return hasAnyScopes(scope); } @Override public boolean hasAnyScopes(String... scope) { LoginUser user = SecurityFrameworkUtils.getLoginUser(); if (user == null) { return false; } return CollUtil.containsAny(user.getScopes(), Arrays.asList(scope)); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/java/co/yixiang/yshop/framework/security/core/util/SecurityFrameworkUtils.java ================================================ package co.yixiang.yshop.framework.security.core.util; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.security.core.LoginUser; import co.yixiang.yshop.framework.web.core.util.WebFrameworkUtils; import org.springframework.lang.Nullable; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.util.StringUtils; import jakarta.servlet.http.HttpServletRequest; import java.util.Collections; /** * 安全服务工具类 * * @author yshop */ public class SecurityFrameworkUtils { /** * HEADER 认证头 value 的前缀 */ public static final String AUTHORIZATION_BEARER = "Bearer"; private SecurityFrameworkUtils() {} /** * 从请求中,获得认证 Token * * @param request 请求 * @param headerName 认证 Token 对应的 Header 名字 * @param parameterName 认证 Token 对应的 Parameter 名字 * @return 认证 Token */ public static String obtainAuthorization(HttpServletRequest request, String headerName, String parameterName) { // 1. 获得 Token。优先级:Header > Parameter String token = request.getHeader(headerName); if (StrUtil.isEmpty(token)) { token = request.getParameter(parameterName); } if (!StringUtils.hasText(token)) { return null; } // 2. 去除 Token 中带的 Bearer int index = token.indexOf(AUTHORIZATION_BEARER + " "); return index >= 0 ? token.substring(index + 7).trim() : token; } /** * 获得当前认证信息 * * @return 认证信息 */ public static Authentication getAuthentication() { SecurityContext context = SecurityContextHolder.getContext(); if (context == null) { return null; } return context.getAuthentication(); } /** * 获取当前用户 * * @return 当前用户 */ @Nullable public static LoginUser getLoginUser() { Authentication authentication = getAuthentication(); if (authentication == null) { return null; } return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null; } /** * 获得当前用户的编号,从上下文中 * * @return 用户编号 */ @Nullable public static Long getLoginUserId() { LoginUser loginUser = getLoginUser(); return loginUser != null ? loginUser.getId() : null; } /** * 获得当前用户的昵称,从上下文中 * * @return 昵称 */ @Nullable public static String getLoginUserNickname() { LoginUser loginUser = getLoginUser(); return loginUser != null ? MapUtil.getStr(loginUser.getInfo(), LoginUser.INFO_KEY_NICKNAME) : null; } /** * 获得当前用户的部门编号,从上下文中 * * @return 部门编号 */ @Nullable public static Long getLoginUserDeptId() { LoginUser loginUser = getLoginUser(); return loginUser != null ? MapUtil.getLong(loginUser.getInfo(), LoginUser.INFO_KEY_DEPT_ID) : null; } /** * 设置当前用户 * * @param loginUser 登录用户 * @param request 请求 */ public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) { // 创建 Authentication,并设置到上下文 Authentication authentication = buildAuthentication(loginUser, request); SecurityContextHolder.getContext().setAuthentication(authentication); // 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号; // 原因是,Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息 WebFrameworkUtils.setLoginUserId(request, loginUser.getId()); WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType()); } private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) { // 创建 UsernamePasswordAuthenticationToken 对象 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( loginUser, null, Collections.emptyList()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); return authenticationToken; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ co.yixiang.yshop.framework.security.config.YshopSecurityAutoConfiguration co.yixiang.yshop.framework.security.config.YshopWebSecurityConfigurerAdapter co.yixiang.yshop.framework.operatelog.config.YshopOperateLogConfiguration ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-test/pom.xml ================================================ co.yixiang.boot yshop-framework ${revision} 4.0.0 yshop-spring-boot-starter-test jar ${project.artifactId} 测试组件,用于单元测试、集成测试 https://gitee.com/guchengwuyue/yshop-drink co.yixiang.boot yshop-common co.yixiang.boot yshop-spring-boot-starter-mybatis co.yixiang.boot yshop-spring-boot-starter-redis org.mockito mockito-inline org.springframework.boot spring-boot-starter-test com.h2database h2 com.github.fppt jedis-mock uk.co.jemos.podam podam ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-test/src/main/java/co/yixiang/yshop/framework/test/config/RedisTestConfiguration.java ================================================ package co.yixiang.yshop.framework.test.config; import com.github.fppt.jedismock.RedisServer; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import java.io.IOException; /** * Redis 测试 Configuration,主要实现内嵌 Redis 的启动 * * @author yshop */ @Configuration(proxyBeanMethods = false) @Lazy(false) // 禁止延迟加载 @EnableConfigurationProperties(RedisProperties.class) public class RedisTestConfiguration { /** * 创建模拟的 Redis Server 服务器 */ @Bean public RedisServer redisServer(RedisProperties properties) throws IOException { RedisServer redisServer = new RedisServer(properties.getPort()); // 一次执行多个单元测试时,貌似创建多个 spring 容器,导致不进行 stop。这样,就导致端口被占用,无法启动。。。 try { redisServer.start(); } catch (Exception ignore) {} return redisServer; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-test/src/main/java/co/yixiang/yshop/framework/test/config/SqlInitializationTestConfiguration.java ================================================ package co.yixiang.yshop.framework.test.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer; import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer; import org.springframework.boot.sql.init.DatabaseInitializationSettings; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import javax.sql.DataSource; /** * SQL 初始化的测试 Configuration * * 为什么不使用 org.springframework.boot.autoconfigure.sql.init.DataSourceInitializationConfiguration 呢? * 因为我们在单元测试会使用 spring.main.lazy-initialization 为 true,开启延迟加载。此时,会导致 DataSourceInitializationConfiguration 初始化 * 不过呢,当前类的实现代码,基本是复制 DataSourceInitializationConfiguration 的哈! * * @author yshop */ @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(AbstractScriptDatabaseInitializer.class) @ConditionalOnSingleCandidate(DataSource.class) @ConditionalOnClass(name = "org.springframework.jdbc.datasource.init.DatabasePopulator") @Lazy(value = false) // 禁止延迟加载 @EnableConfigurationProperties(SqlInitializationProperties.class) public class SqlInitializationTestConfiguration { @Bean public DataSourceScriptDatabaseInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource, SqlInitializationProperties initializationProperties) { DatabaseInitializationSettings settings = createFrom(initializationProperties); return new DataSourceScriptDatabaseInitializer(dataSource, settings); } static DatabaseInitializationSettings createFrom(SqlInitializationProperties properties) { DatabaseInitializationSettings settings = new DatabaseInitializationSettings(); settings.setSchemaLocations(properties.getSchemaLocations()); settings.setDataLocations(properties.getDataLocations()); settings.setContinueOnError(properties.isContinueOnError()); settings.setSeparator(properties.getSeparator()); settings.setEncoding(properties.getEncoding()); settings.setMode(properties.getMode()); return settings; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-test/src/main/java/co/yixiang/yshop/framework/test/core/ut/BaseDbAndRedisUnitTest.java ================================================ package co.yixiang.yshop.framework.test.core.ut; import co.yixiang.yshop.framework.datasource.config.YshopDataSourceAutoConfiguration; import co.yixiang.yshop.framework.mybatis.config.YshopMybatisAutoConfiguration; import co.yixiang.yshop.framework.redis.config.YshopRedisAutoConfiguration; import co.yixiang.yshop.framework.test.config.RedisTestConfiguration; import co.yixiang.yshop.framework.test.config.SqlInitializationTestConfiguration; import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure; import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; import org.redisson.spring.starter.RedissonAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.jdbc.Sql; /** * 依赖内存 DB + Redis 的单元测试 * * 相比 {@link BaseDbUnitTest} 来说,额外增加了内存 Redis * * @author yshop */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisUnitTest.Application.class) @ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件 @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB public class BaseDbAndRedisUnitTest { @Import({ // DB 配置类 YshopDataSourceAutoConfiguration.class, // 自己的 DB 配置类 DataSourceAutoConfiguration.class, // Spring DB 自动配置类 DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类 DruidDataSourceAutoConfigure.class, // Druid 自动配置类 SqlInitializationTestConfiguration.class, // SQL 初始化 // MyBatis 配置类 YshopMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类 MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类 // Redis 配置类 RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer YshopRedisAutoConfiguration.class, // 自己的 Redis 配置类 RedisAutoConfiguration.class, // Spring Redis 自动配置类 RedissonAutoConfiguration.class, // Redisson 自动配置类 }) public static class Application { } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-test/src/main/java/co/yixiang/yshop/framework/test/core/ut/BaseDbUnitTest.java ================================================ package co.yixiang.yshop.framework.test.core.ut; import co.yixiang.yshop.framework.datasource.config.YshopDataSourceAutoConfiguration; import co.yixiang.yshop.framework.mybatis.config.YshopMybatisAutoConfiguration; import co.yixiang.yshop.framework.test.config.SqlInitializationTestConfiguration; import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure; import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; import com.github.yulichang.autoconfigure.MybatisPlusJoinAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.jdbc.Sql; /** * 依赖内存 DB 的单元测试 * * 注意,Service 层同样适用。对于 Service 层的单元测试,我们针对自己模块的 Mapper 走的是 H2 内存数据库,针对别的模块的 Service 走的是 Mock 方法 * * @author yshop */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbUnitTest.Application.class) @ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件 @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB public class BaseDbUnitTest { @Import({ // DB 配置类 YshopDataSourceAutoConfiguration.class, // 自己的 DB 配置类 DataSourceAutoConfiguration.class, // Spring DB 自动配置类 DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类 DruidDataSourceAutoConfigure.class, // Druid 自动配置类 SqlInitializationTestConfiguration.class, // SQL 初始化 // MyBatis 配置类 YshopMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类 MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类 MybatisPlusJoinAutoConfiguration.class, // MyBatis 的Join配置类 }) public static class Application { } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-test/src/main/java/co/yixiang/yshop/framework/test/core/ut/BaseMockitoUnitTest.java ================================================ package co.yixiang.yshop.framework.test.core.ut; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; /** * 纯 Mockito 的单元测试 * * @author yshop */ @ExtendWith(MockitoExtension.class) public class BaseMockitoUnitTest { } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-test/src/main/java/co/yixiang/yshop/framework/test/core/ut/BaseRedisUnitTest.java ================================================ package co.yixiang.yshop.framework.test.core.ut; import co.yixiang.yshop.framework.redis.config.YshopRedisAutoConfiguration; import co.yixiang.yshop.framework.test.config.RedisTestConfiguration; import org.redisson.spring.starter.RedissonAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; /** * 依赖内存 Redis 的单元测试 * * 相比 {@link BaseDbUnitTest} 来说,从内存 DB 改成了内存 Redis * * @author yshop */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseRedisUnitTest.Application.class) @ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件 public class BaseRedisUnitTest { @Import({ // Redis 配置类 RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer RedisAutoConfiguration.class, // Spring Redis 自动配置类 YshopRedisAutoConfiguration.class, // 自己的 Redis 配置类 RedissonAutoConfiguration.class, // Redisson 自动配置类 }) public static class Application { } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-test/src/main/java/co/yixiang/yshop/framework/test/core/util/AssertUtils.java ================================================ package co.yixiang.yshop.framework.test.core.util; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ReflectUtil; import co.yixiang.yshop.framework.common.exception.ErrorCode; import co.yixiang.yshop.framework.common.exception.ServiceException; import co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.function.Executable; import java.lang.reflect.Field; import java.util.Arrays; import java.util.Objects; import static org.junit.jupiter.api.Assertions.assertThrows; /** * 单元测试,assert 断言工具类 * * @author yshop */ public class AssertUtils { /** * 比对两个对象的属性是否一致 * * 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略 * * @param expected 期望对象 * @param actual 实际对象 * @param ignoreFields 忽略的属性数组 */ public static void assertPojoEquals(Object expected, Object actual, String... ignoreFields) { Field[] expectedFields = ReflectUtil.getFields(expected.getClass()); Arrays.stream(expectedFields).forEach(expectedField -> { // 忽略 jacoco 自动生成的 $jacocoData 属性的情况 if (expectedField.isSynthetic()) { return; } // 如果是忽略的属性,则不进行比对 if (ArrayUtil.contains(ignoreFields, expectedField.getName())) { return; } // 忽略不存在的属性 Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName()); if (actualField == null) { return; } // 比对 Assertions.assertEquals( ReflectUtil.getFieldValue(expected, expectedField), ReflectUtil.getFieldValue(actual, actualField), String.format("Field(%s) 不匹配", expectedField.getName()) ); }); } /** * 比对两个对象的属性是否一致 * * 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略 * * @param expected 期望对象 * @param actual 实际对象 * @param ignoreFields 忽略的属性数组 * @return 是否一致 */ public static boolean isPojoEquals(Object expected, Object actual, String... ignoreFields) { Field[] expectedFields = ReflectUtil.getFields(expected.getClass()); return Arrays.stream(expectedFields).allMatch(expectedField -> { // 如果是忽略的属性,则不进行比对 if (ArrayUtil.contains(ignoreFields, expectedField.getName())) { return true; } // 忽略不存在的属性 Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName()); if (actualField == null) { return true; } return Objects.equals(ReflectUtil.getFieldValue(expected, expectedField), ReflectUtil.getFieldValue(actual, actualField)); }); } /** * 执行方法,校验抛出的 Service 是否符合条件 * * @param executable 业务异常 * @param errorCode 错误码对象 * @param messageParams 消息参数 */ public static void assertServiceException(Executable executable, ErrorCode errorCode, Object... messageParams) { // 调用方法 ServiceException serviceException = assertThrows(ServiceException.class, executable); // 校验错误码 Assertions.assertEquals(errorCode.getCode(), serviceException.getCode(), "错误码不匹配"); String message = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), messageParams); Assertions.assertEquals(message, serviceException.getMessage(), "错误提示不匹配"); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-test/src/main/java/co/yixiang/yshop/framework/test/core/util/RandomUtils.java ================================================ package co.yixiang.yshop.framework.test.core.util; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import uk.co.jemos.podam.api.PodamFactory; import uk.co.jemos.podam.api.PodamFactoryImpl; import java.lang.reflect.Type; import java.time.LocalDateTime; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; /** * 随机工具类 * * @author yshop */ public class RandomUtils { private static final int RANDOM_STRING_LENGTH = 10; private static final int TINYINT_MAX = 127; private static final int RANDOM_DATE_MAX = 30; private static final int RANDOM_COLLECTION_LENGTH = 5; private static final PodamFactory PODAM_FACTORY = new PodamFactoryImpl(); static { // 字符串 PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(String.class, (dataProviderStrategy, attributeMetadata, map) -> randomString()); // Integer PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(Integer.class, (dataProviderStrategy, attributeMetadata, map) -> { // 如果是 status 的字段,返回 0 或 1 if ("status".equals(attributeMetadata.getAttributeName())) { return RandomUtil.randomEle(CommonStatusEnum.values()).getStatus(); } // 如果是 type、status 结尾的字段,返回 tinyint 范围 if (StrUtil.endWithAnyIgnoreCase(attributeMetadata.getAttributeName(), "type", "status", "category", "scope", "result")) { return RandomUtil.randomInt(0, TINYINT_MAX + 1); } return RandomUtil.randomInt(); }); // LocalDateTime PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(LocalDateTime.class, (dataProviderStrategy, attributeMetadata, map) -> randomLocalDateTime()); // Boolean PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(Boolean.class, (dataProviderStrategy, attributeMetadata, map) -> { // 如果是 deleted 的字段,返回非删除 if ("deleted".equals(attributeMetadata.getAttributeName())) { return false; } return RandomUtil.randomBoolean(); }); } public static String randomString() { return RandomUtil.randomString(RANDOM_STRING_LENGTH); } public static Long randomLongId() { return RandomUtil.randomLong(0, Long.MAX_VALUE); } public static Integer randomInteger() { return RandomUtil.randomInt(0, Integer.MAX_VALUE); } public static Date randomDate() { return RandomUtil.randomDay(0, RANDOM_DATE_MAX); } public static LocalDateTime randomLocalDateTime() { // 设置 Nano 为零的原因,避免 MySQL、H2 存储不到时间戳 return LocalDateTimeUtil.of(randomDate()).withNano(0); } public static Short randomShort() { return (short) RandomUtil.randomInt(0, Short.MAX_VALUE); } public static Set randomSet(Class clazz) { return Stream.iterate(0, i -> i).limit(RandomUtil.randomInt(1, RANDOM_COLLECTION_LENGTH)) .map(i -> randomPojo(clazz)).collect(Collectors.toSet()); } public static Integer randomCommonStatus() { return RandomUtil.randomEle(CommonStatusEnum.values()).getStatus(); } public static String randomEmail() { return randomString() + "@qq.com"; } public static String randomURL() { return "https://www.yixiang.co/" + randomString(); } @SafeVarargs public static T randomPojo(Class clazz, Consumer... consumers) { T pojo = PODAM_FACTORY.manufacturePojo(clazz); // 非空时,回调逻辑。通过它,可以实现 Pojo 的进一步处理 if (ArrayUtil.isNotEmpty(consumers)) { Arrays.stream(consumers).forEach(consumer -> consumer.accept(pojo)); } return pojo; } @SafeVarargs public static T randomPojo(Class clazz, Type type, Consumer... consumers) { T pojo = PODAM_FACTORY.manufacturePojo(clazz, type); // 非空时,回调逻辑。通过它,可以实现 Pojo 的进一步处理 if (ArrayUtil.isNotEmpty(consumers)) { Arrays.stream(consumers).forEach(consumer -> consumer.accept(pojo)); } return pojo; } @SafeVarargs public static List randomPojoList(Class clazz, Consumer... consumers) { int size = RandomUtil.randomInt(1, RANDOM_COLLECTION_LENGTH); return Stream.iterate(0, i -> i).limit(size).map(o -> randomPojo(clazz, consumers)) .collect(Collectors.toList()); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/pom.xml ================================================ co.yixiang.boot yshop-framework ${revision} 4.0.0 yshop-spring-boot-starter-web jar ${project.artifactId} Web 框架,全局异常、API 日志、脱敏、错误码等 https://gitee.com/guchengwuyue/yshop-drink co.yixiang.boot yshop-common org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-configuration-processor true com.github.xiaoymin knife4j-openapi3-jakarta-spring-boot-starter org.springdoc springdoc-openapi-starter-webmvc-api org.springframework.security spring-security-core provided co.yixiang.boot yshop-module-infra-api ${revision} co.yixiang.boot yshop-module-system-api ${revision} org.jsoup jsoup org.springframework.boot spring-boot-starter-test test org.mockito mockito-inline test ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/apilog/config/YshopApiLogAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.apilog.config; import co.yixiang.yshop.framework.apilog.core.filter.ApiAccessLogFilter; import co.yixiang.yshop.framework.apilog.core.interceptor.ApiAccessLogInterceptor; import co.yixiang.yshop.framework.apilog.core.service.ApiAccessLogFrameworkService; import co.yixiang.yshop.framework.apilog.core.service.ApiAccessLogFrameworkServiceImpl; import co.yixiang.yshop.framework.apilog.core.service.ApiErrorLogFrameworkService; import co.yixiang.yshop.framework.apilog.core.service.ApiErrorLogFrameworkServiceImpl; import co.yixiang.yshop.framework.common.enums.WebFilterOrderEnum; import co.yixiang.yshop.framework.web.config.WebProperties; import co.yixiang.yshop.framework.web.config.YshopWebAutoConfiguration; import co.yixiang.yshop.module.infra.api.logger.ApiAccessLogApi; import co.yixiang.yshop.module.infra.api.logger.ApiErrorLogApi; import jakarta.servlet.Filter; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @AutoConfiguration(after = YshopWebAutoConfiguration.class) public class YshopApiLogAutoConfiguration implements WebMvcConfigurer { @Bean @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") public ApiAccessLogFrameworkService apiAccessLogFrameworkService(ApiAccessLogApi apiAccessLogApi) { return new ApiAccessLogFrameworkServiceImpl(apiAccessLogApi); } @Bean @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") public ApiErrorLogFrameworkService apiErrorLogFrameworkService(ApiErrorLogApi apiErrorLogApi) { return new ApiErrorLogFrameworkServiceImpl(apiErrorLogApi); } /** * 创建 ApiAccessLogFilter Bean,记录 API 请求日志 */ @Bean @ConditionalOnProperty(prefix = "yshop.access-log", value = "enable", matchIfMissing = true) // 允许使用 yshop.access-log.enable=false 禁用访问日志 public FilterRegistrationBean apiAccessLogFilter(WebProperties webProperties, @Value("${spring.application.name}") String applicationName, ApiAccessLogFrameworkService apiAccessLogFrameworkService) { ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogFrameworkService); return createFilterBean(filter, WebFilterOrderEnum.API_ACCESS_LOG_FILTER); } private static FilterRegistrationBean createFilterBean(T filter, Integer order) { FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); bean.setOrder(order); return bean; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new ApiAccessLogInterceptor()); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/apilog/core/annotation/ApiAccessLog.java ================================================ package co.yixiang.yshop.framework.apilog.core.annotation; import co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 访问日志注解 * * @author yshop */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ApiAccessLog { // ========== 开关字段 ========== /** * 是否记录访问日志 */ boolean enable() default true; /** * 是否记录请求参数 * * 默认记录,主要考虑请求数据一般不大。可手动设置为 false 进行关闭 */ boolean requestEnable() default true; /** * 是否记录响应结果 * * 默认不记录,主要考虑响应数据可能比较大。可手动设置为 true 进行打开 */ boolean responseEnable() default false; /** * 敏感参数数组 * * 添加后,请求参数、响应结果不会记录该参数 */ String[] sanitizeKeys() default {}; // ========== 模块字段 ========== /** * 操作模块 * * 为空时,会尝试读取 {@link io.swagger.v3.oas.annotations.tags.Tag#name()} 属性 */ String operateModule() default ""; /** * 操作名 * * 为空时,会尝试读取 {@link io.swagger.v3.oas.annotations.Operation#summary()} 属性 */ String operateName() default ""; /** * 操作分类 * * 实际并不是数组,因为枚举不能设置 null 作为默认值 */ OperateTypeEnum[] operateType() default {}; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/apilog/core/enums/OperateTypeEnum.java ================================================ package co.yixiang.yshop.framework.apilog.core.enums; import lombok.AllArgsConstructor; import lombok.Getter; /** * 操作日志的操作类型 * * @author ruoyi */ @Getter @AllArgsConstructor public enum OperateTypeEnum { /** * 查询 */ GET(1), /** * 新增 */ CREATE(2), /** * 修改 */ UPDATE(3), /** * 删除 */ DELETE(4), /** * 导出 */ EXPORT(5), /** * 导入 */ IMPORT(6), /** * 其它 * * 在无法归类时,可以选择使用其它。因为还有操作名可以进一步标识 */ OTHER(0); /** * 类型 */ private final Integer type; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/apilog/core/filter/ApiAccessLogFilter.java ================================================ package co.yixiang.yshop.framework.apilog.core.filter; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.exceptions.ExceptionUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.apilog.core.annotation.ApiAccessLog; import co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum; import co.yixiang.yshop.framework.apilog.core.service.ApiAccessLogFrameworkService; import co.yixiang.yshop.framework.common.exception.enums.GlobalErrorCodeConstants; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.framework.common.util.monitor.TracerUtils; import co.yixiang.yshop.framework.common.util.servlet.ServletUtils; import co.yixiang.yshop.framework.web.config.WebProperties; import co.yixiang.yshop.framework.web.core.filter.ApiRequestFilter; import co.yixiang.yshop.framework.web.core.util.WebFrameworkUtils; import co.yixiang.yshop.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.method.HandlerMethod; import java.io.IOException; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.Iterator; import java.util.Map; import static co.yixiang.yshop.framework.apilog.core.interceptor.ApiAccessLogInterceptor.*; import static co.yixiang.yshop.framework.common.util.json.JsonUtils.toJsonString; /** * API 访问日志 Filter * * 目的:记录 API 访问日志到数据库中 * * @author yshop */ @Slf4j public class ApiAccessLogFilter extends ApiRequestFilter { private static final String[] SANITIZE_KEYS = new String[]{"password", "token", "accessToken", "refreshToken"}; private final String applicationName; private final ApiAccessLogFrameworkService apiAccessLogFrameworkService; public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogFrameworkService apiAccessLogFrameworkService) { super(webProperties); this.applicationName = applicationName; this.apiAccessLogFrameworkService = apiAccessLogFrameworkService; } @Override @SuppressWarnings("NullableProblems") protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 获得开始时间 LocalDateTime beginTime = LocalDateTime.now(); // 提前获得参数,避免 XssFilter 过滤处理 Map queryString = ServletUtils.getParamMap(request); String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null; try { // 继续过滤器 filterChain.doFilter(request, response); // 正常执行,记录日志 createApiAccessLog(request, beginTime, queryString, requestBody, null); } catch (Exception ex) { // 异常执行,记录日志 createApiAccessLog(request, beginTime, queryString, requestBody, ex); throw ex; } } private void createApiAccessLog(HttpServletRequest request, LocalDateTime beginTime, Map queryString, String requestBody, Exception ex) { ApiAccessLogCreateReqDTO accessLog = new ApiAccessLogCreateReqDTO(); try { boolean enable = buildApiAccessLog(accessLog, request, beginTime, queryString, requestBody, ex); if (!enable) { return; } apiAccessLogFrameworkService.createApiAccessLog(accessLog); } catch (Throwable th) { log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), toJsonString(accessLog), th); } } private boolean buildApiAccessLog(ApiAccessLogCreateReqDTO accessLog, HttpServletRequest request, LocalDateTime beginTime, Map queryString, String requestBody, Exception ex) { // 判断:是否要记录操作日志 HandlerMethod handlerMethod = (HandlerMethod) request.getAttribute(ATTRIBUTE_HANDLER_METHOD); ApiAccessLog accessLogAnnotation = null; if (handlerMethod != null) { accessLogAnnotation = handlerMethod.getMethodAnnotation(ApiAccessLog.class); if (accessLogAnnotation != null && BooleanUtil.isFalse(accessLogAnnotation.enable())) { return false; } } // 处理用户信息 accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request)) .setUserType(WebFrameworkUtils.getLoginUserType(request)); // 设置访问结果 CommonResult result = WebFrameworkUtils.getCommonResult(request); if (result != null) { accessLog.setResultCode(result.getCode()).setResultMsg(result.getMsg()); } else if (ex != null) { accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode()) .setResultMsg(ExceptionUtil.getRootCauseMessage(ex)); } else { accessLog.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode()).setResultMsg(""); } // 设置请求字段 accessLog.setTraceId(TracerUtils.getTraceId()).setApplicationName(applicationName) .setRequestUrl(request.getRequestURI()).setRequestMethod(request.getMethod()) .setUserAgent(ServletUtils.getUserAgent(request)).setUserIp(ServletUtils.getClientIP(request)); String[] sanitizeKeys = accessLogAnnotation != null ? accessLogAnnotation.sanitizeKeys() : null; Boolean requestEnable = accessLogAnnotation != null ? accessLogAnnotation.requestEnable() : Boolean.TRUE; if (!BooleanUtil.isFalse(requestEnable)) { // 默认记录,所以判断 !false Map requestParams = MapUtil.builder() .put("query", sanitizeMap(queryString, sanitizeKeys)) .put("body", sanitizeJson(requestBody, sanitizeKeys)).build(); accessLog.setRequestParams(toJsonString(requestParams)); } Boolean responseEnable = accessLogAnnotation != null ? accessLogAnnotation.responseEnable() : Boolean.FALSE; if (BooleanUtil.isTrue(responseEnable)) { // 默认不记录,默认强制要求 true accessLog.setResponseBody(sanitizeJson(result, sanitizeKeys)); } // 持续时间 accessLog.setBeginTime(beginTime).setEndTime(LocalDateTime.now()) .setDuration((int) LocalDateTimeUtil.between(accessLog.getBeginTime(), accessLog.getEndTime(), ChronoUnit.MILLIS)); // 操作模块 if (handlerMethod != null) { Tag tagAnnotation = handlerMethod.getBeanType().getAnnotation(Tag.class); Operation operationAnnotation = handlerMethod.getMethodAnnotation(Operation.class); String operateModule = accessLogAnnotation != null ? accessLogAnnotation.operateModule() : tagAnnotation != null ? StrUtil.nullToDefault(tagAnnotation.name(), tagAnnotation.description()) : null; String operateName = accessLogAnnotation != null ? accessLogAnnotation.operateName() : operationAnnotation != null ? operationAnnotation.summary() : null; OperateTypeEnum operateType = accessLogAnnotation != null && accessLogAnnotation.operateType().length > 0 ? accessLogAnnotation.operateType()[0] : parseOperateLogType(request); accessLog.setOperateModule(operateModule).setOperateName(operateName).setOperateType(operateType.getType()); } return true; } // ========== 解析 @ApiAccessLog、@Swagger 注解 ========== private static OperateTypeEnum parseOperateLogType(HttpServletRequest request) { RequestMethod requestMethod = RequestMethod.resolve(request.getMethod()); if (requestMethod == null) { return OperateTypeEnum.OTHER; } switch (requestMethod) { case GET: return OperateTypeEnum.GET; case POST: return OperateTypeEnum.CREATE; case PUT: return OperateTypeEnum.UPDATE; case DELETE: return OperateTypeEnum.DELETE; default: return OperateTypeEnum.OTHER; } } // ========== 请求和响应的脱敏逻辑,移除类似 password、token 等敏感字段 ========== private static String sanitizeMap(Map map, String[] sanitizeKeys) { if (CollUtil.isNotEmpty(map)) { return null; } if (sanitizeKeys != null) { MapUtil.removeAny(map, sanitizeKeys); } MapUtil.removeAny(map, SANITIZE_KEYS); return JsonUtils.toJsonString(map); } private static String sanitizeJson(String jsonString, String[] sanitizeKeys) { if (StrUtil.isEmpty(jsonString)) { return null; } try { JsonNode rootNode = JsonUtils.parseTree(jsonString); sanitizeJson(rootNode, sanitizeKeys); return JsonUtils.toJsonString(rootNode); } catch (Exception e) { // 脱敏失败的情况下,直接忽略异常,避免影响用户请求 log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e); return jsonString; } } private static String sanitizeJson(CommonResult commonResult, String[] sanitizeKeys) { if (commonResult == null) { return null; } String jsonString = toJsonString(commonResult); try { JsonNode rootNode = JsonUtils.parseTree(jsonString); sanitizeJson(rootNode.get("data"), sanitizeKeys); // 只处理 data 字段,不处理 code、msg 字段,避免错误被脱敏掉 return JsonUtils.toJsonString(rootNode); } catch (Exception e) { // 脱敏失败的情况下,直接忽略异常,避免影响用户请求 log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e); return jsonString; } } private static void sanitizeJson(JsonNode node, String[] sanitizeKeys) { // 情况一:数组,遍历处理 if (node.isArray()) { for (JsonNode childNode : node) { sanitizeJson(childNode, sanitizeKeys); } return; } // 情况二:非 Object,只是某个值,直接返回 if (!node.isObject()) { return; } // 情况三:Object,遍历处理 Iterator> iterator = node.properties().iterator(); while (iterator.hasNext()) { Map.Entry entry = iterator.next(); if (ArrayUtil.contains(sanitizeKeys, entry.getKey()) || ArrayUtil.contains(SANITIZE_KEYS, entry.getKey())) { iterator.remove(); continue; } sanitizeJson(entry.getValue(), sanitizeKeys); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java ================================================ package co.yixiang.yshop.framework.apilog.core.interceptor; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.util.servlet.ServletUtils; import co.yixiang.yshop.framework.common.util.spring.SpringUtils; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StopWatch; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import java.util.Map; /** * API 访问日志 Interceptor * * 目的:在非 prod 环境时,打印 request 和 response 两条日志到日志文件(控制台)中。 * * @author yshop */ @Slf4j public class ApiAccessLogInterceptor implements HandlerInterceptor { public static final String ATTRIBUTE_HANDLER_METHOD = "HANDLER_METHOD"; private static final String ATTRIBUTE_STOP_WATCH = "ApiAccessLogInterceptor.StopWatch"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 记录 HandlerMethod,提供给 ApiAccessLogFilter 使用 HandlerMethod handlerMethod = handler instanceof HandlerMethod ? (HandlerMethod) handler : null; if (handlerMethod != null) { request.setAttribute(ATTRIBUTE_HANDLER_METHOD, handlerMethod); } // 打印 request 日志 if (!SpringUtils.isProd()) { Map queryString = ServletUtils.getParamMap(request); String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null; if (CollUtil.isEmpty(queryString) && StrUtil.isEmpty(requestBody)) { log.info("[preHandle][开始请求 URL({}) 无参数]", request.getRequestURI()); } else { log.info("[preHandle][开始请求 URL({}) 参数({})]", request.getRequestURI(), StrUtil.blankToDefault(requestBody, queryString.toString())); } // 计时 StopWatch stopWatch = new StopWatch(); stopWatch.start(); request.setAttribute(ATTRIBUTE_STOP_WATCH, stopWatch); } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 打印 response 日志 if (!SpringUtils.isProd()) { StopWatch stopWatch = (StopWatch) request.getAttribute(ATTRIBUTE_STOP_WATCH); stopWatch.stop(); log.info("[afterCompletion][完成请求 URL({}) 耗时({} ms)]", request.getRequestURI(), stopWatch.getTotalTimeMillis()); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/apilog/core/service/ApiAccessLogFrameworkService.java ================================================ package co.yixiang.yshop.framework.apilog.core.service; import co.yixiang.yshop.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; /** * API 访问日志 Framework Service 接口 * * @author yshop */ public interface ApiAccessLogFrameworkService { /** * 创建 API 访问日志 * * @param reqDTO API 访问日志 */ void createApiAccessLog(ApiAccessLogCreateReqDTO reqDTO); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java ================================================ package co.yixiang.yshop.framework.apilog.core.service; import co.yixiang.yshop.module.infra.api.logger.ApiAccessLogApi; import co.yixiang.yshop.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; import lombok.RequiredArgsConstructor; import org.springframework.scheduling.annotation.Async; /** * API 访问日志 Framework Service 实现类 * * 基于 {@link ApiAccessLogApi} 服务,记录访问日志 * * @author yshop */ @RequiredArgsConstructor public class ApiAccessLogFrameworkServiceImpl implements ApiAccessLogFrameworkService { private final ApiAccessLogApi apiAccessLogApi; @Override @Async public void createApiAccessLog(ApiAccessLogCreateReqDTO reqDTO) { apiAccessLogApi.createApiAccessLog(reqDTO); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/apilog/core/service/ApiErrorLogFrameworkService.java ================================================ package co.yixiang.yshop.framework.apilog.core.service; import co.yixiang.yshop.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; /** * API 错误日志 Framework Service 接口 * * @author yshop */ public interface ApiErrorLogFrameworkService { /** * 创建 API 错误日志 * * @param reqDTO API 错误日志 */ void createApiErrorLog(ApiErrorLogCreateReqDTO reqDTO); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java ================================================ package co.yixiang.yshop.framework.apilog.core.service; import co.yixiang.yshop.module.infra.api.logger.ApiErrorLogApi; import co.yixiang.yshop.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; import lombok.RequiredArgsConstructor; import org.springframework.scheduling.annotation.Async; /** * API 错误日志 Framework Service 实现类 * * 基于 {@link ApiErrorLogApi} 服务,记录错误日志 * * @author yshop */ @RequiredArgsConstructor public class ApiErrorLogFrameworkServiceImpl implements ApiErrorLogFrameworkService { private final ApiErrorLogApi apiErrorLogApi; @Override @Async public void createApiErrorLog(ApiErrorLogCreateReqDTO reqDTO) { apiErrorLogApi.createApiErrorLog(reqDTO); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/banner/config/YshopBannerAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.banner.config; import co.yixiang.yshop.framework.banner.core.BannerApplicationRunner; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.context.annotation.Bean; /** * Banner 的自动配置类 * * @author yshop */ @AutoConfiguration public class YshopBannerAutoConfiguration { @Bean public BannerApplicationRunner bannerApplicationRunner() { return new BannerApplicationRunner(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/banner/core/BannerApplicationRunner.java ================================================ package co.yixiang.yshop.framework.banner.core; import cn.hutool.core.thread.ThreadUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.util.ClassUtils; import java.util.concurrent.TimeUnit; /** * 项目启动成功后,提供文档相关的地址 * * @author yshop */ @Slf4j public class BannerApplicationRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) { ThreadUtil.execute(() -> { ThreadUtil.sleep(1, TimeUnit.SECONDS); // 延迟 1 秒,保证输出到结尾 log.info("\n----------------------------------------------------------\n\t" + "项目启动成功!\n\t" + "接口文档: \t{} \n\t" + "开发文档: \t{} \n\t" + "----------------------------------------------------------", "https://www.yixiang.co/api-doc/", "https://www.yixiang.co"); }); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/base/annotation/DesensitizeBy.java ================================================ package co.yixiang.yshop.framework.desensitize.core.base.annotation; import co.yixiang.yshop.framework.desensitize.core.base.handler.DesensitizationHandler; import co.yixiang.yshop.framework.desensitize.core.base.serializer.StringDesensitizeSerializer; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 顶级脱敏注解,自定义注解需要使用此注解 * * @author gaibu */ @Documented @Target(ElementType.ANNOTATION_TYPE) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside // 此注解是其他所有 jackson 注解的元注解,打上了此注解的注解表明是 jackson 注解的一部分 @JsonSerialize(using = StringDesensitizeSerializer.class) // 指定序列化器 public @interface DesensitizeBy { /** * 脱敏处理器 */ @SuppressWarnings("rawtypes") Class handler(); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/base/handler/DesensitizationHandler.java ================================================ package co.yixiang.yshop.framework.desensitize.core.base.handler; import java.lang.annotation.Annotation; /** * 脱敏处理器接口 * * @author gaibu */ public interface DesensitizationHandler { /** * 脱敏 * * @param origin 原始字符串 * @param annotation 注解信息 * @return 脱敏后的字符串 */ String desensitize(String origin, T annotation); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/base/serializer/StringDesensitizeSerializer.java ================================================ package co.yixiang.yshop.framework.desensitize.core.base.serializer; import cn.hutool.core.annotation.AnnotationUtil; import cn.hutool.core.lang.Singleton; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.desensitize.core.base.annotation.DesensitizeBy; import co.yixiang.yshop.framework.desensitize.core.base.handler.DesensitizationHandler; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.BeanProperty; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.ContextualSerializer; import com.fasterxml.jackson.databind.ser.std.StdSerializer; import lombok.Getter; import lombok.Setter; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Field; /** * 脱敏序列化器 * * 实现 JSON 返回数据时,使用 {@link DesensitizationHandler} 对声明脱敏注解的字段,进行脱敏处理。 * * @author gaibu */ @SuppressWarnings("rawtypes") public class StringDesensitizeSerializer extends StdSerializer implements ContextualSerializer { @Getter @Setter private DesensitizationHandler desensitizationHandler; protected StringDesensitizeSerializer() { super(String.class); } @Override public JsonSerializer createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) { DesensitizeBy annotation = beanProperty.getAnnotation(DesensitizeBy.class); if (annotation == null) { return this; } // 创建一个 StringDesensitizeSerializer 对象,使用 DesensitizeBy 对应的处理器 StringDesensitizeSerializer serializer = new StringDesensitizeSerializer(); serializer.setDesensitizationHandler(Singleton.get(annotation.handler())); return serializer; } @Override @SuppressWarnings("unchecked") public void serialize(String value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException { if (StrUtil.isBlank(value)) { gen.writeNull(); return; } // 获取序列化字段 Field field = getField(gen); // 自定义处理器 DesensitizeBy[] annotations = AnnotationUtil.getCombinationAnnotations(field, DesensitizeBy.class); if (ArrayUtil.isEmpty(annotations)) { gen.writeString(value); return; } for (Annotation annotation : field.getAnnotations()) { if (AnnotationUtil.hasAnnotation(annotation.annotationType(), DesensitizeBy.class)) { value = this.desensitizationHandler.desensitize(value, annotation); gen.writeString(value); return; } } gen.writeString(value); } /** * 获取字段 * * @param generator JsonGenerator * @return 字段 */ private Field getField(JsonGenerator generator) { String currentName = generator.getOutputContext().getCurrentName(); Object currentValue = generator.getCurrentValue(); Class currentValueClass = currentValue.getClass(); return ReflectUtil.getField(currentValueClass, currentName); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/regex/annotation/EmailDesensitize.java ================================================ package co.yixiang.yshop.framework.desensitize.core.regex.annotation; import co.yixiang.yshop.framework.desensitize.core.base.annotation.DesensitizeBy; import co.yixiang.yshop.framework.desensitize.core.regex.handler.EmailDesensitizationHandler; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 邮箱脱敏注解 * * @author gaibu */ @Documented @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside @DesensitizeBy(handler = EmailDesensitizationHandler.class) public @interface EmailDesensitize { /** * 匹配的正则表达式 */ String regex() default "(^.)[^@]*(@.*$)"; /** * 替换规则,邮箱; * * 比如:example@gmail.com 脱敏之后为 e****@gmail.com */ String replacer() default "$1****$2"; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/regex/annotation/RegexDesensitize.java ================================================ package co.yixiang.yshop.framework.desensitize.core.regex.annotation; import co.yixiang.yshop.framework.desensitize.core.base.annotation.DesensitizeBy; import co.yixiang.yshop.framework.desensitize.core.regex.handler.DefaultRegexDesensitizationHandler; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 正则脱敏注解 * * @author gaibu */ @Documented @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside @DesensitizeBy(handler = DefaultRegexDesensitizationHandler.class) public @interface RegexDesensitize { /** * 匹配的正则表达式(默认匹配所有) */ String regex() default "^[\\s\\S]*$"; /** * 替换规则,会将匹配到的字符串全部替换成 replacer * * 例如:regex=123; replacer=****** * 原始字符串 123456789 * 脱敏后字符串 ******456789 */ String replacer() default "******"; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/regex/handler/AbstractRegexDesensitizationHandler.java ================================================ package co.yixiang.yshop.framework.desensitize.core.regex.handler; import co.yixiang.yshop.framework.desensitize.core.base.handler.DesensitizationHandler; import java.lang.annotation.Annotation; /** * 正则表达式脱敏处理器抽象类,已实现通用的方法 * * @author gaibu */ public abstract class AbstractRegexDesensitizationHandler implements DesensitizationHandler { @Override public String desensitize(String origin, T annotation) { String regex = getRegex(annotation); String replacer = getReplacer(annotation); return origin.replaceAll(regex, replacer); } /** * 获取注解上的 regex 参数 * * @param annotation 注解信息 * @return 正则表达式 */ abstract String getRegex(T annotation); /** * 获取注解上的 replacer 参数 * * @param annotation 注解信息 * @return 待替换的字符串 */ abstract String getReplacer(T annotation); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/regex/handler/DefaultRegexDesensitizationHandler.java ================================================ package co.yixiang.yshop.framework.desensitize.core.regex.handler; import co.yixiang.yshop.framework.desensitize.core.regex.annotation.RegexDesensitize; /** * {@link RegexDesensitize} 的正则脱敏处理器 * * @author gaibu */ public class DefaultRegexDesensitizationHandler extends AbstractRegexDesensitizationHandler { @Override String getRegex(RegexDesensitize annotation) { return annotation.regex(); } @Override String getReplacer(RegexDesensitize annotation) { return annotation.replacer(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/regex/handler/EmailDesensitizationHandler.java ================================================ package co.yixiang.yshop.framework.desensitize.core.regex.handler; import co.yixiang.yshop.framework.desensitize.core.regex.annotation.EmailDesensitize; /** * {@link EmailDesensitize} 的脱敏处理器 * * @author gaibu */ public class EmailDesensitizationHandler extends AbstractRegexDesensitizationHandler { @Override String getRegex(EmailDesensitize annotation) { return annotation.regex(); } @Override String getReplacer(EmailDesensitize annotation) { return annotation.replacer(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/slider/annotation/BankCardDesensitize.java ================================================ package co.yixiang.yshop.framework.desensitize.core.slider.annotation; import co.yixiang.yshop.framework.desensitize.core.base.annotation.DesensitizeBy; import co.yixiang.yshop.framework.desensitize.core.slider.handler.BankCardDesensitization; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 银行卡号 * * @author gaibu */ @Documented @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside @DesensitizeBy(handler = BankCardDesensitization.class) public @interface BankCardDesensitize { /** * 前缀保留长度 */ int prefixKeep() default 6; /** * 后缀保留长度 */ int suffixKeep() default 2; /** * 替换规则,银行卡号; 比如:9988002866797031 脱敏之后为 998800********31 */ String replacer() default "*"; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/slider/annotation/CarLicenseDesensitize.java ================================================ package co.yixiang.yshop.framework.desensitize.core.slider.annotation; import co.yixiang.yshop.framework.desensitize.core.base.annotation.DesensitizeBy; import co.yixiang.yshop.framework.desensitize.core.slider.handler.CarLicenseDesensitization; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 车牌号 * * @author gaibu */ @Documented @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside @DesensitizeBy(handler = CarLicenseDesensitization.class) public @interface CarLicenseDesensitize { /** * 前缀保留长度 */ int prefixKeep() default 3; /** * 后缀保留长度 */ int suffixKeep() default 1; /** * 替换规则,车牌号;比如:粤A66666 脱敏之后为粤A6***6 */ String replacer() default "*"; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/slider/annotation/ChineseNameDesensitize.java ================================================ package co.yixiang.yshop.framework.desensitize.core.slider.annotation; import co.yixiang.yshop.framework.desensitize.core.base.annotation.DesensitizeBy; import co.yixiang.yshop.framework.desensitize.core.slider.handler.ChineseNameDesensitization; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 中文名 * * @author gaibu */ @Documented @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside @DesensitizeBy(handler = ChineseNameDesensitization.class) public @interface ChineseNameDesensitize { /** * 前缀保留长度 */ int prefixKeep() default 1; /** * 后缀保留长度 */ int suffixKeep() default 0; /** * 替换规则,中文名;比如:刘子豪脱敏之后为刘** */ String replacer() default "*"; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/slider/annotation/FixedPhoneDesensitize.java ================================================ package co.yixiang.yshop.framework.desensitize.core.slider.annotation; import co.yixiang.yshop.framework.desensitize.core.base.annotation.DesensitizeBy; import co.yixiang.yshop.framework.desensitize.core.slider.handler.FixedPhoneDesensitization; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 固定电话 * * @author gaibu */ @Documented @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside @DesensitizeBy(handler = FixedPhoneDesensitization.class) public @interface FixedPhoneDesensitize { /** * 前缀保留长度 */ int prefixKeep() default 4; /** * 后缀保留长度 */ int suffixKeep() default 2; /** * 替换规则,固定电话;比如:01086551122 脱敏之后为 0108*****22 */ String replacer() default "*"; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/slider/annotation/IdCardDesensitize.java ================================================ package co.yixiang.yshop.framework.desensitize.core.slider.annotation; import co.yixiang.yshop.framework.desensitize.core.base.annotation.DesensitizeBy; import co.yixiang.yshop.framework.desensitize.core.slider.handler.IdCardDesensitization; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 身份证 * * @author gaibu */ @Documented @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside @DesensitizeBy(handler = IdCardDesensitization.class) public @interface IdCardDesensitize { /** * 前缀保留长度 */ int prefixKeep() default 6; /** * 后缀保留长度 */ int suffixKeep() default 2; /** * 替换规则,身份证号码;比如:530321199204074611 脱敏之后为 530321**********11 */ String replacer() default "*"; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/slider/annotation/MobileDesensitize.java ================================================ package co.yixiang.yshop.framework.desensitize.core.slider.annotation; import co.yixiang.yshop.framework.desensitize.core.base.annotation.DesensitizeBy; import co.yixiang.yshop.framework.desensitize.core.slider.handler.MobileDesensitization; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 手机号 * * @author gaibu */ @Documented @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside @DesensitizeBy(handler = MobileDesensitization.class) public @interface MobileDesensitize { /** * 前缀保留长度 */ int prefixKeep() default 3; /** * 后缀保留长度 */ int suffixKeep() default 4; /** * 替换规则,手机号;比如:13248765917 脱敏之后为 132****5917 */ String replacer() default "*"; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/slider/annotation/PasswordDesensitize.java ================================================ package co.yixiang.yshop.framework.desensitize.core.slider.annotation; import co.yixiang.yshop.framework.desensitize.core.base.annotation.DesensitizeBy; import co.yixiang.yshop.framework.desensitize.core.slider.handler.PasswordDesensitization; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 密码 * * @author gaibu */ @Documented @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside @DesensitizeBy(handler = PasswordDesensitization.class) public @interface PasswordDesensitize { /** * 前缀保留长度 */ int prefixKeep() default 0; /** * 后缀保留长度 */ int suffixKeep() default 0; /** * 替换规则,密码; * * 比如:123456 脱敏之后为 ****** */ String replacer() default "*"; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/slider/annotation/SliderDesensitize.java ================================================ package co.yixiang.yshop.framework.desensitize.core.slider.annotation; import co.yixiang.yshop.framework.desensitize.core.base.annotation.DesensitizeBy; import co.yixiang.yshop.framework.desensitize.core.slider.handler.DefaultDesensitizationHandler; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 滑动脱敏注解 * * @author gaibu */ @Documented @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside @DesensitizeBy(handler = DefaultDesensitizationHandler.class) public @interface SliderDesensitize { /** * 后缀保留长度 */ int suffixKeep() default 0; /** * 替换规则,会将前缀后缀保留后,全部替换成 replacer * * 例如:prefixKeep = 1; suffixKeep = 2; replacer = "*"; * 原始字符串 123456 * 脱敏后 1***56 */ String replacer() default "*"; /** * 前缀保留长度 */ int prefixKeep() default 0; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/slider/handler/AbstractSliderDesensitizationHandler.java ================================================ package co.yixiang.yshop.framework.desensitize.core.slider.handler; import co.yixiang.yshop.framework.desensitize.core.base.handler.DesensitizationHandler; import java.lang.annotation.Annotation; /** * 滑动脱敏处理器抽象类,已实现通用的方法 * * @author gaibu */ public abstract class AbstractSliderDesensitizationHandler implements DesensitizationHandler { @Override public String desensitize(String origin, T annotation) { int prefixKeep = getPrefixKeep(annotation); int suffixKeep = getSuffixKeep(annotation); String replacer = getReplacer(annotation); int length = origin.length(); // 情况一:原始字符串长度小于等于保留长度,则原始字符串全部替换 if (prefixKeep >= length || suffixKeep >= length) { return buildReplacerByLength(replacer, length); } // 情况二:原始字符串长度小于等于前后缀保留字符串长度,则原始字符串全部替换 if ((prefixKeep + suffixKeep) >= length) { return buildReplacerByLength(replacer, length); } // 情况三:原始字符串长度大于前后缀保留字符串长度,则替换中间字符串 int interval = length - prefixKeep - suffixKeep; return origin.substring(0, prefixKeep) + buildReplacerByLength(replacer, interval) + origin.substring(prefixKeep + interval); } /** * 根据长度循环构建替换符 * * @param replacer 替换符 * @param length 长度 * @return 构建后的替换符 */ private String buildReplacerByLength(String replacer, int length) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < length; i++) { builder.append(replacer); } return builder.toString(); } /** * 前缀保留长度 * * @param annotation 注解信息 * @return 前缀保留长度 */ abstract Integer getPrefixKeep(T annotation); /** * 后缀保留长度 * * @param annotation 注解信息 * @return 后缀保留长度 */ abstract Integer getSuffixKeep(T annotation); /** * 替换符 * * @param annotation 注解信息 * @return 替换符 */ abstract String getReplacer(T annotation); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/slider/handler/BankCardDesensitization.java ================================================ package co.yixiang.yshop.framework.desensitize.core.slider.handler; import co.yixiang.yshop.framework.desensitize.core.slider.annotation.BankCardDesensitize; /** * {@link BankCardDesensitize} 的脱敏处理器 * * @author gaibu */ public class BankCardDesensitization extends AbstractSliderDesensitizationHandler { @Override Integer getPrefixKeep(BankCardDesensitize annotation) { return annotation.prefixKeep(); } @Override Integer getSuffixKeep(BankCardDesensitize annotation) { return annotation.suffixKeep(); } @Override String getReplacer(BankCardDesensitize annotation) { return annotation.replacer(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/slider/handler/CarLicenseDesensitization.java ================================================ package co.yixiang.yshop.framework.desensitize.core.slider.handler; import co.yixiang.yshop.framework.desensitize.core.slider.annotation.CarLicenseDesensitize; /** * {@link CarLicenseDesensitize} 的脱敏处理器 * * @author gaibu */ public class CarLicenseDesensitization extends AbstractSliderDesensitizationHandler { @Override Integer getPrefixKeep(CarLicenseDesensitize annotation) { return annotation.prefixKeep(); } @Override Integer getSuffixKeep(CarLicenseDesensitize annotation) { return annotation.suffixKeep(); } @Override String getReplacer(CarLicenseDesensitize annotation) { return annotation.replacer(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/slider/handler/ChineseNameDesensitization.java ================================================ package co.yixiang.yshop.framework.desensitize.core.slider.handler; import co.yixiang.yshop.framework.desensitize.core.slider.annotation.ChineseNameDesensitize; /** * {@link ChineseNameDesensitize} 的脱敏处理器 * * @author gaibu */ public class ChineseNameDesensitization extends AbstractSliderDesensitizationHandler { @Override Integer getPrefixKeep(ChineseNameDesensitize annotation) { return annotation.prefixKeep(); } @Override Integer getSuffixKeep(ChineseNameDesensitize annotation) { return annotation.suffixKeep(); } @Override String getReplacer(ChineseNameDesensitize annotation) { return annotation.replacer(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/slider/handler/DefaultDesensitizationHandler.java ================================================ package co.yixiang.yshop.framework.desensitize.core.slider.handler; import co.yixiang.yshop.framework.desensitize.core.slider.annotation.SliderDesensitize; /** * {@link SliderDesensitize} 的脱敏处理器 * * @author gaibu */ public class DefaultDesensitizationHandler extends AbstractSliderDesensitizationHandler { @Override Integer getPrefixKeep(SliderDesensitize annotation) { return annotation.prefixKeep(); } @Override Integer getSuffixKeep(SliderDesensitize annotation) { return annotation.suffixKeep(); } @Override String getReplacer(SliderDesensitize annotation) { return annotation.replacer(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/slider/handler/FixedPhoneDesensitization.java ================================================ package co.yixiang.yshop.framework.desensitize.core.slider.handler; import co.yixiang.yshop.framework.desensitize.core.slider.annotation.FixedPhoneDesensitize; /** * {@link FixedPhoneDesensitize} 的脱敏处理器 * * @author gaibu */ public class FixedPhoneDesensitization extends AbstractSliderDesensitizationHandler { @Override Integer getPrefixKeep(FixedPhoneDesensitize annotation) { return annotation.prefixKeep(); } @Override Integer getSuffixKeep(FixedPhoneDesensitize annotation) { return annotation.suffixKeep(); } @Override String getReplacer(FixedPhoneDesensitize annotation) { return annotation.replacer(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/slider/handler/IdCardDesensitization.java ================================================ package co.yixiang.yshop.framework.desensitize.core.slider.handler; import co.yixiang.yshop.framework.desensitize.core.slider.annotation.IdCardDesensitize; /** * {@link IdCardDesensitize} 的脱敏处理器 * * @author gaibu */ public class IdCardDesensitization extends AbstractSliderDesensitizationHandler { @Override Integer getPrefixKeep(IdCardDesensitize annotation) { return annotation.prefixKeep(); } @Override Integer getSuffixKeep(IdCardDesensitize annotation) { return annotation.suffixKeep(); } @Override String getReplacer(IdCardDesensitize annotation) { return annotation.replacer(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/slider/handler/MobileDesensitization.java ================================================ package co.yixiang.yshop.framework.desensitize.core.slider.handler; import co.yixiang.yshop.framework.desensitize.core.slider.annotation.MobileDesensitize; /** * {@link MobileDesensitize} 的脱敏处理器 * * @author gaibu */ public class MobileDesensitization extends AbstractSliderDesensitizationHandler { @Override Integer getPrefixKeep(MobileDesensitize annotation) { return annotation.prefixKeep(); } @Override Integer getSuffixKeep(MobileDesensitize annotation) { return annotation.suffixKeep(); } @Override String getReplacer(MobileDesensitize annotation) { return annotation.replacer(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/desensitize/core/slider/handler/PasswordDesensitization.java ================================================ package co.yixiang.yshop.framework.desensitize.core.slider.handler; import co.yixiang.yshop.framework.desensitize.core.slider.annotation.PasswordDesensitize; /** * {@link PasswordDesensitize} 的码脱敏处理器 * * @author gaibu */ public class PasswordDesensitization extends AbstractSliderDesensitizationHandler { @Override Integer getPrefixKeep(PasswordDesensitize annotation) { return annotation.prefixKeep(); } @Override Integer getSuffixKeep(PasswordDesensitize annotation) { return annotation.suffixKeep(); } @Override String getReplacer(PasswordDesensitize annotation) { return annotation.replacer(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/jackson/config/YshopJacksonAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.jackson.config; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.framework.jackson.core.databind.NumberSerializer; import co.yixiang.yshop.framework.jackson.core.databind.TimestampLocalDateTimeDeserializer; import co.yixiang.yshop.framework.jackson.core.databind.TimestampLocalDateTimeSerializer; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.context.annotation.Bean; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.List; @AutoConfiguration @Slf4j public class YshopJacksonAutoConfiguration { @Bean @SuppressWarnings("InstantiationOfUtilityClass") public JsonUtils jsonUtils(List objectMappers) { // 1.1 创建 SimpleModule 对象 SimpleModule simpleModule = new SimpleModule(); simpleModule // 新增 Long 类型序列化规则,数值超过 2^53-1,在 JS 会出现精度丢失问题,因此 Long 自动序列化为字符串类型 .addSerializer(Long.class, NumberSerializer.INSTANCE) .addSerializer(Long.TYPE, NumberSerializer.INSTANCE) .addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE) .addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE) .addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE) .addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE) // 新增 LocalDateTime 序列化、反序列化规则,使用 Long 时间戳 .addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE) .addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE); // 1.2 注册到 objectMapper objectMappers.forEach(objectMapper -> objectMapper.registerModule(simpleModule)); // 2. 设置 objectMapper 到 JsonUtils JsonUtils.init(CollUtil.getFirst(objectMappers)); log.info("[init][初始化 JsonUtils 成功]"); return new JsonUtils(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/jackson/core/databind/NumberSerializer.java ================================================ package co.yixiang.yshop.framework.jackson.core.databind; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; import java.io.IOException; /** * Long 序列化规则 * * 会将超长 long 值转换为 string,解决前端 JavaScript 最大安全整数是 2^53-1 的问题 * * @author 星语 */ @JacksonStdImpl public class NumberSerializer extends com.fasterxml.jackson.databind.ser.std.NumberSerializer { private static final long MAX_SAFE_INTEGER = 9007199254740991L; private static final long MIN_SAFE_INTEGER = -9007199254740991L; public static final NumberSerializer INSTANCE = new NumberSerializer(Number.class); public NumberSerializer(Class rawType) { super(rawType); } @Override public void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException { // 超出范围 序列化位字符串 if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) { super.serialize(value, gen, serializers); } else { gen.writeString(value.toString()); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/jackson/core/databind/TimestampLocalDateTimeDeserializer.java ================================================ package co.yixiang.yshop.framework.jackson.core.databind; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import java.io.IOException; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; /** * 基于时间戳的 LocalDateTime 反序列化器 * * @author 老五 */ public class TimestampLocalDateTimeDeserializer extends JsonDeserializer { public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer(); @Override public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { // 将 Long 时间戳,转换为 LocalDateTime 对象 return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault()); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/jackson/core/databind/TimestampLocalDateTimeSerializer.java ================================================ package co.yixiang.yshop.framework.jackson.core.databind; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import java.io.IOException; import java.time.LocalDateTime; import java.time.ZoneId; /** * 基于时间戳的 LocalDateTime 序列化器 * * @author 老五 */ public class TimestampLocalDateTimeSerializer extends JsonSerializer { public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer(); @Override public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { // 将 LocalDateTime 对象,转换为 Long 时间戳 gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/swagger/config/SwaggerProperties.java ================================================ package co.yixiang.yshop.framework.swagger.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import jakarta.validation.constraints.NotEmpty; /** * Swagger 配置属性 * * @author yshop */ @ConfigurationProperties("yshop.swagger") @Data public class SwaggerProperties { /** * 标题 */ @NotEmpty(message = "标题不能为空") private String title; /** * 描述 */ @NotEmpty(message = "描述不能为空") private String description; /** * 作者 */ @NotEmpty(message = "作者不能为空") private String author; /** * 版本 */ @NotEmpty(message = "版本不能为空") private String version; /** * url */ @NotEmpty(message = "扫描的 package 不能为空") private String url; /** * email */ @NotEmpty(message = "扫描的 email 不能为空") private String email; /** * license */ @NotEmpty(message = "扫描的 license 不能为空") private String license; /** * license-url */ @NotEmpty(message = "扫描的 license-url 不能为空") private String licenseUrl; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/swagger/config/YshopSwaggerAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.swagger.config; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; import io.swagger.v3.oas.models.media.IntegerSchema; import io.swagger.v3.oas.models.media.StringSchema; import io.swagger.v3.oas.models.parameters.Parameter; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; import org.springdoc.core.customizers.OpenApiBuilderCustomizer; import org.springdoc.core.customizers.ServerBaseUrlCustomizer; import org.springdoc.core.models.GroupedOpenApi; import org.springdoc.core.properties.SpringDocConfigProperties; import org.springdoc.core.providers.JavadocProvider; import org.springdoc.core.service.OpenAPIService; import org.springdoc.core.service.SecurityService; import org.springdoc.core.utils.PropertyResolverUtils; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.http.HttpHeaders; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import static co.yixiang.yshop.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; /** * Swagger 自动配置类,基于 OpenAPI + Springdoc 实现。 * * 友情提示: * 1. Springdoc 文档地址:仓库 * 2. Swagger 规范,于 2015 更名为 OpenAPI 规范,本质是一个东西 * * @author yshop */ @AutoConfiguration @ConditionalOnClass({OpenAPI.class}) @EnableConfigurationProperties(SwaggerProperties.class) @ConditionalOnProperty(prefix = "springdoc.api-docs", name = "enabled", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用 public class YshopSwaggerAutoConfiguration { // ========== 全局 OpenAPI 配置 ========== @Bean public OpenAPI createApi(SwaggerProperties properties) { Map securitySchemas = buildSecuritySchemes(); OpenAPI openAPI = new OpenAPI() // 接口信息 .info(buildInfo(properties)) // 接口安全配置 .components(new Components().securitySchemes(securitySchemas)) .addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)); securitySchemas.keySet().forEach(key -> openAPI.addSecurityItem(new SecurityRequirement().addList(key))); return openAPI; } /** * API 摘要信息 */ private Info buildInfo(SwaggerProperties properties) { return new Info() .title(properties.getTitle()) .description(properties.getDescription()) .version(properties.getVersion()) .contact(new Contact().name(properties.getAuthor()).url(properties.getUrl()).email(properties.getEmail())) .license(new License().name(properties.getLicense()).url(properties.getLicenseUrl())); } /** * 安全模式,这里配置通过请求头 Authorization 传递 token 参数 */ private Map buildSecuritySchemes() { Map securitySchemes = new HashMap<>(); SecurityScheme securityScheme = new SecurityScheme() .type(SecurityScheme.Type.APIKEY) // 类型 .name(HttpHeaders.AUTHORIZATION) // 请求头的 name .in(SecurityScheme.In.HEADER); // token 所在位置 securitySchemes.put(HttpHeaders.AUTHORIZATION, securityScheme); return securitySchemes; } /** * 自定义 OpenAPI 处理器 */ @Bean @Primary // 目的:以我们创建的 OpenAPIService Bean 为主,避免一键改包后,启动报错! public OpenAPIService openApiBuilder(Optional openAPI, SecurityService securityParser, SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils, Optional> openApiBuilderCustomizers, Optional> serverBaseUrlCustomizers, Optional javadocProvider) { return new OpenAPIService(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider); } // ========== 分组 OpenAPI 配置 ========== /** * 所有模块的 API 分组 */ @Bean public GroupedOpenApi allGroupedOpenApi() { return buildGroupedOpenApi("all", ""); } public static GroupedOpenApi buildGroupedOpenApi(String group) { return buildGroupedOpenApi(group, group); } public static GroupedOpenApi buildGroupedOpenApi(String group, String path) { return GroupedOpenApi.builder() .group(group) .pathsToMatch("/admin-api/" + path + "/**", "/app-api/" + path + "/**") .addOperationCustomizer((operation, handlerMethod) -> operation .addParametersItem(buildTenantHeaderParameter()) .addParametersItem(buildSecurityHeaderParameter())) .build(); } /** * 构建 Tenant 租户编号请求头参数 * * @return 多租户参数 */ private static Parameter buildTenantHeaderParameter() { return new Parameter() .name(HEADER_TENANT_ID) // header 名 .description("租户编号") // 描述 .in(String.valueOf(SecurityScheme.In.HEADER)) // 请求 header .schema(new IntegerSchema()._default(1L).name(HEADER_TENANT_ID).description("租户编号")); // 默认:使用租户编号为 1 } /** * 构建 Authorization 认证请求头参数 * * 解决 Knife4j Authorize 未生效,请求header里未包含参数 * * @return 认证参数 */ private static Parameter buildSecurityHeaderParameter() { return new Parameter() .name(HttpHeaders.AUTHORIZATION) // header 名 .description("认证 Token") // 描述 .in(String.valueOf(SecurityScheme.In.HEADER)) // 请求 header .schema(new StringSchema()._default("Bearer test1").name(HEADER_TENANT_ID).description("认证 Token")); // 默认:使用用户编号为 1 } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/web/config/WebProperties.java ================================================ package co.yixiang.yshop.framework.web.config; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @ConfigurationProperties(prefix = "yshop.web") @Validated @Data public class WebProperties { @NotNull(message = "APP API 不能为空") private Api appApi = new Api("/app-api", "**.controller.app.**"); @NotNull(message = "Admin API 不能为空") private Api adminApi = new Api("/admin-api", "**.controller.admin.**"); @NotNull(message = "Admin UI 不能为空") private Ui adminUi; @Data @AllArgsConstructor @NoArgsConstructor @Valid public static class Api { /** * API 前缀,实现所有 Controller 提供的 RESTFul API 的统一前缀 * * * 意义:通过该前缀,避免 Swagger、Actuator 意外通过 Nginx 暴露出来给外部,带来安全性问题 * 这样,Nginx 只需要配置转发到 /api/* 的所有接口即可。 * * @see YshopWebAutoConfiguration#configurePathMatch(PathMatchConfigurer) */ @NotEmpty(message = "API 前缀不能为空") private String prefix; /** * Controller 所在包的 Ant 路径规则 * * 主要目的是,给该 Controller 设置指定的 {@link #prefix} */ @NotEmpty(message = "Controller 所在包不能为空") private String controller; } @Data @Valid public static class Ui { /** * 访问地址 */ private String url; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/web/config/YshopWebAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.web.config; import co.yixiang.yshop.framework.apilog.core.service.ApiErrorLogFrameworkService; import co.yixiang.yshop.framework.common.enums.WebFilterOrderEnum; import co.yixiang.yshop.framework.web.core.filter.CacheRequestBodyFilter; import co.yixiang.yshop.framework.web.core.filter.DemoFilter; import co.yixiang.yshop.framework.web.core.handler.GlobalExceptionHandler; import co.yixiang.yshop.framework.web.core.handler.GlobalResponseBodyHandler; import co.yixiang.yshop.framework.web.core.util.WebFrameworkUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.util.AntPathMatcher; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import jakarta.annotation.Resource; import jakarta.servlet.Filter; @AutoConfiguration @EnableConfigurationProperties(WebProperties.class) public class YshopWebAutoConfiguration implements WebMvcConfigurer { @Resource private WebProperties webProperties; /** * 应用名 */ @Value("${spring.application.name}") private String applicationName; @Override public void configurePathMatch(PathMatchConfigurer configurer) { configurePathMatch(configurer, webProperties.getAdminApi()); configurePathMatch(configurer, webProperties.getAppApi()); } /** * 设置 API 前缀,仅仅匹配 controller 包下的 * * @param configurer 配置 * @param api API 配置 */ private void configurePathMatch(PathMatchConfigurer configurer, WebProperties.Api api) { AntPathMatcher antPathMatcher = new AntPathMatcher("."); configurer.addPathPrefix(api.getPrefix(), clazz -> clazz.isAnnotationPresent(RestController.class) && antPathMatcher.match(api.getController(), clazz.getPackage().getName())); // 仅仅匹配 controller 包 } @Bean public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogFrameworkService ApiErrorLogFrameworkService) { return new GlobalExceptionHandler(applicationName, ApiErrorLogFrameworkService); } @Bean public GlobalResponseBodyHandler globalResponseBodyHandler() { return new GlobalResponseBodyHandler(); } @Bean @SuppressWarnings("InstantiationOfUtilityClass") public WebFrameworkUtils webFrameworkUtils(WebProperties webProperties) { // 由于 WebFrameworkUtils 需要使用到 webProperties 属性,所以注册为一个 Bean return new WebFrameworkUtils(webProperties); } // ========== Filter 相关 ========== /** * 创建 CorsFilter Bean,解决跨域问题 */ @Bean public FilterRegistrationBean corsFilterBean() { // 创建 CorsConfiguration 对象 CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOriginPattern("*"); // 设置访问源地址 config.addAllowedHeader("*"); // 设置访问源请求头 config.addAllowedMethod("*"); // 设置访问源请求方法 // 创建 UrlBasedCorsConfigurationSource 对象 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置 return createFilterBean(new CorsFilter(source), WebFilterOrderEnum.CORS_FILTER); } /** * 创建 RequestBodyCacheFilter Bean,可重复读取请求内容 */ @Bean public FilterRegistrationBean requestBodyCacheFilter() { return createFilterBean(new CacheRequestBodyFilter(), WebFilterOrderEnum.REQUEST_BODY_CACHE_FILTER); } /** * 创建 DemoFilter Bean,演示模式 */ @Bean @ConditionalOnProperty(value = "yshop.demo", havingValue = "true") public FilterRegistrationBean demoFilter() { return createFilterBean(new DemoFilter(), WebFilterOrderEnum.DEMO_FILTER); } public static FilterRegistrationBean createFilterBean(T filter, Integer order) { FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); bean.setOrder(order); return bean; } /** * 创建 RestTemplate 实例 * * @param restTemplateBuilder {@link RestTemplateAutoConfiguration#restTemplateBuilder} */ @Bean @ConditionalOnMissingBean public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { return restTemplateBuilder.build(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/web/core/filter/ApiRequestFilter.java ================================================ package co.yixiang.yshop.framework.web.core.filter; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.web.config.WebProperties; import lombok.RequiredArgsConstructor; import org.springframework.web.filter.OncePerRequestFilter; import jakarta.servlet.http.HttpServletRequest; /** * 过滤 /admin-api、/app-api 等 API 请求的过滤器 * * @author yshop */ @RequiredArgsConstructor public abstract class ApiRequestFilter extends OncePerRequestFilter { protected final WebProperties webProperties; @Override protected boolean shouldNotFilter(HttpServletRequest request) { // 只过滤 API 请求的地址 return !StrUtil.startWithAny(request.getRequestURI(), webProperties.getAdminApi().getPrefix(), webProperties.getAppApi().getPrefix()); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/web/core/filter/CacheRequestBodyFilter.java ================================================ package co.yixiang.yshop.framework.web.core.filter; import co.yixiang.yshop.framework.common.util.servlet.ServletUtils; import org.springframework.web.filter.OncePerRequestFilter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; /** * Request Body 缓存 Filter,实现它的可重复读取 * * @author yshop */ public class CacheRequestBodyFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { filterChain.doFilter(new CacheRequestBodyWrapper(request), response); } @Override protected boolean shouldNotFilter(HttpServletRequest request) { // 只处理 json 请求内容 return !ServletUtils.isJsonRequest(request); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/web/core/filter/CacheRequestBodyWrapper.java ================================================ package co.yixiang.yshop.framework.web.core.filter; import co.yixiang.yshop.framework.common.util.servlet.ServletUtils; import jakarta.servlet.ReadListener; import jakarta.servlet.ServletInputStream; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequestWrapper; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; /** * Request Body 缓存 Wrapper * * @author yshop */ public class CacheRequestBodyWrapper extends HttpServletRequestWrapper { /** * 缓存的内容 */ private final byte[] body; public CacheRequestBodyWrapper(HttpServletRequest request) { super(request); body = ServletUtils.getBodyBytes(request); } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(this.getInputStream())); } @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); // 返回 ServletInputStream return new ServletInputStream() { @Override public int read() { return inputStream.read(); } @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) {} @Override public int available() { return body.length; } }; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/web/core/filter/DemoFilter.java ================================================ package co.yixiang.yshop.framework.web.core.filter; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.util.servlet.ServletUtils; import co.yixiang.yshop.framework.web.core.util.WebFrameworkUtils; import org.springframework.web.filter.OncePerRequestFilter; import jakarta.servlet.FilterChain; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import static co.yixiang.yshop.framework.common.exception.enums.GlobalErrorCodeConstants.DEMO_DENY; /** * 演示 Filter,禁止用户发起写操作,避免影响测试数据 * * @author yshop */ public class DemoFilter extends OncePerRequestFilter { @Override protected boolean shouldNotFilter(HttpServletRequest request) { String method = request.getMethod(); return !StrUtil.equalsAnyIgnoreCase(method, "POST", "PUT", "DELETE") // 写操作时,不进行过滤率 || WebFrameworkUtils.getLoginUserId(request) == null; // 非登录用户时,不进行过滤 } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { // 直接返回 DEMO_DENY 的结果。即,请求不继续 ServletUtils.writeJSON(response, CommonResult.error(DEMO_DENY)); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/web/core/handler/GlobalExceptionHandler.java ================================================ package co.yixiang.yshop.framework.web.core.handler; import cn.hutool.core.exceptions.ExceptionUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.apilog.core.service.ApiErrorLogFrameworkService; import co.yixiang.yshop.framework.common.exception.ServiceException; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.util.collection.SetUtils; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.framework.common.util.monitor.TracerUtils; import co.yixiang.yshop.framework.common.util.servlet.ServletUtils; import co.yixiang.yshop.framework.web.core.util.WebFrameworkUtils; import co.yixiang.yshop.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; import jakarta.validation.ValidationException; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.security.access.AccessDeniedException; import org.springframework.util.Assert; import org.springframework.validation.BindException; import org.springframework.validation.FieldError; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.servlet.NoHandlerFoundException; import java.time.LocalDateTime; import java.util.Map; import java.util.Set; import static co.yixiang.yshop.framework.common.exception.enums.GlobalErrorCodeConstants.*; /** * 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号 * * @author yshop */ @RestControllerAdvice @AllArgsConstructor @Slf4j public class GlobalExceptionHandler { /** * 忽略的 ServiceException 错误提示,避免打印过多 logger */ public static final Set IGNORE_ERROR_MESSAGES = SetUtils.asSet("无效的刷新令牌"); @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") private final String applicationName; private final ApiErrorLogFrameworkService apiErrorLogFrameworkService; /** * 处理所有异常,主要是提供给 Filter 使用 * 因为 Filter 不走 SpringMVC 的流程,但是我们又需要兜底处理异常,所以这里提供一个全量的异常处理过程,保持逻辑统一。 * * @param request 请求 * @param ex 异常 * @return 通用返回 */ public CommonResult allExceptionHandler(HttpServletRequest request, Throwable ex) { if (ex instanceof MissingServletRequestParameterException) { return missingServletRequestParameterExceptionHandler((MissingServletRequestParameterException) ex); } if (ex instanceof MethodArgumentTypeMismatchException) { return methodArgumentTypeMismatchExceptionHandler((MethodArgumentTypeMismatchException) ex); } if (ex instanceof MethodArgumentNotValidException) { return methodArgumentNotValidExceptionExceptionHandler((MethodArgumentNotValidException) ex); } if (ex instanceof BindException) { return bindExceptionHandler((BindException) ex); } if (ex instanceof ConstraintViolationException) { return constraintViolationExceptionHandler((ConstraintViolationException) ex); } if (ex instanceof ValidationException) { return validationException((ValidationException) ex); } if (ex instanceof NoHandlerFoundException) { return noHandlerFoundExceptionHandler(request, (NoHandlerFoundException) ex); } if (ex instanceof HttpRequestMethodNotSupportedException) { return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex); } if (ex instanceof ServiceException) { return serviceExceptionHandler((ServiceException) ex); } if (ex instanceof AccessDeniedException) { return accessDeniedExceptionHandler(request, (AccessDeniedException) ex); } return defaultExceptionHandler(request, ex); } /** * 处理 SpringMVC 请求参数缺失 * * 例如说,接口上设置了 @RequestParam("xx") 参数,结果并未传递 xx 参数 */ @ExceptionHandler(value = MissingServletRequestParameterException.class) public CommonResult missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException ex) { log.warn("[missingServletRequestParameterExceptionHandler]", ex); return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数缺失:%s", ex.getParameterName())); } /** * 处理 SpringMVC 请求参数类型错误 * * 例如说,接口上设置了 @RequestParam("xx") 参数为 Integer,结果传递 xx 参数类型为 String */ @ExceptionHandler(MethodArgumentTypeMismatchException.class) public CommonResult methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) { log.warn("[missingServletRequestParameterExceptionHandler]", ex); return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage())); } /** * 处理 SpringMVC 参数校验不正确 */ @ExceptionHandler(MethodArgumentNotValidException.class) public CommonResult methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) { log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex); FieldError fieldError = ex.getBindingResult().getFieldError(); assert fieldError != null; // 断言,避免告警 return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); } /** * 处理 SpringMVC 参数绑定不正确,本质上也是通过 Validator 校验 */ @ExceptionHandler(BindException.class) public CommonResult bindExceptionHandler(BindException ex) { log.warn("[handleBindException]", ex); FieldError fieldError = ex.getFieldError(); assert fieldError != null; // 断言,避免告警 return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); } /** * 处理 Validator 校验不通过产生的异常 */ @ExceptionHandler(value = ConstraintViolationException.class) public CommonResult constraintViolationExceptionHandler(ConstraintViolationException ex) { log.warn("[constraintViolationExceptionHandler]", ex); ConstraintViolation constraintViolation = ex.getConstraintViolations().iterator().next(); return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", constraintViolation.getMessage())); } /** * 处理 Dubbo Consumer 本地参数校验时,抛出的 ValidationException 异常 */ @ExceptionHandler(value = ValidationException.class) public CommonResult validationException(ValidationException ex) { log.warn("[constraintViolationExceptionHandler]", ex); // 无法拼接明细的错误信息,因为 Dubbo Consumer 抛出 ValidationException 异常时,是直接的字符串信息,且人类不可读 return CommonResult.error(BAD_REQUEST); } /** * 处理 SpringMVC 请求地址不存在 * * 注意,它需要设置如下两个配置项: * 1. spring.mvc.throw-exception-if-no-handler-found 为 true * 2. spring.mvc.static-path-pattern 为 /statics/** */ @ExceptionHandler(NoHandlerFoundException.class) public CommonResult noHandlerFoundExceptionHandler(HttpServletRequest req, NoHandlerFoundException ex) { log.warn("[noHandlerFoundExceptionHandler]", ex); return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL())); } /** * 处理 SpringMVC 请求方法不正确 * * 例如说,A 接口的方法为 GET 方式,结果请求方法为 POST 方式,导致不匹配 */ @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public CommonResult httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) { log.warn("[httpRequestMethodNotSupportedExceptionHandler]", ex); return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage())); } /** * 处理 Spring Security 权限不足的异常 * * 来源是,使用 @PreAuthorize 注解,AOP 进行权限拦截 */ @ExceptionHandler(value = AccessDeniedException.class) public CommonResult accessDeniedExceptionHandler(HttpServletRequest req, AccessDeniedException ex) { log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", WebFrameworkUtils.getLoginUserId(req), req.getRequestURL(), ex); return CommonResult.error(FORBIDDEN); } /** * 处理业务异常 ServiceException * * 例如说,商品库存不足,用户手机号已存在。 */ @ExceptionHandler(value = ServiceException.class) public CommonResult serviceExceptionHandler(ServiceException ex) { if (!IGNORE_ERROR_MESSAGES.contains(ex.getMessage())) { // 不包含的时候,才进行打印,避免 ex 堆栈过多 log.info("[serviceExceptionHandler]", ex); } return CommonResult.error(ex.getCode(), ex.getMessage()); } /** * 处理系统异常,兜底处理所有的一切 */ @ExceptionHandler(value = Exception.class) public CommonResult defaultExceptionHandler(HttpServletRequest req, Throwable ex) { // 情况一:处理表不存在的异常 CommonResult tableNotExistsResult = handleTableNotExists(ex); if (tableNotExistsResult != null) { return tableNotExistsResult; } // 情况二:处理异常 log.error("[defaultExceptionHandler]", ex); // 插入异常日志 this.createExceptionLog(req, ex); // 返回 ERROR CommonResult return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); } private void createExceptionLog(HttpServletRequest req, Throwable e) { // 插入错误日志 ApiErrorLogCreateReqDTO errorLog = new ApiErrorLogCreateReqDTO(); try { // 初始化 errorLog buildExceptionLog(errorLog, req, e); // 执行插入 errorLog apiErrorLogFrameworkService.createApiErrorLog(errorLog); } catch (Throwable th) { log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th); } } private void buildExceptionLog(ApiErrorLogCreateReqDTO errorLog, HttpServletRequest request, Throwable e) { // 处理用户信息 errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); errorLog.setUserType(WebFrameworkUtils.getLoginUserType(request)); // 设置异常字段 errorLog.setExceptionName(e.getClass().getName()); errorLog.setExceptionMessage(ExceptionUtil.getMessage(e)); errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e)); errorLog.setExceptionStackTrace(ExceptionUtils.getStackTrace(e)); StackTraceElement[] stackTraceElements = e.getStackTrace(); Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空"); StackTraceElement stackTraceElement = stackTraceElements[0]; errorLog.setExceptionClassName(stackTraceElement.getClassName()); errorLog.setExceptionFileName(stackTraceElement.getFileName()); errorLog.setExceptionMethodName(stackTraceElement.getMethodName()); errorLog.setExceptionLineNumber(stackTraceElement.getLineNumber()); // 设置其它字段 errorLog.setTraceId(TracerUtils.getTraceId()); errorLog.setApplicationName(applicationName); errorLog.setRequestUrl(request.getRequestURI()); Map requestParams = MapUtil.builder() .put("query", ServletUtils.getParamMap(request)) .put("body", ServletUtils.getBody(request)).build(); errorLog.setRequestParams(JsonUtils.toJsonString(requestParams)); errorLog.setRequestMethod(request.getMethod()); errorLog.setUserAgent(ServletUtils.getUserAgent(request)); errorLog.setUserIp(ServletUtils.getClientIP(request)); errorLog.setExceptionTime(LocalDateTime.now()); } /** * 处理 Table 不存在的异常情况 * * @param ex 异常 * @return 如果是 Table 不存在的异常,则返回对应的 CommonResult */ private CommonResult handleTableNotExists(Throwable ex) { String message = ExceptionUtil.getRootCauseMessage(ex); if (!message.contains("doesn't exist")) { return null; } // 1. 数据报表 if (message.contains("report_")) { log.error("[报表模块 yshop-module-report - 表结构未导入][参考 https://www.yixiang.co/report/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[报表模块 yshop-module-report - 表结构未导入][参考 https://www.yixiang.co/report/ 开启]"); } // 2. 工作流 if (message.contains("bpm_")) { log.error("[工作流模块 yshop-module-bpm - 表结构未导入][参考 https://www.yixiang.co/bpm/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[工作流模块 yshop-module-bpm - 表结构未导入][参考 https://www.yixiang.co/bpm/ 开启]"); } // 3. 微信公众号 if (message.contains("mp_")) { log.error("[微信公众号 yshop-module-mp - 表结构未导入][参考 https://www.yixiang.co/mp/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[微信公众号 yshop-module-mp - 表结构未导入][参考 https://www.yixiang.co/mp/build/ 开启]"); } // 4. 商城系统 if (StrUtil.containsAny(message, "product_", "promotion_", "trade_")) { log.error("[商城系统 yshop-module-mall - 已禁用][参考 https://www.yixiang.co/mall/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[商城系统 yshop-module-mall - 已禁用][参考 https://www.yixiang.co/mall/build/ 开启]"); } // 5. ERP 系统 if (message.contains("erp_")) { log.error("[ERP 系统 yshop-module-erp - 表结构未导入][参考 https://www.yixiang.co/erp/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[ERP 系统 yshop-module-erp - 表结构未导入][参考 https://www.yixiang.co/erp/build/ 开启]"); } // 6. CRM 系统 if (message.contains("crm_")) { log.error("[CRM 系统 yshop-module-crm - 表结构未导入][参考 https://www.yixiang.co/crm/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[CRM 系统 yshop-module-crm - 表结构未导入][参考 https://www.yixiang.co/crm/build/ 开启]"); } // 7. 支付平台 if (message.contains("pay_")) { log.error("[支付模块 yshop-module-pay - 表结构未导入][参考 https://www.yixiang.co/pay/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[支付模块 yshop-module-pay - 表结构未导入][参考 https://www.yixiang.co/pay/build/ 开启]"); } return null; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/web/core/handler/GlobalResponseBodyHandler.java ================================================ package co.yixiang.yshop.framework.web.core.handler; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.web.core.util.WebFrameworkUtils; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; /** * 全局响应结果(ResponseBody)处理器 * * 不同于在网上看到的很多文章,会选择自动将 Controller 返回结果包上 {@link CommonResult}, * 在 onemall 中,是 Controller 在返回时,主动自己包上 {@link CommonResult}。 * 原因是,GlobalResponseBodyHandler 本质上是 AOP,它不应该改变 Controller 返回的数据结构 * * 目前,GlobalResponseBodyHandler 的主要作用是,记录 Controller 的返回结果, * 方便 {@link co.yixiang.yshop.framework.apilog.core.filter.ApiAccessLogFilter} 记录访问日志 */ @ControllerAdvice public class GlobalResponseBodyHandler implements ResponseBodyAdvice { @Override @SuppressWarnings("NullableProblems") // 避免 IDEA 警告 public boolean supports(MethodParameter returnType, Class converterType) { if (returnType.getMethod() == null) { return false; } // 只拦截返回结果为 CommonResult 类型 return returnType.getMethod().getReturnType() == CommonResult.class; } @Override @SuppressWarnings("NullableProblems") // 避免 IDEA 警告 public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { // 记录 Controller 结果 WebFrameworkUtils.setCommonResult(((ServletServerHttpRequest) request).getServletRequest(), (CommonResult) body); return body; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/web/core/util/WebFrameworkUtils.java ================================================ package co.yixiang.yshop.framework.web.core.util; import cn.hutool.core.util.NumberUtil; import cn.hutool.extra.servlet.ServletUtil; import co.yixiang.yshop.framework.common.enums.TerminalEnum; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.util.servlet.ServletUtils; import co.yixiang.yshop.framework.web.config.WebProperties; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import jakarta.servlet.ServletRequest; import jakarta.servlet.http.HttpServletRequest; /** * 专属于 web 包的工具类 * * @author yshop */ public class WebFrameworkUtils { private static final String REQUEST_ATTRIBUTE_LOGIN_USER_ID = "login_user_id"; private static final String REQUEST_ATTRIBUTE_LOGIN_USER_TYPE = "login_user_type"; private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result"; public static final String HEADER_TENANT_ID = "tenant-id"; /** * 终端的 Header * * @see co.yixiang.yshop.framework.common.enums.TerminalEnum */ public static final String HEADER_TERMINAL = "terminal"; private static WebProperties properties; public WebFrameworkUtils(WebProperties webProperties) { WebFrameworkUtils.properties = webProperties; } /** * 获得租户编号,从 header 中 * 考虑到其它 framework 组件也会使用到租户编号,所以不得不放在 WebFrameworkUtils 统一提供 * * @param request 请求 * @return 租户编号 */ public static Long getTenantId(HttpServletRequest request) { String tenantId = request.getHeader(HEADER_TENANT_ID); return NumberUtil.isNumber(tenantId) ? Long.valueOf(tenantId) : null; } public static void setLoginUserId(ServletRequest request, Long userId) { request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId); } /** * 设置用户类型 * * @param request 请求 * @param userType 用户类型 */ public static void setLoginUserType(ServletRequest request, Integer userType) { request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE, userType); } /** * 获得当前用户的编号,从请求中 * 注意:该方法仅限于 framework 框架使用!!! * * @param request 请求 * @return 用户编号 */ public static Long getLoginUserId(HttpServletRequest request) { if (request == null) { return null; } return (Long) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID); } /** * 获得当前用户的类型 * 注意:该方法仅限于 web 相关的 framework 组件使用!!! * * @param request 请求 * @return 用户编号 */ public static Integer getLoginUserType(HttpServletRequest request) { if (request == null) { return null; } // 1. 优先,从 Attribute 中获取 Integer userType = (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE); if (userType != null) { return userType; } // 2. 其次,基于 URL 前缀的约定 if (request.getServletPath().startsWith(properties.getAdminApi().getPrefix())) { return UserTypeEnum.ADMIN.getValue(); } if (request.getServletPath().startsWith(properties.getAppApi().getPrefix())) { return UserTypeEnum.MEMBER.getValue(); } return null; } public static Integer getLoginUserType() { HttpServletRequest request = getRequest(); return getLoginUserType(request); } public static Long getLoginUserId() { HttpServletRequest request = getRequest(); return getLoginUserId(request); } public static Integer getTerminal() { HttpServletRequest request = getRequest(); if (request == null) { return TerminalEnum.UNKNOWN.getTerminal(); } String terminalValue = request.getHeader(HEADER_TERMINAL); return NumberUtil.parseInt(terminalValue, TerminalEnum.UNKNOWN.getTerminal()); } public static void setCommonResult(ServletRequest request, CommonResult result) { request.setAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT, result); } public static CommonResult getCommonResult(ServletRequest request) { return (CommonResult) request.getAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT); } public static HttpServletRequest getRequest() { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if (!(requestAttributes instanceof ServletRequestAttributes)) { return null; } ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes; return servletRequestAttributes.getRequest(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/xss/config/XssProperties.java ================================================ package co.yixiang.yshop.framework.xss.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; import java.util.Collections; import java.util.List; /** * Xss 配置属性 * * @author yshop */ @ConfigurationProperties(prefix = "yshop.xss") @Validated @Data public class XssProperties { /** * 是否开启,默认为 true */ private boolean enable = true; /** * 需要排除的 URL,默认为空 */ private List excludeUrls = Collections.emptyList(); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/xss/config/YshopXssAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.xss.config; import co.yixiang.yshop.framework.common.enums.WebFilterOrderEnum; import co.yixiang.yshop.framework.xss.core.clean.JsoupXssCleaner; import co.yixiang.yshop.framework.xss.core.clean.XssCleaner; import co.yixiang.yshop.framework.xss.core.filter.XssFilter; import co.yixiang.yshop.framework.xss.core.json.XssStringJsonDeserializer; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.util.PathMatcher; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import static co.yixiang.yshop.framework.web.config.YshopWebAutoConfiguration.createFilterBean; @AutoConfiguration @EnableConfigurationProperties(XssProperties.class) @ConditionalOnProperty(prefix = "yshop.xss", name = "enable", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用 public class YshopXssAutoConfiguration implements WebMvcConfigurer { /** * Xss 清理者 * * @return XssCleaner */ @Bean @ConditionalOnMissingBean(XssCleaner.class) public XssCleaner xssCleaner() { return new JsoupXssCleaner(); } /** * 注册 Jackson 的序列化器,用于处理 json 类型参数的 xss 过滤 * * @return Jackson2ObjectMapperBuilderCustomizer */ @Bean @ConditionalOnMissingBean(name = "xssJacksonCustomizer") @ConditionalOnBean(ObjectMapper.class) @ConditionalOnProperty(value = "yshop.xss.enable", havingValue = "true") public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssProperties properties, PathMatcher pathMatcher, XssCleaner xssCleaner) { // 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer,在序列化时进行处理 return builder -> builder.deserializerByType(String.class, new XssStringJsonDeserializer(properties, pathMatcher, xssCleaner)); } /** * 创建 XssFilter Bean,解决 Xss 安全问题 */ @Bean @ConditionalOnBean(XssCleaner.class) public FilterRegistrationBean xssFilter(XssProperties properties, PathMatcher pathMatcher, XssCleaner xssCleaner) { return createFilterBean(new XssFilter(properties, pathMatcher, xssCleaner), WebFilterOrderEnum.XSS_FILTER); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/xss/core/clean/JsoupXssCleaner.java ================================================ package co.yixiang.yshop.framework.xss.core.clean; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.safety.Safelist; /** * 基于 JSONP 实现 XSS 过滤字符串 */ public class JsoupXssCleaner implements XssCleaner { private final Safelist safelist; /** * 用于在 src 属性使用相对路径时,强制转换为绝对路径。 为空时不处理,值应为绝对路径的前缀(包含协议部分) */ private final String baseUri; /** * 无参构造,默认使用 {@link JsoupXssCleaner#buildSafelist} 方法构建一个安全列表 */ public JsoupXssCleaner() { this.safelist = buildSafelist(); this.baseUri = ""; } /** * 构建一个 Xss 清理的 Safelist 规则。 * 基于 Safelist#relaxed() 的基础上: * 1. 扩展支持了 style 和 class 属性 * 2. a 标签额外支持了 target 属性 * 3. img 标签额外支持了 data 协议,便于支持 base64 * * @return Safelist */ private Safelist buildSafelist() { // 使用 jsoup 提供的默认的 Safelist relaxedSafelist = Safelist.relaxed(); // 富文本编辑时一些样式是使用 style 来进行实现的 // 比如红色字体 style="color:red;", 所以需要给所有标签添加 style 属性 // 注意:style 属性会有注入风险 relaxedSafelist.addAttributes(":all", "style", "class"); // 保留 a 标签的 target 属性 relaxedSafelist.addAttributes("a", "target"); // 支持img 为base64 relaxedSafelist.addProtocols("img", "src", "data"); // 保留相对路径, 保留相对路径时,必须提供对应的 baseUri 属性,否则依然会被删除 // WHITELIST.preserveRelativeLinks(false); // 移除 a 标签和 img 标签的一些协议限制,这会导致 xss 防注入失效,如 // 虽然可以重写 WhiteList#isSafeAttribute 来处理,但是有隐患,所以暂时不支持相对路径 // WHITELIST.removeProtocols("a", "href", "ftp", "http", "https", "mailto"); // WHITELIST.removeProtocols("img", "src", "http", "https"); return relaxedSafelist; } @Override public String clean(String html) { return Jsoup.clean(html, baseUri, safelist, new Document.OutputSettings().prettyPrint(false)); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/xss/core/clean/XssCleaner.java ================================================ package co.yixiang.yshop.framework.xss.core.clean; /** * 对 html 文本中的有 Xss 风险的数据进行清理 */ public interface XssCleaner { /** * 清理有 Xss 风险的文本 * * @param html 原 html * @return 清理后的 html */ String clean(String html); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/xss/core/filter/XssFilter.java ================================================ package co.yixiang.yshop.framework.xss.core.filter; import co.yixiang.yshop.framework.xss.config.XssProperties; import co.yixiang.yshop.framework.xss.core.clean.XssCleaner; import lombok.AllArgsConstructor; import org.springframework.util.PathMatcher; import org.springframework.web.filter.OncePerRequestFilter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; /** * Xss 过滤器 * * @author yshop */ @AllArgsConstructor public class XssFilter extends OncePerRequestFilter { /** * 属性 */ private final XssProperties properties; /** * 路径匹配器 */ private final PathMatcher pathMatcher; private final XssCleaner xssCleaner; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { filterChain.doFilter(new XssRequestWrapper(request, xssCleaner), response); } @Override protected boolean shouldNotFilter(HttpServletRequest request) { // 如果关闭,则不过滤 if (!properties.isEnable()) { return true; } // 如果匹配到无需过滤,则不过滤 String uri = request.getRequestURI(); return properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, uri)); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/xss/core/filter/XssRequestWrapper.java ================================================ package co.yixiang.yshop.framework.xss.core.filter; import co.yixiang.yshop.framework.xss.core.clean.XssCleaner; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequestWrapper; import java.util.LinkedHashMap; import java.util.Map; /** * Xss 请求 Wrapper * * @author yshop */ public class XssRequestWrapper extends HttpServletRequestWrapper { private final XssCleaner xssCleaner; public XssRequestWrapper(HttpServletRequest request, XssCleaner xssCleaner) { super(request); this.xssCleaner = xssCleaner; } // ============================ parameter ============================ @Override public Map getParameterMap() { Map map = new LinkedHashMap<>(); Map parameters = super.getParameterMap(); for (Map.Entry entry : parameters.entrySet()) { String[] values = entry.getValue(); for (int i = 0; i < values.length; i++) { values[i] = xssCleaner.clean(values[i]); } map.put(entry.getKey(), values); } return map; } @Override public String[] getParameterValues(String name) { String[] values = super.getParameterValues(name); if (values == null) { return null; } int count = values.length; String[] encodedValues = new String[count]; for (int i = 0; i < count; i++) { encodedValues[i] = xssCleaner.clean(values[i]); } return encodedValues; } @Override public String getParameter(String name) { String value = super.getParameter(name); if (value == null) { return null; } return xssCleaner.clean(value); } // ============================ attribute ============================ @Override public Object getAttribute(String name) { Object value = super.getAttribute(name); if (value instanceof String) { return xssCleaner.clean((String) value); } return value; } // ============================ header ============================ @Override public String getHeader(String name) { String value = super.getHeader(name); if (value == null) { return null; } return xssCleaner.clean(value); } // ============================ queryString ============================ @Override public String getQueryString() { String value = super.getQueryString(); if (value == null) { return null; } return xssCleaner.clean(value); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/java/co/yixiang/yshop/framework/xss/core/json/XssStringJsonDeserializer.java ================================================ package co.yixiang.yshop.framework.xss.core.json; import co.yixiang.yshop.framework.common.util.servlet.ServletUtils; import co.yixiang.yshop.framework.xss.config.XssProperties; import co.yixiang.yshop.framework.xss.core.clean.XssCleaner; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.deser.std.StringDeserializer; import jakarta.servlet.http.HttpServletRequest; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.util.PathMatcher; import java.io.IOException; /** * XSS 过滤 jackson 反序列化器。 * 在反序列化的过程中,会对字符串进行 XSS 过滤。 * * @author Hccake */ @Slf4j @AllArgsConstructor public class XssStringJsonDeserializer extends StringDeserializer { /** * 属性 */ private final XssProperties properties; /** * 路径匹配器 */ private final PathMatcher pathMatcher; private final XssCleaner xssCleaner; @Override public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { // 1. 白名单 URL 的处理 HttpServletRequest request = ServletUtils.getRequest(); if (request != null) { String uri = ServletUtils.getRequest().getRequestURI(); if (properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, uri))) { return p.getText(); } } // 2. 真正使用 xssCleaner 进行过滤 if (p.hasToken(JsonToken.VALUE_STRING)) { return xssCleaner.clean(p.getText()); } JsonToken t = p.currentToken(); // [databind#381] if (t == JsonToken.START_ARRAY) { return _deserializeFromArray(p, ctxt); } // need to gracefully handle byte[] data, as base64 if (t == JsonToken.VALUE_EMBEDDED_OBJECT) { Object ob = p.getEmbeddedObject(); if (ob == null) { return null; } if (ob instanceof byte[]) { return ctxt.getBase64Variant().encode((byte[]) ob, false); } // otherwise, try conversion using toString()... return ob.toString(); } // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) if (t == JsonToken.START_OBJECT) { return ctxt.extractScalarFromObject(p, this, _valueClass); } if (t.isScalarValue()) { String text = p.getValueAsString(); return xssCleaner.clean(text); } return (String) ctxt.handleUnexpectedToken(_valueClass, p); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ co.yixiang.yshop.framework.apilog.config.YshopApiLogAutoConfiguration co.yixiang.yshop.framework.jackson.config.YshopJacksonAutoConfiguration co.yixiang.yshop.framework.swagger.config.YshopSwaggerAutoConfiguration co.yixiang.yshop.framework.web.config.YshopWebAutoConfiguration co.yixiang.yshop.framework.xss.config.YshopXssAutoConfiguration co.yixiang.yshop.framework.banner.config.YshopBannerAutoConfiguration ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/main/resources/banner.txt ================================================ 意象电商 http://www.yixiang.co Application Version: ${yshop.info.version} Spring Boot Version: ${spring-boot.version} .__ __. ______ .______ __ __ _______ | \ | | / __ \ | _ \ | | | | / _____| | \| | | | | | | |_) | | | | | | | __ | . ` | | | | | | _ < | | | | | | |_ | | |\ | | `--' | | |_) | | `--' | | |__| | |__| \__| \______/ |______/ \______/ \______| ███╗ ██╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗ ████╗ ██║██╔═══██╗ ██╔══██╗██║ ██║██╔════╝ ██╔██╗ ██║██║ ██║ ██████╔╝██║ ██║██║ ███╗ ██║╚██╗██║██║ ██║ ██╔══██╗██║ ██║██║ ██║ ██║ ╚████║╚██████╔╝ ██████╔╝╚██████╔╝╚██████╔╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/test/java/co/yixiang/yshop/framework/desensitize/core/DesensitizeTest.java ================================================ package co.yixiang.yshop.framework.desensitize.core; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.framework.desensitize.core.annotation.Address; import co.yixiang.yshop.framework.desensitize.core.regex.annotation.EmailDesensitize; import co.yixiang.yshop.framework.desensitize.core.regex.annotation.RegexDesensitize; import co.yixiang.yshop.framework.desensitize.core.slider.annotation.*; import lombok.Data; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; /** * {@link DesensitizeTest} 的单元测试 */ @ExtendWith(MockitoExtension.class) public class DesensitizeTest { @Test public void test() { // 准备参数 DesensitizeDemo desensitizeDemo = new DesensitizeDemo(); desensitizeDemo.setNickname("yshop"); desensitizeDemo.setBankCard("9988002866797031"); desensitizeDemo.setCarLicense("粤A66666"); desensitizeDemo.setFixedPhone("01086551122"); desensitizeDemo.setIdCard("530321199204074611"); desensitizeDemo.setPassword("123456"); desensitizeDemo.setPhoneNumber("13248765917"); desensitizeDemo.setSlider1("ABCDEFG"); desensitizeDemo.setSlider2("ABCDEFG"); desensitizeDemo.setSlider3("ABCDEFG"); desensitizeDemo.setEmail("1@email.com"); desensitizeDemo.setRegex("你好,我是yshop"); desensitizeDemo.setAddress("北京市海淀区上地十街10号"); desensitizeDemo.setOrigin("yshop"); // 调用 DesensitizeDemo d = JsonUtils.parseObject(JsonUtils.toJsonString(desensitizeDemo), DesensitizeDemo.class); // 断言 assertNotNull(d); assertEquals("芋***", d.getNickname()); assertEquals("998800********31", d.getBankCard()); assertEquals("粤A6***6", d.getCarLicense()); assertEquals("0108*****22", d.getFixedPhone()); assertEquals("530321**********11", d.getIdCard()); assertEquals("******", d.getPassword()); assertEquals("132****5917", d.getPhoneNumber()); assertEquals("#######", d.getSlider1()); assertEquals("ABC*EFG", d.getSlider2()); assertEquals("*******", d.getSlider3()); assertEquals("1****@email.com", d.getEmail()); assertEquals("你好,我是*", d.getRegex()); assertEquals("北京市海淀区上地十街10号*", d.getAddress()); assertEquals("yshop", d.getOrigin()); } @Data public static class DesensitizeDemo { @ChineseNameDesensitize private String nickname; @BankCardDesensitize private String bankCard; @CarLicenseDesensitize private String carLicense; @FixedPhoneDesensitize private String fixedPhone; @IdCardDesensitize private String idCard; @PasswordDesensitize private String password; @MobileDesensitize private String phoneNumber; @SliderDesensitize(prefixKeep = 6, suffixKeep = 1, replacer = "#") private String slider1; @SliderDesensitize(prefixKeep = 3, suffixKeep = 3) private String slider2; @SliderDesensitize(prefixKeep = 10) private String slider3; @EmailDesensitize private String email; @RegexDesensitize(regex = "yshop", replacer = "*") private String regex; @Address private String address; private String origin; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/test/java/co/yixiang/yshop/framework/desensitize/core/annotation/Address.java ================================================ package co.yixiang.yshop.framework.desensitize.core.annotation; import co.yixiang.yshop.framework.desensitize.core.DesensitizeTest; import co.yixiang.yshop.framework.desensitize.core.handler.AddressHandler; import co.yixiang.yshop.framework.desensitize.core.base.annotation.DesensitizeBy; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 地址 * * 用于 {@link DesensitizeTest} 测试使用 * * @author gaibu */ @Documented @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside @DesensitizeBy(handler = AddressHandler.class) public @interface Address { String replacer() default "*"; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-web/src/test/java/co/yixiang/yshop/framework/desensitize/core/handler/AddressHandler.java ================================================ package co.yixiang.yshop.framework.desensitize.core.handler; import co.yixiang.yshop.framework.desensitize.core.DesensitizeTest; import co.yixiang.yshop.framework.desensitize.core.base.handler.DesensitizationHandler; import co.yixiang.yshop.framework.desensitize.core.annotation.Address; /** * {@link Address} 的脱敏处理器 * * 用于 {@link DesensitizeTest} 测试使用 */ public class AddressHandler implements DesensitizationHandler
{ @Override public String desensitize(String origin, Address annotation) { return origin + annotation.replacer(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/pom.xml ================================================ co.yixiang.boot yshop-framework ${revision} 4.0.0 yshop-spring-boot-starter-websocket jar ${project.artifactId} WebSocket 框架,支持多节点的广播 https://gitee.com/guchengwuyue/yshop-drink co.yixiang.boot yshop-common co.yixiang.boot yshop-spring-boot-starter-security provided org.springframework.boot spring-boot-starter-websocket co.yixiang.boot yshop-spring-boot-starter-mq org.springframework.kafka spring-kafka true org.springframework.amqp spring-rabbit true org.apache.rocketmq rocketmq-spring-boot-starter true co.yixiang.boot yshop-spring-boot-starter-biz-tenant provided ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/config/WebSocketProperties.java ================================================ package co.yixiang.yshop.framework.websocket.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; /** * WebSocket 配置项 * * @author xingyu4j */ @ConfigurationProperties("yshop.websocket") @Data @Validated public class WebSocketProperties { /** * WebSocket 的连接路径 */ @NotEmpty(message = "WebSocket 的连接路径不能为空") private String path = "/ws"; /** * 消息发送器的类型 * * 可选值:local、redis、rocketmq、kafka、rabbitmq */ @NotNull(message = "WebSocket 的消息发送者不能为空") private String senderType = "local"; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/config/YshopWebSocketAutoConfiguration.java ================================================ package co.yixiang.yshop.framework.websocket.config; import co.yixiang.yshop.framework.mq.redis.config.YshopRedisMQConsumerAutoConfiguration; import co.yixiang.yshop.framework.mq.redis.core.RedisMQTemplate; import co.yixiang.yshop.framework.websocket.core.handler.JsonWebSocketMessageHandler; import co.yixiang.yshop.framework.websocket.core.listener.WebSocketMessageListener; import co.yixiang.yshop.framework.websocket.core.security.LoginUserHandshakeInterceptor; import co.yixiang.yshop.framework.websocket.core.sender.kafka.KafkaWebSocketMessageConsumer; import co.yixiang.yshop.framework.websocket.core.sender.kafka.KafkaWebSocketMessageSender; import co.yixiang.yshop.framework.websocket.core.sender.local.LocalWebSocketMessageSender; import co.yixiang.yshop.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageConsumer; import co.yixiang.yshop.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageSender; import co.yixiang.yshop.framework.websocket.core.sender.redis.RedisWebSocketMessageConsumer; import co.yixiang.yshop.framework.websocket.core.sender.redis.RedisWebSocketMessageSender; import co.yixiang.yshop.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageConsumer; import co.yixiang.yshop.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageSender; import co.yixiang.yshop.framework.websocket.core.session.WebSocketSessionHandlerDecorator; import co.yixiang.yshop.framework.websocket.core.session.WebSocketSessionManager; import co.yixiang.yshop.framework.websocket.core.session.WebSocketSessionManagerImpl; import org.apache.rocketmq.spring.core.RocketMQTemplate; import org.springframework.amqp.core.TopicExchange; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.server.HandshakeInterceptor; import java.util.List; /** * WebSocket 自动配置 * * @author xingyu4j */ @AutoConfiguration(before = YshopRedisMQConsumerAutoConfiguration.class) // before YshopRedisMQConsumerAutoConfiguration 的原因是,需要保证 RedisWebSocketMessageConsumer 先创建,才能创建 RedisMessageListenerContainer @EnableWebSocket // 开启 websocket @ConditionalOnProperty(prefix = "yshop.websocket", value = "enable", matchIfMissing = true) // 允许使用 yshop.websocket.enable=false 禁用 websocket @EnableConfigurationProperties(WebSocketProperties.class) public class YshopWebSocketAutoConfiguration { @Bean public WebSocketConfigurer webSocketConfigurer(HandshakeInterceptor[] handshakeInterceptors, WebSocketHandler webSocketHandler, WebSocketProperties webSocketProperties) { return registry -> registry // 添加 WebSocketHandler .addHandler(webSocketHandler, webSocketProperties.getPath()) .addInterceptors(handshakeInterceptors) // 允许跨域,否则前端连接会直接断开 .setAllowedOriginPatterns("*"); } @Bean public HandshakeInterceptor handshakeInterceptor() { return new LoginUserHandshakeInterceptor(); } @Bean public WebSocketHandler webSocketHandler(WebSocketSessionManager sessionManager, List> messageListeners) { // 1. 创建 JsonWebSocketMessageHandler 对象,处理消息 JsonWebSocketMessageHandler messageHandler = new JsonWebSocketMessageHandler(messageListeners); // 2. 创建 WebSocketSessionHandlerDecorator 对象,处理连接 return new WebSocketSessionHandlerDecorator(messageHandler, sessionManager); } @Bean public WebSocketSessionManager webSocketSessionManager() { return new WebSocketSessionManagerImpl(); } // ==================== Sender 相关 ==================== @Configuration @ConditionalOnProperty(prefix = "yshop.websocket", name = "sender-type", havingValue = "local", matchIfMissing = true) public class LocalWebSocketMessageSenderConfiguration { @Bean public LocalWebSocketMessageSender localWebSocketMessageSender(WebSocketSessionManager sessionManager) { return new LocalWebSocketMessageSender(sessionManager); } } @Configuration @ConditionalOnProperty(prefix = "yshop.websocket", name = "sender-type", havingValue = "redis", matchIfMissing = true) public class RedisWebSocketMessageSenderConfiguration { @Bean public RedisWebSocketMessageSender redisWebSocketMessageSender(WebSocketSessionManager sessionManager, RedisMQTemplate redisMQTemplate) { return new RedisWebSocketMessageSender(sessionManager, redisMQTemplate); } @Bean public RedisWebSocketMessageConsumer redisWebSocketMessageConsumer( RedisWebSocketMessageSender redisWebSocketMessageSender) { return new RedisWebSocketMessageConsumer(redisWebSocketMessageSender); } } @Configuration @ConditionalOnProperty(prefix = "yshop.websocket", name = "sender-type", havingValue = "rocketmq", matchIfMissing = true) public class RocketMQWebSocketMessageSenderConfiguration { @Bean public RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender( WebSocketSessionManager sessionManager, RocketMQTemplate rocketMQTemplate, @Value("${yshop.websocket.sender-rocketmq.topic}") String topic) { return new RocketMQWebSocketMessageSender(sessionManager, rocketMQTemplate, topic); } @Bean public RocketMQWebSocketMessageConsumer rocketMQWebSocketMessageConsumer( RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender) { return new RocketMQWebSocketMessageConsumer(rocketMQWebSocketMessageSender); } } @Configuration @ConditionalOnProperty(prefix = "yshop.websocket", name = "sender-type", havingValue = "rabbitmq", matchIfMissing = true) public class RabbitMQWebSocketMessageSenderConfiguration { @Bean public RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender( WebSocketSessionManager sessionManager, RabbitTemplate rabbitTemplate, TopicExchange websocketTopicExchange) { return new RabbitMQWebSocketMessageSender(sessionManager, rabbitTemplate, websocketTopicExchange); } @Bean public RabbitMQWebSocketMessageConsumer rabbitMQWebSocketMessageConsumer( RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender) { return new RabbitMQWebSocketMessageConsumer(rabbitMQWebSocketMessageSender); } /** * 创建 Topic Exchange */ @Bean public TopicExchange websocketTopicExchange(@Value("${yshop.websocket.sender-rabbitmq.exchange}") String exchange) { return new TopicExchange(exchange, true, // durable: 是否持久化 false); // exclusive: 是否排它 } } @Configuration @ConditionalOnProperty(prefix = "yshop.websocket", name = "sender-type", havingValue = "kafka", matchIfMissing = true) public class KafkaWebSocketMessageSenderConfiguration { @Bean public KafkaWebSocketMessageSender kafkaWebSocketMessageSender( WebSocketSessionManager sessionManager, KafkaTemplate kafkaTemplate, @Value("${yshop.websocket.sender-kafka.topic}") String topic) { return new KafkaWebSocketMessageSender(sessionManager, kafkaTemplate, topic); } @Bean public KafkaWebSocketMessageConsumer kafkaWebSocketMessageConsumer( KafkaWebSocketMessageSender kafkaWebSocketMessageSender) { return new KafkaWebSocketMessageConsumer(kafkaWebSocketMessageSender); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/handler/JsonWebSocketMessageHandler.java ================================================ package co.yixiang.yshop.framework.websocket.core.handler; import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.TypeUtil; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.framework.tenant.core.util.TenantUtils; import co.yixiang.yshop.framework.websocket.core.listener.WebSocketMessageListener; import co.yixiang.yshop.framework.websocket.core.message.JsonWebSocketMessage; import co.yixiang.yshop.framework.websocket.core.util.WebSocketFrameworkUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; import java.lang.reflect.Type; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Consumer; /** * JSON 格式 {@link WebSocketHandler} 实现类 * * 基于 {@link JsonWebSocketMessage#getType()} 消息类型,调度到对应的 {@link WebSocketMessageListener} 监听器。 * * @author yshop */ @Slf4j public class JsonWebSocketMessageHandler extends TextWebSocketHandler { /** * type 与 WebSocketMessageListener 的映射 */ private final Map> listeners = new HashMap<>(); @SuppressWarnings({"rawtypes", "unchecked"}) public JsonWebSocketMessageHandler(List listenersList) { listenersList.forEach((Consumer) listener -> listeners.put(listener.getType(), listener)); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { // 1.1 空消息,跳过 if (message.getPayloadLength() == 0) { return; } // 1.2 ping 心跳消息,直接返回 pong 消息。 if (message.getPayloadLength() == 4 && Objects.equals(message.getPayload(), "ping")) { session.sendMessage(new TextMessage("pong")); return; } // 2.1 解析消息 try { JsonWebSocketMessage jsonMessage = JsonUtils.parseObject(message.getPayload(), JsonWebSocketMessage.class); if (jsonMessage == null) { log.error("[handleTextMessage][session({}) message({}) 解析为空]", session.getId(), message.getPayload()); return; } if (StrUtil.isEmpty(jsonMessage.getType())) { log.error("[handleTextMessage][session({}) message({}) 类型为空]", session.getId(), message.getPayload()); return; } // 2.2 获得对应的 WebSocketMessageListener WebSocketMessageListener messageListener = listeners.get(jsonMessage.getType()); if (messageListener == null) { log.error("[handleTextMessage][session({}) message({}) 监听器为空]", session.getId(), message.getPayload()); return; } // 2.3 处理消息 Type type = TypeUtil.getTypeArgument(messageListener.getClass(), 0); Object messageObj = JsonUtils.parseObject(jsonMessage.getContent(), type); Long tenantId = WebSocketFrameworkUtils.getTenantId(session); TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj)); } catch (Throwable ex) { log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload()); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/listener/WebSocketMessageListener.java ================================================ package co.yixiang.yshop.framework.websocket.core.listener; import co.yixiang.yshop.framework.websocket.core.message.JsonWebSocketMessage; import org.springframework.web.socket.WebSocketSession; /** * WebSocket 消息监听器接口 * * 目的:前端发送消息给后端后,处理对应 {@link #getType()} 类型的消息 * * @param 泛型,消息类型 */ public interface WebSocketMessageListener { /** * 处理消息 * * @param session Session * @param message 消息 */ void onMessage(WebSocketSession session, T message); /** * 获得消息类型 * * @see JsonWebSocketMessage#getType() * @return 消息类型 */ String getType(); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/message/JsonWebSocketMessage.java ================================================ package co.yixiang.yshop.framework.websocket.core.message; import co.yixiang.yshop.framework.websocket.core.listener.WebSocketMessageListener; import lombok.Data; import java.io.Serializable; /** * JSON 格式的 WebSocket 消息帧 * * @author yshop */ @Data public class JsonWebSocketMessage implements Serializable { /** * 消息类型 * * 目的:用于分发到对应的 {@link WebSocketMessageListener} 实现类 */ private String type; /** * 消息内容 * * 要求 JSON 对象 */ private String content; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/security/LoginUserHandshakeInterceptor.java ================================================ package co.yixiang.yshop.framework.websocket.core.security; import co.yixiang.yshop.framework.security.core.LoginUser; import co.yixiang.yshop.framework.security.core.filter.TokenAuthenticationFilter; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import co.yixiang.yshop.framework.websocket.core.util.WebSocketFrameworkUtils; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.server.HandshakeInterceptor; import java.util.Map; /** * 登录用户的 {@link HandshakeInterceptor} 实现类 * * 流程如下: * 1. 前端连接 websocket 时,会通过拼接 ?token={token} 到 ws:// 连接后,这样它可以被 {@link TokenAuthenticationFilter} 所认证通过 * 2. {@link LoginUserHandshakeInterceptor} 负责把 {@link LoginUser} 添加到 {@link WebSocketSession} 中 * * @author yshop */ public class LoginUserHandshakeInterceptor implements HandshakeInterceptor { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) { LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); if (loginUser != null) { WebSocketFrameworkUtils.setLoginUser(loginUser, attributes); } return true; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { // do nothing } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java ================================================ package co.yixiang.yshop.framework.websocket.core.security; import co.yixiang.yshop.framework.security.config.AuthorizeRequestsCustomizer; import co.yixiang.yshop.framework.websocket.config.WebSocketProperties; import lombok.RequiredArgsConstructor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; /** * WebSocket 的权限自定义 * * @author yshop */ @RequiredArgsConstructor public class WebSocketAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer { private final WebSocketProperties webSocketProperties; @Override public void customize(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { registry.requestMatchers(webSocketProperties.getPath()).permitAll(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/sender/AbstractWebSocketMessageSender.java ================================================ package co.yixiang.yshop.framework.websocket.core.sender; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.framework.websocket.core.message.JsonWebSocketMessage; import co.yixiang.yshop.framework.websocket.core.session.WebSocketSessionManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.List; /** * WebSocketMessageSender 实现类 * * @author yshop */ @Slf4j @RequiredArgsConstructor public abstract class AbstractWebSocketMessageSender implements WebSocketMessageSender { private final WebSocketSessionManager sessionManager; @Override public void send(Integer userType, Long userId, String messageType, String messageContent) { send(null, userType, userId, messageType, messageContent); } @Override public void send(Integer userType, String messageType, String messageContent) { send(null, userType, null, messageType, messageContent); } @Override public void send(String sessionId, String messageType, String messageContent) { send(sessionId, null, null, messageType, messageContent); } /** * 发送消息 * * @param sessionId Session 编号 * @param userType 用户类型 * @param userId 用户编号 * @param messageType 消息类型 * @param messageContent 消息内容 */ public void send(String sessionId, Integer userType, Long userId, String messageType, String messageContent) { // 1. 获得 Session 列表 List sessions = Collections.emptyList(); if (StrUtil.isNotEmpty(sessionId)) { WebSocketSession session = sessionManager.getSession(sessionId); if (session != null) { sessions = Collections.singletonList(session); } } else if (userType != null && userId != null) { sessions = (List) sessionManager.getSessionList(userType, userId); } else if (userType != null) { sessions = (List) sessionManager.getSessionList(userType); } if (CollUtil.isEmpty(sessions)) { log.info("[send][sessionId({}) userType({}) userId({}) messageType({}) messageContent({}) 未匹配到会话]", sessionId, userType, userId, messageType, messageContent); } // 2. 执行发送 doSend(sessions, messageType, messageContent); } /** * 发送消息的具体实现 * * @param sessions Session 列表 * @param messageType 消息类型 * @param messageContent 消息内容 */ public void doSend(Collection sessions, String messageType, String messageContent) { JsonWebSocketMessage message = new JsonWebSocketMessage().setType(messageType).setContent(messageContent); String payload = JsonUtils.toJsonString(message); // 关键,使用 JSON 序列化 sessions.forEach(session -> { // 1. 各种校验,保证 Session 可以被发送 if (session == null) { log.error("[doSend][session 为空, message({})]", message); return; } if (!session.isOpen()) { log.error("[doSend][session({}) 已关闭, message({})]", session.getId(), message); return; } // 2. 执行发送 try { session.sendMessage(new TextMessage(payload)); log.info("[doSend][session({}) 发送消息成功,message({})]", session.getId(), message); } catch (IOException ex) { log.error("[doSend][session({}) 发送消息失败,message({})]", session.getId(), message, ex); } }); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/sender/WebSocketMessageSender.java ================================================ package co.yixiang.yshop.framework.websocket.core.sender; import co.yixiang.yshop.framework.common.util.json.JsonUtils; /** * WebSocket 消息的发送器接口 * * @author yshop */ public interface WebSocketMessageSender { /** * 发送消息给指定用户 * * @param userType 用户类型 * @param userId 用户编号 * @param messageType 消息类型 * @param messageContent 消息内容,JSON 格式 */ void send(Integer userType, Long userId, String messageType, String messageContent); /** * 发送消息给指定用户类型 * * @param userType 用户类型 * @param messageType 消息类型 * @param messageContent 消息内容,JSON 格式 */ void send(Integer userType, String messageType, String messageContent); /** * 发送消息给指定 Session * * @param sessionId Session 编号 * @param messageType 消息类型 * @param messageContent 消息内容,JSON 格式 */ void send(String sessionId, String messageType, String messageContent); default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) { send(userType, userId, messageType, JsonUtils.toJsonString(messageContent)); } default void sendObject(Integer userType, String messageType, Object messageContent) { send(userType, messageType, JsonUtils.toJsonString(messageContent)); } default void sendObject(String sessionId, String messageType, Object messageContent) { send(sessionId, messageType, JsonUtils.toJsonString(messageContent)); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java ================================================ package co.yixiang.yshop.framework.websocket.core.sender.kafka; import lombok.Data; /** * Kafka 广播 WebSocket 的消息 * * @author yshop */ @Data public class KafkaWebSocketMessage { /** * Session 编号 */ private String sessionId; /** * 用户类型 */ private Integer userType; /** * 用户编号 */ private Long userId; /** * 消息类型 */ private String messageType; /** * 消息内容 */ private String messageContent; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java ================================================ package co.yixiang.yshop.framework.websocket.core.sender.kafka; import lombok.RequiredArgsConstructor; import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.kafka.annotation.KafkaListener; /** * {@link KafkaWebSocketMessage} 广播消息的消费者,真正把消息发送出去 * * @author yshop */ @RequiredArgsConstructor public class KafkaWebSocketMessageConsumer { private final KafkaWebSocketMessageSender rabbitMQWebSocketMessageSender; @RabbitHandler @KafkaListener( topics = "${yshop.websocket.sender-kafka.topic}", // 在 Group 上,使用 UUID 生成其后缀。这样,启动的 Consumer 的 Group 不同,以达到广播消费的目的 groupId = "${yshop.websocket.sender-kafka.consumer-group}" + "-" + "#{T(java.util.UUID).randomUUID()}") public void onMessage(KafkaWebSocketMessage message) { rabbitMQWebSocketMessageSender.send(message.getSessionId(), message.getUserType(), message.getUserId(), message.getMessageType(), message.getMessageContent()); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java ================================================ package co.yixiang.yshop.framework.websocket.core.sender.kafka; import co.yixiang.yshop.framework.websocket.core.sender.AbstractWebSocketMessageSender; import co.yixiang.yshop.framework.websocket.core.sender.WebSocketMessageSender; import co.yixiang.yshop.framework.websocket.core.session.WebSocketSessionManager; import lombok.extern.slf4j.Slf4j; import org.springframework.kafka.core.KafkaTemplate; import java.util.concurrent.ExecutionException; /** * 基于 Kafka 的 {@link WebSocketMessageSender} 实现类 * * @author yshop */ @Slf4j public class KafkaWebSocketMessageSender extends AbstractWebSocketMessageSender { private final KafkaTemplate kafkaTemplate; private final String topic; public KafkaWebSocketMessageSender(WebSocketSessionManager sessionManager, KafkaTemplate kafkaTemplate, String topic) { super(sessionManager); this.kafkaTemplate = kafkaTemplate; this.topic = topic; } @Override public void send(Integer userType, Long userId, String messageType, String messageContent) { sendKafkaMessage(null, userId, userType, messageType, messageContent); } @Override public void send(Integer userType, String messageType, String messageContent) { sendKafkaMessage(null, null, userType, messageType, messageContent); } @Override public void send(String sessionId, String messageType, String messageContent) { sendKafkaMessage(sessionId, null, null, messageType, messageContent); } /** * 通过 Kafka 广播消息 * * @param sessionId Session 编号 * @param userId 用户编号 * @param userType 用户类型 * @param messageType 消息类型 * @param messageContent 消息内容 */ private void sendKafkaMessage(String sessionId, Long userId, Integer userType, String messageType, String messageContent) { KafkaWebSocketMessage mqMessage = new KafkaWebSocketMessage() .setSessionId(sessionId).setUserId(userId).setUserType(userType) .setMessageType(messageType).setMessageContent(messageContent); try { kafkaTemplate.send(topic, mqMessage).get(); } catch (InterruptedException | ExecutionException e) { log.error("[sendKafkaMessage][发送消息({}) 到 Kafka 失败]", mqMessage, e); } } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java ================================================ package co.yixiang.yshop.framework.websocket.core.sender.local; import co.yixiang.yshop.framework.websocket.core.sender.AbstractWebSocketMessageSender; import co.yixiang.yshop.framework.websocket.core.sender.WebSocketMessageSender; import co.yixiang.yshop.framework.websocket.core.session.WebSocketSessionManager; /** * 本地的 {@link WebSocketMessageSender} 实现类 * * 注意:仅仅适合单机场景!!! * * @author yshop */ public class LocalWebSocketMessageSender extends AbstractWebSocketMessageSender { public LocalWebSocketMessageSender(WebSocketSessionManager sessionManager) { super(sessionManager); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java ================================================ package co.yixiang.yshop.framework.websocket.core.sender.rabbitmq; import lombok.Data; import java.io.Serializable; /** * RabbitMQ 广播 WebSocket 的消息 * * @author yshop */ @Data public class RabbitMQWebSocketMessage implements Serializable { /** * Session 编号 */ private String sessionId; /** * 用户类型 */ private Integer userType; /** * 用户编号 */ private Long userId; /** * 消息类型 */ private String messageType; /** * 消息内容 */ private String messageContent; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java ================================================ package co.yixiang.yshop.framework.websocket.core.sender.rabbitmq; import lombok.RequiredArgsConstructor; import org.springframework.amqp.core.ExchangeTypes; import org.springframework.amqp.rabbit.annotation.*; /** * {@link RabbitMQWebSocketMessage} 广播消息的消费者,真正把消息发送出去 * * @author yshop */ @RabbitListener( bindings = @QueueBinding( value = @Queue( // 在 Queue 的名字上,使用 UUID 生成其后缀。这样,启动的 Consumer 的 Queue 不同,以达到广播消费的目的 name = "${yshop.websocket.sender-rabbitmq.queue}" + "-" + "#{T(java.util.UUID).randomUUID()}", // Consumer 关闭时,该队列就可以被自动删除了 autoDelete = "true" ), exchange = @Exchange( name = "${yshop.websocket.sender-rabbitmq.exchange}", type = ExchangeTypes.TOPIC, declare = "false" ) ) ) @RequiredArgsConstructor public class RabbitMQWebSocketMessageConsumer { private final RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender; @RabbitHandler public void onMessage(RabbitMQWebSocketMessage message) { rabbitMQWebSocketMessageSender.send(message.getSessionId(), message.getUserType(), message.getUserId(), message.getMessageType(), message.getMessageContent()); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java ================================================ package co.yixiang.yshop.framework.websocket.core.sender.rabbitmq; import co.yixiang.yshop.framework.websocket.core.sender.AbstractWebSocketMessageSender; import co.yixiang.yshop.framework.websocket.core.sender.WebSocketMessageSender; import co.yixiang.yshop.framework.websocket.core.session.WebSocketSessionManager; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.TopicExchange; import org.springframework.amqp.rabbit.core.RabbitTemplate; /** * 基于 RabbitMQ 的 {@link WebSocketMessageSender} 实现类 * * @author yshop */ @Slf4j public class RabbitMQWebSocketMessageSender extends AbstractWebSocketMessageSender { private final RabbitTemplate rabbitTemplate; private final TopicExchange topicExchange; public RabbitMQWebSocketMessageSender(WebSocketSessionManager sessionManager, RabbitTemplate rabbitTemplate, TopicExchange topicExchange) { super(sessionManager); this.rabbitTemplate = rabbitTemplate; this.topicExchange = topicExchange; } @Override public void send(Integer userType, Long userId, String messageType, String messageContent) { sendRabbitMQMessage(null, userId, userType, messageType, messageContent); } @Override public void send(Integer userType, String messageType, String messageContent) { sendRabbitMQMessage(null, null, userType, messageType, messageContent); } @Override public void send(String sessionId, String messageType, String messageContent) { sendRabbitMQMessage(sessionId, null, null, messageType, messageContent); } /** * 通过 RabbitMQ 广播消息 * * @param sessionId Session 编号 * @param userId 用户编号 * @param userType 用户类型 * @param messageType 消息类型 * @param messageContent 消息内容 */ private void sendRabbitMQMessage(String sessionId, Long userId, Integer userType, String messageType, String messageContent) { RabbitMQWebSocketMessage mqMessage = new RabbitMQWebSocketMessage() .setSessionId(sessionId).setUserId(userId).setUserType(userType) .setMessageType(messageType).setMessageContent(messageContent); rabbitTemplate.convertAndSend(topicExchange.getName(), null, mqMessage); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/sender/redis/RedisWebSocketMessage.java ================================================ package co.yixiang.yshop.framework.websocket.core.sender.redis; import co.yixiang.yshop.framework.mq.redis.core.pubsub.AbstractRedisChannelMessage; import lombok.Data; /** * Redis 广播 WebSocket 的消息 */ @Data public class RedisWebSocketMessage extends AbstractRedisChannelMessage { /** * Session 编号 */ private String sessionId; /** * 用户类型 */ private Integer userType; /** * 用户编号 */ private Long userId; /** * 消息类型 */ private String messageType; /** * 消息内容 */ private String messageContent; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java ================================================ package co.yixiang.yshop.framework.websocket.core.sender.redis; import co.yixiang.yshop.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener; import lombok.RequiredArgsConstructor; /** * {@link RedisWebSocketMessage} 广播消息的消费者,真正把消息发送出去 * * @author yshop */ @RequiredArgsConstructor public class RedisWebSocketMessageConsumer extends AbstractRedisChannelMessageListener { private final RedisWebSocketMessageSender redisWebSocketMessageSender; @Override public void onMessage(RedisWebSocketMessage message) { redisWebSocketMessageSender.send(message.getSessionId(), message.getUserType(), message.getUserId(), message.getMessageType(), message.getMessageContent()); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java ================================================ package co.yixiang.yshop.framework.websocket.core.sender.redis; import co.yixiang.yshop.framework.mq.redis.core.RedisMQTemplate; import co.yixiang.yshop.framework.websocket.core.sender.AbstractWebSocketMessageSender; import co.yixiang.yshop.framework.websocket.core.sender.WebSocketMessageSender; import co.yixiang.yshop.framework.websocket.core.session.WebSocketSessionManager; import lombok.extern.slf4j.Slf4j; /** * 基于 Redis 的 {@link WebSocketMessageSender} 实现类 * * @author yshop */ @Slf4j public class RedisWebSocketMessageSender extends AbstractWebSocketMessageSender { private final RedisMQTemplate redisMQTemplate; public RedisWebSocketMessageSender(WebSocketSessionManager sessionManager, RedisMQTemplate redisMQTemplate) { super(sessionManager); this.redisMQTemplate = redisMQTemplate; } @Override public void send(Integer userType, Long userId, String messageType, String messageContent) { sendRedisMessage(null, userId, userType, messageType, messageContent); } @Override public void send(Integer userType, String messageType, String messageContent) { sendRedisMessage(null, null, userType, messageType, messageContent); } @Override public void send(String sessionId, String messageType, String messageContent) { sendRedisMessage(sessionId, null, null, messageType, messageContent); } /** * 通过 Redis 广播消息 * * @param sessionId Session 编号 * @param userId 用户编号 * @param userType 用户类型 * @param messageType 消息类型 * @param messageContent 消息内容 */ private void sendRedisMessage(String sessionId, Long userId, Integer userType, String messageType, String messageContent) { RedisWebSocketMessage mqMessage = new RedisWebSocketMessage() .setSessionId(sessionId).setUserId(userId).setUserType(userType) .setMessageType(messageType).setMessageContent(messageContent); redisMQTemplate.send(mqMessage); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java ================================================ package co.yixiang.yshop.framework.websocket.core.sender.rocketmq; import lombok.Data; /** * RocketMQ 广播 WebSocket 的消息 * * @author yshop */ @Data public class RocketMQWebSocketMessage { /** * Session 编号 */ private String sessionId; /** * 用户类型 */ private Integer userType; /** * 用户编号 */ private Long userId; /** * 消息类型 */ private String messageType; /** * 消息内容 */ private String messageContent; } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java ================================================ package co.yixiang.yshop.framework.websocket.core.sender.rocketmq; import lombok.RequiredArgsConstructor; import org.apache.rocketmq.spring.annotation.MessageModel; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.core.RocketMQListener; /** * {@link RocketMQWebSocketMessage} 广播消息的消费者,真正把消息发送出去 * * @author yshop */ @RocketMQMessageListener( // 重点:添加 @RocketMQMessageListener 注解,声明消费的 topic topic = "${yshop.websocket.sender-rocketmq.topic}", consumerGroup = "${yshop.websocket.sender-rocketmq.consumer-group}", messageModel = MessageModel.BROADCASTING // 设置为广播模式,保证每个实例都能收到消息 ) @RequiredArgsConstructor public class RocketMQWebSocketMessageConsumer implements RocketMQListener { private final RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender; @Override public void onMessage(RocketMQWebSocketMessage message) { rocketMQWebSocketMessageSender.send(message.getSessionId(), message.getUserType(), message.getUserId(), message.getMessageType(), message.getMessageContent()); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java ================================================ package co.yixiang.yshop.framework.websocket.core.sender.rocketmq; import co.yixiang.yshop.framework.websocket.core.sender.AbstractWebSocketMessageSender; import co.yixiang.yshop.framework.websocket.core.sender.WebSocketMessageSender; import co.yixiang.yshop.framework.websocket.core.session.WebSocketSessionManager; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.spring.core.RocketMQTemplate; /** * 基于 RocketMQ 的 {@link WebSocketMessageSender} 实现类 * * @author yshop */ @Slf4j public class RocketMQWebSocketMessageSender extends AbstractWebSocketMessageSender { private final RocketMQTemplate rocketMQTemplate; private final String topic; public RocketMQWebSocketMessageSender(WebSocketSessionManager sessionManager, RocketMQTemplate rocketMQTemplate, String topic) { super(sessionManager); this.rocketMQTemplate = rocketMQTemplate; this.topic = topic; } @Override public void send(Integer userType, Long userId, String messageType, String messageContent) { sendRocketMQMessage(null, userId, userType, messageType, messageContent); } @Override public void send(Integer userType, String messageType, String messageContent) { sendRocketMQMessage(null, null, userType, messageType, messageContent); } @Override public void send(String sessionId, String messageType, String messageContent) { sendRocketMQMessage(sessionId, null, null, messageType, messageContent); } /** * 通过 RocketMQ 广播消息 * * @param sessionId Session 编号 * @param userId 用户编号 * @param userType 用户类型 * @param messageType 消息类型 * @param messageContent 消息内容 */ private void sendRocketMQMessage(String sessionId, Long userId, Integer userType, String messageType, String messageContent) { RocketMQWebSocketMessage mqMessage = new RocketMQWebSocketMessage() .setSessionId(sessionId).setUserId(userId).setUserType(userType) .setMessageType(messageType).setMessageContent(messageContent); rocketMQTemplate.syncSend(topic, mqMessage); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java ================================================ package co.yixiang.yshop.framework.websocket.core.session; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator; import org.springframework.web.socket.handler.WebSocketHandlerDecorator; /** * {@link WebSocketHandler} 的装饰类,实现了以下功能: * * 1. {@link WebSocketSession} 连接或关闭时,使用 {@link #sessionManager} 进行管理 * 2. 封装 {@link WebSocketSession} 支持并发操作 * * @author yshop */ public class WebSocketSessionHandlerDecorator extends WebSocketHandlerDecorator { /** * 发送时间的限制,单位:毫秒 */ private static final Integer SEND_TIME_LIMIT = 1000 * 5; /** * 发送消息缓冲上线,单位:bytes */ private static final Integer BUFFER_SIZE_LIMIT = 1024 * 100; private final WebSocketSessionManager sessionManager; public WebSocketSessionHandlerDecorator(WebSocketHandler delegate, WebSocketSessionManager sessionManager) { super(delegate); this.sessionManager = sessionManager; } @Override public void afterConnectionEstablished(WebSocketSession session) { // 实现 session 支持并发,可参考 https://blog.csdn.net/abu935009066/article/details/131218149 session = new ConcurrentWebSocketSessionDecorator(session, SEND_TIME_LIMIT, BUFFER_SIZE_LIMIT); // 添加到 WebSocketSessionManager 中 sessionManager.addSession(session); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) { sessionManager.removeSession(session); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/session/WebSocketSessionManager.java ================================================ package co.yixiang.yshop.framework.websocket.core.session; import org.springframework.web.socket.WebSocketSession; import java.util.Collection; /** * {@link WebSocketSession} 管理器的接口 * * @author yshop */ public interface WebSocketSessionManager { /** * 添加 Session * * @param session Session */ void addSession(WebSocketSession session); /** * 移除 Session * * @param session Session */ void removeSession(WebSocketSession session); /** * 获得指定编号的 Session * * @param id Session 编号 * @return Session */ WebSocketSession getSession(String id); /** * 获得指定用户类型的 Session 列表 * * @param userType 用户类型 * @return Session 列表 */ Collection getSessionList(Integer userType); /** * 获得指定用户编号的 Session 列表 * * @param userType 用户类型 * @param userId 用户编号 * @return Session 列表 */ Collection getSessionList(Integer userType, Long userId); } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/session/WebSocketSessionManagerImpl.java ================================================ package co.yixiang.yshop.framework.websocket.core.session; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.security.core.LoginUser; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import co.yixiang.yshop.framework.websocket.core.util.WebSocketFrameworkUtils; import org.springframework.web.socket.WebSocketSession; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; /** * 默认的 {@link WebSocketSessionManager} 实现类 * * @author yshop */ public class WebSocketSessionManagerImpl implements WebSocketSessionManager { /** * id 与 WebSocketSession 映射 * * key:Session 编号 */ private final ConcurrentMap idSessions = new ConcurrentHashMap<>(); /** * user 与 WebSocketSession 映射 * * key1:用户类型 * key2:用户编号 */ private final ConcurrentMap>> userSessions = new ConcurrentHashMap<>(); @Override public void addSession(WebSocketSession session) { // 添加到 idSessions 中 idSessions.put(session.getId(), session); // 添加到 userSessions 中 LoginUser user = WebSocketFrameworkUtils.getLoginUser(session); if (user == null) { return; } ConcurrentMap> userSessionsMap = userSessions.get(user.getUserType()); if (userSessionsMap == null) { userSessionsMap = new ConcurrentHashMap<>(); if (userSessions.putIfAbsent(user.getUserType(), userSessionsMap) != null) { userSessionsMap = userSessions.get(user.getUserType()); } } CopyOnWriteArrayList sessions = userSessionsMap.get(user.getId()); if (sessions == null) { sessions = new CopyOnWriteArrayList<>(); if (userSessionsMap.putIfAbsent(user.getId(), sessions) != null) { sessions = userSessionsMap.get(user.getId()); } } sessions.add(session); } @Override public void removeSession(WebSocketSession session) { // 移除从 idSessions 中 idSessions.remove(session.getId()); // 移除从 idSessions 中 LoginUser user = WebSocketFrameworkUtils.getLoginUser(session); if (user == null) { return; } ConcurrentMap> userSessionsMap = userSessions.get(user.getUserType()); if (userSessionsMap == null) { return; } CopyOnWriteArrayList sessions = userSessionsMap.get(user.getId()); sessions.removeIf(session0 -> session0.getId().equals(session.getId())); if (CollUtil.isEmpty(sessions)) { userSessionsMap.remove(user.getId(), sessions); } } @Override public WebSocketSession getSession(String id) { return idSessions.get(id); } @Override public Collection getSessionList(Integer userType) { ConcurrentMap> userSessionsMap = userSessions.get(userType); if (CollUtil.isEmpty(userSessionsMap)) { return new ArrayList<>(); } LinkedList result = new LinkedList<>(); // 避免扩容 Long contextTenantId = TenantContextHolder.getTenantId(); for (List sessions : userSessionsMap.values()) { if (CollUtil.isEmpty(sessions)) { continue; } // 特殊:如果租户不匹配,则直接排除 if (contextTenantId != null) { Long userTenantId = WebSocketFrameworkUtils.getTenantId(sessions.get(0)); if (!contextTenantId.equals(userTenantId)) { continue; } } result.addAll(sessions); } return result; } @Override public Collection getSessionList(Integer userType, Long userId) { ConcurrentMap> userSessionsMap = userSessions.get(userType); if (CollUtil.isEmpty(userSessionsMap)) { return new ArrayList<>(); } CopyOnWriteArrayList sessions = userSessionsMap.get(userId); return CollUtil.isNotEmpty(sessions) ? new ArrayList<>(sessions) : new ArrayList<>(); } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/java/co/yixiang/yshop/framework/websocket/core/util/WebSocketFrameworkUtils.java ================================================ package co.yixiang.yshop.framework.websocket.core.util; import co.yixiang.yshop.framework.security.core.LoginUser; import org.springframework.web.socket.WebSocketSession; import java.util.Map; /** * 专属于 web 包的工具类 * * @author yshop */ public class WebSocketFrameworkUtils { public static final String ATTRIBUTE_LOGIN_USER = "LOGIN_USER"; /** * 设置当前用户 * * @param loginUser 登录用户 * @param attributes Session */ public static void setLoginUser(LoginUser loginUser, Map attributes) { attributes.put(ATTRIBUTE_LOGIN_USER, loginUser); } /** * 获取当前用户 * * @return 当前用户 */ public static LoginUser getLoginUser(WebSocketSession session) { return (LoginUser) session.getAttributes().get(ATTRIBUTE_LOGIN_USER); } /** * 获得当前用户的编号 * * @return 用户编号 */ public static Long getLoginUserId(WebSocketSession session) { LoginUser loginUser = getLoginUser(session); return loginUser != null ? loginUser.getId() : null; } /** * 获得当前用户的类型 * * @return 用户编号 */ public static Integer getLoginUserType(WebSocketSession session) { LoginUser loginUser = getLoginUser(session); return loginUser != null ? loginUser.getUserType() : null; } /** * 获得当前用户的租户编号 * * @param session Session * @return 租户编号 */ public static Long getTenantId(WebSocketSession session) { LoginUser loginUser = getLoginUser(session); return loginUser != null ? loginUser.getTenantId() : null; } } ================================================ FILE: yshop-drink-boot3/yshop-framework/yshop-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ co.yixiang.yshop.framework.websocket.config.YshopWebSocketAutoConfiguration ================================================ FILE: yshop-drink-boot3/yshop-module-express/pom.xml ================================================ co.yixiang.boot yshop ${revision} 4.0.0 yshop-module-express pom yshop-module-express-api yshop-module-express-biz ${project.artifactId} express 模块 ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-api/pom.xml ================================================ yshop-module-express co.yixiang.boot ${revision} 4.0.0 yshop-module-express-api jar ${project.artifactId} pay 模块 API,暴露给其它模块调用 co.yixiang.boot yshop-common org.springframework.boot spring-boot-starter-validation true co.yixiang.boot yshop-spring-boot-starter-mybatis ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-api/src/main/java/co/yixiang/yshop/module/express/enums/ErrorCodeConstants.java ================================================ package co.yixiang.yshop.module.express.enums; import co.yixiang.yshop.framework.common.exception.ErrorCode; public interface ErrorCodeConstants { // ========== 快递公司 1008008000 ========== ErrorCode EXPRESS_NOT_EXISTS = new ErrorCode(1008008000, "快递公司不存在"); // ========== 电子面单 ========== ErrorCode ELECTRONICS_ORDER_NOT_EXISTS = new ErrorCode(1008009000, "电子面单不存在"); } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-api/src/main/java/co/yixiang/yshop/module/express/kdniao/enums/KdniaoLogisticsCodeEnum.java ================================================ package co.yixiang.yshop.module.express.kdniao.enums; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.SneakyThrows; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** *

物流公司-对应编码-枚举

* * @author hupeng * @date 2023/7/219 */ @Getter @AllArgsConstructor public enum KdniaoLogisticsCodeEnum { /** * 申通 */ STO("STO", "申通"), /** * 中通 */ ZTO("ZTO", "中通"), /** * 圆通 */ YTO("YTO", "圆通"), /** * 韵达 */ YD("YD", "韵达"), /** * 顺丰 */ SF("SF", "顺丰"); /** * 物流编码 */ private final String code; /** * 物流名 */ private final String name; private static final List LIST = new ArrayList(); static { LIST.addAll(Arrays.asList(KdniaoLogisticsCodeEnum.values())); } /** * 根据值查找相应枚举 */ @SneakyThrows(Exception.class) public static KdniaoLogisticsCodeEnum getEnumByName(String name) { for (KdniaoLogisticsCodeEnum itemEnum : LIST) { if (itemEnum.getName().equals(name)) { return itemEnum; } } throw new Exception("暂无此物流编码信息,请联系系统管理员!"); } } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-api/src/main/java/co/yixiang/yshop/module/express/kdniao/enums/KdniaoLogisticsStatusEnum.java ================================================ package co.yixiang.yshop.module.express.kdniao.enums; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** *

物流状态枚举

* * @author hupeng * @date 2023/7/219 */ @Getter @AllArgsConstructor public enum KdniaoLogisticsStatusEnum { /** * 暂无轨迹信息 */ NO_TRACE(0, "暂无轨迹信息"), /** * 已揽收 */ HAVE_PAID(1, "已揽收"), /** * 已揽收 ----------------------------------------------------------------------------- */ ON_THE_WAY(2, "在途中"), /** * 到达派件城市 */ ARRIVE_AT_THE_DISPATCH_CITY(201, "到达派件城市"), /** * 派件中 */ IN_THE_DELIVERY(202, "派件中"), /** * 已放入快递柜或驿站 */ HAS_STORED(211, "已放入快递柜或驿站"), /** * 签收 ----------------------------------------------------------------------------- */ SIGN(3, "签收"), /** * 正常签收 */ SIGN_NORMAL(301, "正常签收"), /** * 派件异常后最终签收 */ SIGN_ABNORMAL(302, "派件异常后最终签收"), /** * 代收签收 */ SIGN_COLLECTION(304, "代收签收"), /** * 快递柜或驿站签收 */ SIGN_STORED(311, "快递柜或驿站签收"), /** * 问题件 ----------------------------------------------------------------------------- */ PROBLEM_SHIPMENT(4, "问题件"), /** * 发货无信息 */ DELIVERY_NO_INFO(401, "发货无信息"), /** * 超时未签收 */ NO_SIGN_OVER_TIME(402, "超时未签收"), /** * 超时未更新 */ NOT_UPDATED_DUE_TO_TIMEOUT(403, "超时未更新"), /** * 拒收(退件) */ REJECTION(404, "拒收(退件)"), /** * 派件异常 */ SEND_A_ABNORMAL(405, "派件异常"), /** * 退货签收 */ RETURN_TO_SIGN_FOR(406, "退货签收"), /** * 退货未签收 */ RETURN_NOT_SIGNED_FOR(407, "退货未签收"), /** * 快递柜或驿站超时未取 */ STORED_OVER_TIME(412, "快递柜或驿站超时未取"), /** * - */ DEFAULT(0, "-"); /** * 状态 */ private final Integer status; /** * 描述 */ private final String desc; private static final List LIST = new ArrayList(); static { LIST.addAll(Arrays.asList(KdniaoLogisticsStatusEnum.values())); } /** * 根据物流状态查找相应枚举 */ public static KdniaoLogisticsStatusEnum getEnum(Integer status) { for (KdniaoLogisticsStatusEnum itemEnum : LIST) { if (itemEnum.getStatus().equals(status)) { return itemEnum; } } return KdniaoLogisticsStatusEnum.DEFAULT; } } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-api/src/main/java/co/yixiang/yshop/module/express/kdniao/model/dto/KdniaoApiBaseDTO.java ================================================ package co.yixiang.yshop.module.express.kdniao.model.dto; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; /** *

* 快递鸟-物流-查询base参数 *

* * @author hupeng * @date 2023/7/21 */ @Data @SuperBuilder @NoArgsConstructor @AllArgsConstructor //快递鸟-物流-查询base参数 public class KdniaoApiBaseDTO { //用户ID private String eBusinessID; //API key private String apiKey; //请求url //https://api.kdniao.com/Ebusiness/EbusinessOrderHandle.aspx private String reqURL; private Boolean isFree; } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-api/src/main/java/co/yixiang/yshop/module/express/kdniao/model/dto/KdniaoApiDTO.java ================================================ package co.yixiang.yshop.module.express.kdniao.model.dto; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; /** *

* 快递鸟-物流-查询参数 *

* * @author hupeng * @date 2023/7/21 */ @Data @SuperBuilder @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(callSuper = true) //快递鸟-物流-查询参数 public class KdniaoApiDTO extends KdniaoApiBaseDTO { //快递公司编码 ZTO private String shipperCode; //快递单号 private String logisticCode; //是否收费 true=免费 false 收费 private Boolean isFree; } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-api/src/main/java/co/yixiang/yshop/module/express/kdniao/model/dto/KdniaoElectronicsOrderDTO.java ================================================ package co.yixiang.yshop.module.express.kdniao.model.dto; import lombok.*; /** * 电子面单 DTO * * @author yshop */ @Data @ToString(callSuper = true) @NoArgsConstructor @AllArgsConstructor public class KdniaoElectronicsOrderDTO extends KdniaoApiBaseDTO { //用户 private Long memberID; //订单编号 private String orderCode; //快递公司编码 private String shipperCode; //快递单号 private String logisticCode; //其他费用 private Double otherCost; /** * 运费支付方式:1=现付,2=到付,3=月结,4=第三方付(仅SF支持) */ private Integer paytype; /** * 线下网点客户号 */ private String customerName; /** * 线下网点密码 */ private String customerPwd; /** * 网点名称 */ private String sendSite; /** * 网点快递员 */ private String sendStaff; //快递运费 private Double cost; /** * 月结编号 */ private String monthCode; /** * 是否通知揽件:0=通知揽件,1=不通知揽件 */ private Integer isNotice; /** * 是否返回电子面单模板:0=不返回,1=返回 */ private Integer isReturnTemp; /** * 是否需要短信提醒:0=否,1=是 */ private Integer isSendMessage; /** * 模板尺寸 */ private String templateSize; /** * 签回单操作要求(如:签名、盖章、身份证复印件等) */ private String operateRequire; /** * 上门揽件开始时间 */ private Integer startDate; /** * 上门揽件结束时间 */ private Integer endDate; /** * 备注 */ private String remark; /** * 快递类型:1=标准快件 */ private Integer expType; /** * 是否要签回单:0=否,1=是 */ private Integer isReturnSignBill; /** * 发件人公司 */ private String company; /** * 发件人省 */ private String provinceName; /** * 发件人市 */ private String cityName; /** * 发件人区 */ private String expAreaName; /** * 发件人详细地址 */ private String address; /** * 发件人姓名 */ private String name; /** * 发件人电话 */ private String tel; /** * 发件人手机号码 */ private String mobile; /** * 发件地邮编 */ private String postCode; /** * 收件人公司 */ private String receiverCompany; /** * 收件人省 */ private String receiverProvinceName; /** * 收件人市 */ private String receiverCityName; /** * 收件人区 */ private String receiverExpAreaName; /** * 收件人详细地址 */ private String receiverAddress; /** * 收件人姓名 */ private String receiverName; /** * 收件人电话 */ private String receiverTel; /** * 收件人手机号码 */ private String receiverMobile; /** * 收件地邮编 */ private String receiverPostCode; /** * 重量 */ private Double weight; /** * 重量 */ private Integer quantity; /** * 体积 * */ private Double volume; } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-api/src/main/java/co/yixiang/yshop/module/express/kdniao/model/dto/KdniaoElectronicsOrderGoodsDTO.java ================================================ package co.yixiang.yshop.module.express.kdniao.model.dto; import lombok.*; /** * 电子面单 DTO * * @author yshop */ @Data @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class KdniaoElectronicsOrderGoodsDTO { /** * 商品名称 */ private String goodsName; /** * 商品编号 */ private String goodsCode; /** * 商品件数 */ private Integer goodsQuantity; /** * 商品价格 */ private Double goodsPrice; /** * 商品重量kg */ private Double goodsWeight; /** * 商品描述 */ private String GoodsDesc; /** * 商品体积m³ */ private Double GoodsVol; } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-api/src/main/java/co/yixiang/yshop/module/express/kdniao/model/vo/KdniaoApiVO.java ================================================ package co.yixiang.yshop.module.express.kdniao.model.vo; import co.yixiang.yshop.module.express.kdniao.enums.KdniaoLogisticsStatusEnum; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.List; /** *

* 快递鸟-物流-响应参数 *

* @author hupeng * @date 2023/7/21 */ @Data @SuperBuilder @NoArgsConstructor @AllArgsConstructor public class KdniaoApiVO { /** * {@link KdniaoLogisticsStatusEnum } * 增值物流状态: * 0-暂无轨迹信息 * 1-已揽收 * 2-在途中 * 201-到达派件城市, 202-派件中, 211-已放入快递柜或驿站, * 3-已签收 * 301-正常签收, 302-派件异常后最终签收, 304-代收签收, 311-快递柜或驿站签收, * 4-问题件 * 401-发货无信息, 402-超时未签收, 403-超时未更新, 404-拒收(退件), 405-派件异常, 406-退货签收, 407-退货未签收, 412-快递柜或驿站超时未取 */ //增值物流状态 private Integer StateEx; //增值物流状态名称 private String statusExName; //快递单号 private String LogisticCode; //快递公司编码 private String ShipperCode; //失败原因 private String Reason; //事件轨迹集 private List Traces; /** * {@link KdniaoLogisticsStatusEnum } */ //物流状态:0-暂无轨迹信息,1-已揽收,2-在途中,3-签收,4-问题件 private Integer State; //状态名称 private String statusName; //用户ID private String EBusinessID; //送货人 private String DeliveryMan; //送货人电话号码 private String DeliveryManTel; //成功与否 true/false private String Success; //所在城市 private String Location; @Data @Builder @NoArgsConstructor @AllArgsConstructor //事件轨迹集 public static class TraceItem { /** * {@link KdniaoLogisticsStatusEnum } */ //当前状态(同StateEx) private Integer Action; //状态名称 private String actionName; //描述 private String AcceptStation; //时间 private String AcceptTime; //所在城市 private String Location; } public void handleData() { this.statusName = KdniaoLogisticsStatusEnum.getEnum(this.State).getDesc(); this.statusExName = KdniaoLogisticsStatusEnum.getEnum(this.StateEx).getDesc(); if (CollectionUtils.isEmpty(this.Traces)) { this.Traces = new ArrayList(); } this.Traces.forEach(item -> item.actionName = KdniaoLogisticsStatusEnum.getEnum(item.Action).getDesc()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-api/src/main/java/co/yixiang/yshop/module/express/kdniao/model/vo/KdniaoLogisticsVO.java ================================================ package co.yixiang.yshop.module.express.kdniao.model.vo; import co.yixiang.yshop.module.express.kdniao.enums.KdniaoLogisticsCodeEnum; import co.yixiang.yshop.module.express.kdniao.enums.KdniaoLogisticsStatusEnum; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.List; /** *

快递鸟-物流-参数

* * @author hupeng * @date 2023/7/21 */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class KdniaoLogisticsVO { /** * {@link KdniaoLogisticsCodeEnum } */ //快递公司编码 private String logisticsCode; //快递单号 private String logisticsNo; //事件轨迹集 private List traceList; //送货人名称 private String delivererName; //送货人电话号码 private String delivererPhone; //所在城市 private String location; /** * {@link KdniaoLogisticsStatusEnum } */ //物流状态 private Integer status; //状态名称 private String statusName; /** * {@link KdniaoLogisticsStatusEnum } */ //增值物流状态 private Integer statusEx; //增值物流状态名称 private String statusExName; @Data @Builder @NoArgsConstructor @AllArgsConstructor //事件轨迹集 public static class TraceItem { //状态 private Integer status; //状态名称 private String statusName; //描述 private String desc; //时间 private String time; //所在城市 private String location; } public void handleData() { this.statusName = KdniaoLogisticsStatusEnum.getEnum(this.status).getDesc(); this.statusExName = KdniaoLogisticsStatusEnum.getEnum(this.statusEx).getDesc(); if (CollectionUtils.isEmpty(this.traceList)) { this.traceList = new ArrayList(); } this.traceList.forEach(item -> item.statusName = KdniaoLogisticsStatusEnum.getEnum(item.status).getDesc()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-api/src/main/java/co/yixiang/yshop/module/express/kdniao/model/vo/KdniaoOrderVO.java ================================================ package co.yixiang.yshop.module.express.kdniao.model.vo; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; /** *

* 快递鸟-面单-响应参数 *

* @author hupeng * @date 2023/7/22 */ @Data @SuperBuilder @NoArgsConstructor @AllArgsConstructor public class KdniaoOrderVO { //返回编码 private String ResultCode; //失败原因 private String Reason; //成功与否 true/false private String Success; //面单打印模板内容(html格式) private String PrintTemplate; //唯一标识 private String UniquerRequestNumber; //子单数量 private Integer SubCount; //子单单号 private String SubOrders; //子单模板内容(html格式) private String SubPrintTemplates; //签回单模板内容 private String SignBillPrintTemplate; //订单信息 private OrderInfo Order; @Data @Builder @NoArgsConstructor @AllArgsConstructor public static class OrderInfo { //订单编号 private String OrderCode; //快递公司编码 private String ShipperCode; //快递单号 private String LogisticCode; //大头笔 private String MarkDestination; //签回单单号 private String SignWaybillCode; //始发地区域编码 private String OriginCode; //始发地/始发网点 private String OriginName; //目的地区域编码 private String DestinatioCode; //目的地/到达网点 private String DestinatioName; //分拣编码 private String SortingCode; //集包编码 private String PackageCode; //集包地 private String PackageName; //目的地分拨 private String DestinationAllocationCentre; //配送产品类型 private Integer TransType; //运输方式(用于自行设计京东模板): //0:陆运 //1:航空 private Integer TransportType; //自行设计模板用 private String ShipperInfo; } } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-api/src/main/java/co/yixiang/yshop/module/express/kdniao/util/KdniaoUtil.java ================================================ package co.yixiang.yshop.module.express.kdniao.util; import co.yixiang.yshop.framework.common.exception.ErrorCode; import co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil; import co.yixiang.yshop.module.express.kdniao.model.dto.KdniaoApiDTO; import co.yixiang.yshop.module.express.kdniao.model.dto.KdniaoElectronicsOrderDTO; import co.yixiang.yshop.module.express.kdniao.model.dto.KdniaoElectronicsOrderGoodsDTO; import co.yixiang.yshop.module.express.kdniao.model.vo.KdniaoApiVO; import co.yixiang.yshop.module.express.kdniao.model.vo.KdniaoOrderVO; import com.alibaba.fastjson.JSON; import cn.hutool.core.codec.Base64; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.security.MessageDigest; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** *

快递鸟工具类

* * @author hupeng * @date 2023/7/219 */ @Slf4j public class KdniaoUtil { //查询物流当url private final String KDNIAO_LOGISTIC_QUERY = "https://api.kdniao.com/Ebusiness/EbusinessOrderHandle.aspx"; //电子面单url private final String KDNIAO_ELECT_QUERY = "https://api.kdniao.com/api/EOrderService"; /** * 快递查询接口 * * @param queryDTO 请求参数 * @return 物流信息 */ public static KdniaoApiVO getLogisticInfo(KdniaoApiDTO queryDTO){ KdniaoApiVO kdniaoApiVO = new KdniaoUtil().getLogisticBase(queryDTO); if (kdniaoApiVO.getSuccess() == "false"){ throw ServiceExceptionUtil.exception(new ErrorCode(999999,kdniaoApiVO.getReason())); } kdniaoApiVO.handleData(); return kdniaoApiVO; } /** * 获取电子面单信息 * @param queryDTO * @param kdniaoElectronicsOrderGoodsDTOList * @return */ public static KdniaoOrderVO getOrderInfo(KdniaoElectronicsOrderDTO queryDTO, List kdniaoElectronicsOrderGoodsDTOList) { KdniaoOrderVO kdniaoOrderVO = new KdniaoUtil().getEleCtBase(queryDTO,kdniaoElectronicsOrderGoodsDTOList); //todo 由于目前快递鸟订单打印需要申请当地营业网店账号 所有目前这个没法测试 如果有其他用户有可以测试反馈给我们官方 if (kdniaoOrderVO.getSuccess() == "false"){ log.error(kdniaoOrderVO.getReason()); throw ServiceExceptionUtil.exception(new ErrorCode(999999,kdniaoOrderVO.getReason())); } return kdniaoOrderVO; } /** * 快递查询接口 * * @param queryDTO 请求参数 * @return 物流信息 */ @SneakyThrows(Exception.class) private KdniaoApiVO getLogisticBase(KdniaoApiDTO queryDTO) { String EBusinessID = queryDTO.getEBusinessID(); String ApiKey = queryDTO.getApiKey(); String shipperCode = queryDTO.getShipperCode(); String logisticCode = queryDTO.getLogisticCode(); // 组装应用级参数 Map requestParamMap = new HashMap<>(); requestParamMap.put("shipperCode", shipperCode); requestParamMap.put("LogisticCode", logisticCode); String RequestData = JSON.toJSONString(requestParamMap); // 组装系统级参数 Map params = new HashMap<>(); params.put("RequestData", this.urlEncoder(RequestData, "UTF-8")); params.put("EBusinessID", EBusinessID); if(queryDTO.getIsFree()) { params.put("RequestType", "1002");//免费1002 收费8001 }else{ params.put("RequestType", "8001"); } String dataSign = this.encrypt(RequestData, ApiKey, "UTF-8"); params.put("DataSign", this.urlEncoder(dataSign, "UTF-8")); params.put("DataType", "2"); // 以form表单形式提交post请求,post请求体中包含了应用级参数和系统级参数 String resultJson = this.sendPost(KDNIAO_LOGISTIC_QUERY, params); return JSON.parseObject(resultJson, KdniaoApiVO.class); } /** * 快递查询接口 * * @param queryDTO 请求参数 * @return 物流信息 */ @SneakyThrows(Exception.class) private KdniaoOrderVO getEleCtBase(KdniaoElectronicsOrderDTO queryDTO, List kdniaoElectronicsOrderGoodsDTOList) { String EBusinessID = queryDTO.getEBusinessID(); String ApiKey = queryDTO.getApiKey(); Map requestParamMap = this.doMap(queryDTO,kdniaoElectronicsOrderGoodsDTOList); System.out.println("map:"+requestParamMap); String RequestData = JSON.toJSONString(requestParamMap); // 组装系统级参数 Map params = new HashMap<>(); params.put("RequestData", this.urlEncoder(RequestData, "UTF-8")); params.put("EBusinessID", EBusinessID); params.put("RequestType", "1007"); String dataSign = this.encrypt(RequestData, ApiKey, "UTF-8"); params.put("DataSign", this.urlEncoder(dataSign, "UTF-8")); params.put("DataType", "2"); String resultJson = this.sendPost(KDNIAO_ELECT_QUERY, params); return JSON.parseObject(resultJson, KdniaoOrderVO.class); } //组合数据 private Map doMap(KdniaoElectronicsOrderDTO queryDTO, List kdniaoElectronicsOrderGoodsDTOList){ // 组装应用级参数 Map requestParamMap = new HashMap<>(); requestParamMap.put("MemberID", queryDTO.getMemberID()); requestParamMap.put("OrderCode", queryDTO.getOrderCode()); requestParamMap.put("ShipperCode", queryDTO.getShipperCode()); requestParamMap.put("LogisticCode", queryDTO.getLogisticCode()); requestParamMap.put("CustomerName", queryDTO.getCustomerName()); requestParamMap.put("CustomerPwd", queryDTO.getCustomerPwd()); requestParamMap.put("SendSite", queryDTO.getSendSite()); requestParamMap.put("PayType", queryDTO.getPaytype()); requestParamMap.put("MonthCode", queryDTO.getMonthCode()); requestParamMap.put("IsReturnSignBill", queryDTO.getIsReturnSignBill()); requestParamMap.put("OperateRequire", queryDTO.getOperateRequire()); requestParamMap.put("ExpType", queryDTO.getExpType()); requestParamMap.put("Cost", queryDTO.getCost()); requestParamMap.put("OtherCost", queryDTO.getOtherCost()); Map senderMap = new HashMap<>(); senderMap.put("Name", queryDTO.getName()); senderMap.put("Tel", queryDTO.getTel()); senderMap.put("Company", queryDTO.getCompany()); senderMap.put("Mobile", queryDTO.getMobile()); senderMap.put("PostCode", queryDTO.getPostCode()); senderMap.put("ProvinceName", queryDTO.getProvinceName()); senderMap.put("CityName", queryDTO.getCityName()); senderMap.put("ExpAreaName", queryDTO.getExpAreaName()); senderMap.put("Address", queryDTO.getAddress()); requestParamMap.put("Sender", senderMap); Map receiverMap = new HashMap<>(); receiverMap.put("Name", queryDTO.getReceiverName()); receiverMap.put("Tel", queryDTO.getReceiverTel()); receiverMap.put("Company", queryDTO.getReceiverCompany()); receiverMap.put("Mobile", queryDTO.getReceiverMobile()); receiverMap.put("PostCode", queryDTO.getReceiverPostCode()); receiverMap.put("ProvinceName", queryDTO.getReceiverProvinceName()); receiverMap.put("CityName", queryDTO.getReceiverCityName()); receiverMap.put("ExpAreaName", queryDTO.getReceiverExpAreaName()); receiverMap.put("Address", queryDTO.getReceiverAddress()); requestParamMap.put("Receiver", receiverMap); List> commodityMapList = new ArrayList<>(); for (KdniaoElectronicsOrderGoodsDTO kdniaoElectronicsOrderGoodsDTO : kdniaoElectronicsOrderGoodsDTOList) { Map commodityMap = new HashMap<>(); commodityMap.put("GoodsName", kdniaoElectronicsOrderGoodsDTO.getGoodsName()); commodityMap.put("GoodsCode", kdniaoElectronicsOrderGoodsDTO.getGoodsCode()); commodityMap.put("Goodsquantity", kdniaoElectronicsOrderGoodsDTO.getGoodsQuantity()); commodityMap.put("GoodsPrice", kdniaoElectronicsOrderGoodsDTO.getGoodsPrice()); commodityMap.put("GoodsWeight", kdniaoElectronicsOrderGoodsDTO.getGoodsWeight()); commodityMap.put("GoodsVol", kdniaoElectronicsOrderGoodsDTO.getGoodsVol()); commodityMap.put("GoodsDesc", kdniaoElectronicsOrderGoodsDTO.getGoodsDesc()); commodityMapList.add(commodityMap); } requestParamMap.put("Commodity", commodityMapList); requestParamMap.put("IsNotice", queryDTO.getIsNotice()); requestParamMap.put("StartDate", queryDTO.getStartDate()); requestParamMap.put("EndDate", queryDTO.getEndDate()); requestParamMap.put("Weight", queryDTO.getWeight()); requestParamMap.put("Quantity", queryDTO.getQuantity()); requestParamMap.put("Volume", queryDTO.getVolume()); requestParamMap.put("IsReturnPrintTemplate", queryDTO.getIsReturnTemp()); requestParamMap.put("Remark", queryDTO.getRemark()); return requestParamMap; } /** * MD5加密 * str 内容 * charset 编码方式 * * @throws Exception */ @SuppressWarnings("unused") private String MD5(String str, String charset) throws Exception { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(str.getBytes(charset)); byte[] result = md.digest(); StringBuffer sb = new StringBuffer(32); for (int i = 0; i < result.length; i++) { int val = result[i] & 0xff; if (val <= 0xf) { sb.append("0"); } sb.append(Integer.toHexString(val)); } return sb.toString().toLowerCase(); } /** * base64编码 * str 内容 * charset 编码方式 * * @throws UnsupportedEncodingException */ private String base64(String str, String charset) throws UnsupportedEncodingException { String encoded = Base64.encode(str.getBytes(charset)); return encoded; } @SuppressWarnings("unused") private String urlEncoder(String str, String charset) throws UnsupportedEncodingException { String result = URLEncoder.encode(str, charset); return result; } /** * 电商Sign签名生成 * content 内容 * keyValue ApiKey * charset 编码方式 * * @return DataSign签名 * @throws UnsupportedEncodingException ,Exception */ @SuppressWarnings("unused") private String encrypt(String content, String keyValue, String charset) throws UnsupportedEncodingException, Exception { if (keyValue != null) { return base64(MD5(content + keyValue, charset), charset); } return base64(MD5(content, charset), charset); } /** * 向指定 URL 发送POST方法的请求 * url 发送请求的 URL * params 请求的参数集合 * * @return 远程资源的响应结果 */ @SuppressWarnings("unused") private String sendPost(String url, Map params) { OutputStreamWriter out = null; BufferedReader in = null; StringBuilder result = new StringBuilder(); try { URL realUrl = new URL(url); HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection(); // 发送POST请求必须设置如下两行 conn.setDoOutput(true); conn.setDoInput(true); // POST方法 conn.setRequestMethod("POST"); // 设置通用的请求属性 conn.setRequestProperty("accept", "*/*"); conn.setRequestProperty("connection", "Keep-Alive"); conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); conn.connect(); // 获取URLConnection对象对应的输出流 out = new OutputStreamWriter(conn.getOutputStream(), "UTF-8"); // 发送请求参数 if (params != null) { StringBuilder param = new StringBuilder(); for (Map.Entry entry : params.entrySet()) { if (param.length() > 0) { param.append("&"); } param.append(entry.getKey()); param.append("="); param.append(entry.getValue()); } log.info("[快递鸟] 请求参数: [{}]", param); out.write(param.toString()); } // flush输出流的缓冲 out.flush(); // 定义BufferedReader输入流来读取URL的响应 in = new BufferedReader( new InputStreamReader(conn.getInputStream(), "UTF-8")); String line; while ((line = in.readLine()) != null) { result.append(line); } } catch (Exception e) { e.printStackTrace(); } //使用finally块来关闭输出流、输入流 finally { try { if (out != null) { out.close(); } if (in != null) { in.close(); } } catch (IOException ex) { ex.printStackTrace(); } } return result.toString(); } } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-biz/pom.xml ================================================ yshop-module-express co.yixiang.boot ${revision} 4.0.0 yshop-module-express-biz jar ${project.artifactId} 快递 模块 co.yixiang.boot yshop-module-express-api ${revision} co.yixiang.boot yshop-spring-boot-starter-biz-tenant co.yixiang.boot yshop-spring-boot-starter-security co.yixiang.boot yshop-spring-boot-starter-redis co.yixiang.boot yshop-spring-boot-starter-mq co.yixiang.boot yshop-spring-boot-starter-test test co.yixiang.boot yshop-spring-boot-starter-excel ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-biz/src/main/java/co/yixiang/yshop/module/express/controller/admin/express/ExpressController.java ================================================ package co.yixiang.yshop.module.express.controller.admin.express; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.express.controller.admin.express.vo.*; import co.yixiang.yshop.module.express.convert.express.ExpressConvert; import co.yixiang.yshop.module.express.dal.dataobject.express.ExpressDO; import co.yixiang.yshop.module.express.dal.redis.express.ExpressRedisDAO; import co.yixiang.yshop.module.express.kdniao.model.dto.KdniaoApiBaseDTO; import co.yixiang.yshop.module.express.kdniao.model.dto.KdniaoApiDTO; import co.yixiang.yshop.module.express.kdniao.model.vo.KdniaoApiVO; import co.yixiang.yshop.module.express.kdniao.util.KdniaoUtil; import co.yixiang.yshop.module.express.service.express.ExpressService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import java.io.IOException; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 快递公司") @RestController @RequestMapping("/order/express") @Validated public class ExpressController { @Resource private ExpressService expressService; @Resource private ExpressRedisDAO expressRedisDAO; @PostMapping("/create") @Operation(summary = "创建快递公司") @PreAuthorize("@ss.hasPermission('order:express:create')") public CommonResult createExpress(@Valid @RequestBody ExpressCreateReqVO createReqVO) { return success(expressService.createExpress(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新快递公司") @PreAuthorize("@ss.hasPermission('order:express:update')") public CommonResult updateExpress(@Valid @RequestBody ExpressUpdateReqVO updateReqVO) { expressService.updateExpress(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除快递公司") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('order:express:delete')") public CommonResult deleteExpress(@RequestParam("id") Integer id) { expressService.deleteExpress(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得快递公司") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('order:express:query')") public CommonResult getExpress(@RequestParam("id") Integer id) { ExpressDO express = expressService.getExpress(id); return success(ExpressConvert.INSTANCE.convert(express)); } @GetMapping("/list") @Operation(summary = "获得快递公司列表") @PreAuthorize("@ss.hasPermission('order:express:query')") public CommonResult> getExpressList() { List list = expressService.getExpressList(); return success(ExpressConvert.INSTANCE.convertList(list)); } @GetMapping("/page") @Operation(summary = "获得快递公司分页") @PreAuthorize("@ss.hasPermission('order:express:query')") public CommonResult> getExpressPage(@Valid ExpressPageReqVO pageVO) { PageResult pageResult = expressService.getExpressPage(pageVO); return success(ExpressConvert.INSTANCE.convertPage(pageResult)); } @GetMapping("/export-excel") @Operation(summary = "导出快递公司 Excel") @PreAuthorize("@ss.hasPermission('order:express:export')") public void exportExpressExcel(@Valid ExpressExportReqVO exportReqVO, HttpServletResponse response) throws IOException { List list = expressService.getExpressList(exportReqVO); // 导出 Excel List datas = ExpressConvert.INSTANCE.convertList02(list); ExcelUtils.write(response, "快递公司.xls", "数据", ExpressExcelVO.class, datas); } @GetMapping("/set") @Operation(summary = "获得快递鸟配置") public CommonResult getExpressSet() { return success(expressRedisDAO.get()); } @PostMapping("/set") @Operation(summary = "快递鸟配置") public CommonResult postExpressSet(@RequestBody KdniaoApiBaseDTO kdniaoApiBaseDTO) { expressRedisDAO.set(kdniaoApiBaseDTO); return success(true); } @GetMapping("/getLogistic") @Parameters({ @Parameter(name = "shipperCode", description = "快递公司编码", required = true), @Parameter(name = "logisticCode", description = "快递单号", required = true), }) @Operation(summary = "查询物流") public CommonResult getLogistic(@RequestParam(value = "shipperCode") String shipperCode, @RequestParam("logisticCode") String logisticCode) { KdniaoApiBaseDTO kdniaoApiBaseDTO = expressRedisDAO.get(); KdniaoApiDTO params = new KdniaoApiDTO(); params.setLogisticCode(logisticCode); params.setShipperCode(shipperCode); params.setApiKey(kdniaoApiBaseDTO.getApiKey()); params.setEBusinessID(kdniaoApiBaseDTO.getEBusinessID()); params.setIsFree(kdniaoApiBaseDTO.getIsFree()); //此处注意 这个地方分收费与免费当 目前用当免费免费当只支持圆通 申通 百世 如果是收费当 改里面RequestType参数 函数里有说明 return success(KdniaoUtil.getLogisticInfo(params)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-biz/src/main/java/co/yixiang/yshop/module/express/controller/admin/express/vo/ExpressBaseVO.java ================================================ package co.yixiang.yshop.module.express.controller.admin.express.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotNull; /** * 快递公司 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class ExpressBaseVO { @Schema(description = "快递公司简称", required = true) @NotNull(message = "快递公司简称不能为空") private String code; @Schema(description = "快递公司全称", required = true, example = "yshop") @NotNull(message = "快递公司全称不能为空") private String name; @Schema(description = "排序", required = true) @NotNull(message = "排序不能为空") private Integer sort; } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-biz/src/main/java/co/yixiang/yshop/module/express/controller/admin/express/vo/ExpressCreateReqVO.java ================================================ package co.yixiang.yshop.module.express.controller.admin.express.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @Schema(description = "管理后台 - 快递公司创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ExpressCreateReqVO extends ExpressBaseVO { } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-biz/src/main/java/co/yixiang/yshop/module/express/controller/admin/express/vo/ExpressExcelVO.java ================================================ package co.yixiang.yshop.module.express.controller.admin.express.vo; import com.alibaba.excel.annotation.ExcelProperty; import lombok.Data; import java.time.LocalDateTime; /** * 快递公司 Excel VO * * @author yshop */ @Data public class ExpressExcelVO { @ExcelProperty("快递公司id") private Integer id; @ExcelProperty("快递公司简称") private String code; @ExcelProperty("快递公司全称") private String name; @ExcelProperty("排序") private Integer sort; @ExcelProperty("添加时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-biz/src/main/java/co/yixiang/yshop/module/express/controller/admin/express/vo/ExpressExportReqVO.java ================================================ package co.yixiang.yshop.module.express.controller.admin.express.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "管理后台 - 快递公司 Excel 导出 Request VO,参数和 ExpressPageReqVO 是一致的") @Data public class ExpressExportReqVO { @Schema(description = "快递公司简称") private String code; @Schema(description = "快递公司全称", example = "yshop") private String name; } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-biz/src/main/java/co/yixiang/yshop/module/express/controller/admin/express/vo/ExpressPageReqVO.java ================================================ package co.yixiang.yshop.module.express.controller.admin.express.vo; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @Schema(description = "管理后台 - 快递公司分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ExpressPageReqVO extends PageParam { @Schema(description = "快递公司简称") private String code; @Schema(description = "快递公司全称", example = "yshop") private String name; } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-biz/src/main/java/co/yixiang/yshop/module/express/controller/admin/express/vo/ExpressRespVO.java ================================================ package co.yixiang.yshop.module.express.controller.admin.express.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import java.time.LocalDateTime; @Schema(description = "管理后台 - 快递公司 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ExpressRespVO extends ExpressBaseVO { @Schema(description = "快递公司id", required = true, example = "27172") private Integer id; @Schema(description = "添加时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-biz/src/main/java/co/yixiang/yshop/module/express/controller/admin/express/vo/ExpressUpdateReqVO.java ================================================ package co.yixiang.yshop.module.express.controller.admin.express.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 快递公司更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ExpressUpdateReqVO extends ExpressBaseVO { @Schema(description = "快递公司id", required = true, example = "27172") @NotNull(message = "快递公司id不能为空") private Integer id; } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-biz/src/main/java/co/yixiang/yshop/module/express/controller/app/express/AppExpressController.java ================================================ package co.yixiang.yshop.module.express.controller.app.express; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.module.express.dal.redis.express.ExpressRedisDAO; import co.yixiang.yshop.module.express.kdniao.model.dto.KdniaoApiBaseDTO; import co.yixiang.yshop.module.express.kdniao.model.dto.KdniaoApiDTO; import co.yixiang.yshop.module.express.kdniao.model.vo.KdniaoApiVO; import co.yixiang.yshop.module.express.kdniao.util.KdniaoUtil; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import jakarta.annotation.Resource; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "app - 查询快递") @RestController @RequestMapping("/express") @Validated public class AppExpressController { @Resource private ExpressRedisDAO expressRedisDAO; @GetMapping("/getLogistic") @Parameters({ @Parameter(name = "shipperCode", description = "快递公司编码", required = true), @Parameter(name = "logisticCode", description = "快递单号", required = true), }) @Operation(summary = "查询物流") public CommonResult getLogistic(@RequestParam(value = "shipperCode") String shipperCode, @RequestParam("logisticCode") String logisticCode) { KdniaoApiBaseDTO kdniaoApiBaseDTO = expressRedisDAO.get(); KdniaoApiDTO params = new KdniaoApiDTO(); params.setLogisticCode(logisticCode); params.setShipperCode(shipperCode); params.setApiKey(kdniaoApiBaseDTO.getApiKey()); params.setEBusinessID(kdniaoApiBaseDTO.getEBusinessID()); params.setIsFree(kdniaoApiBaseDTO.getIsFree()); //此处注意 这个地方分收费与免费当 目前用当免费免费当只支持圆通 申通 百世 如果是收费当 改里面RequestType参数 函数里有说明 return success(KdniaoUtil.getLogisticInfo(params)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-biz/src/main/java/co/yixiang/yshop/module/express/convert/express/ExpressConvert.java ================================================ package co.yixiang.yshop.module.express.convert.express; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.express.controller.admin.express.vo.ExpressCreateReqVO; import co.yixiang.yshop.module.express.controller.admin.express.vo.ExpressExcelVO; import co.yixiang.yshop.module.express.controller.admin.express.vo.ExpressRespVO; import co.yixiang.yshop.module.express.controller.admin.express.vo.ExpressUpdateReqVO; import co.yixiang.yshop.module.express.dal.dataobject.express.ExpressDO; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import java.util.List; /** * 快递公司 Convert * * @author yshop */ @Mapper public interface ExpressConvert { ExpressConvert INSTANCE = Mappers.getMapper(ExpressConvert.class); ExpressDO convert(ExpressCreateReqVO bean); ExpressDO convert(ExpressUpdateReqVO bean); ExpressRespVO convert(ExpressDO bean); List convertList(List list); PageResult convertPage(PageResult page); List convertList02(List list); } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-biz/src/main/java/co/yixiang/yshop/module/express/dal/dataobject/express/ExpressDO.java ================================================ package co.yixiang.yshop.module.express.dal.dataobject.express; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; /** * 快递公司 DO * * @author yshop */ @TableName("yshop_express") @KeySequence("yshop_express_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class ExpressDO extends BaseDO { /** * 快递公司id */ @TableId private Integer id; /** * 快递公司简称 */ private String code; /** * 快递公司全称 */ private String name; /** * 排序 */ private Integer sort; } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-biz/src/main/java/co/yixiang/yshop/module/express/dal/mysql/express/ExpressMapper.java ================================================ package co.yixiang.yshop.module.express.dal.mysql.express; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.express.controller.admin.express.vo.ExpressExportReqVO; import co.yixiang.yshop.module.express.controller.admin.express.vo.ExpressPageReqVO; import co.yixiang.yshop.module.express.dal.dataobject.express.ExpressDO; import org.apache.ibatis.annotations.Mapper; import java.util.List; /** * 快递公司 Mapper * * @author yshop */ @Mapper public interface ExpressMapper extends BaseMapperX { default PageResult selectPage(ExpressPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(ExpressDO::getCode, reqVO.getCode()) .likeIfPresent(ExpressDO::getName, reqVO.getName()) .orderByDesc(ExpressDO::getId)); } default List selectList(ExpressExportReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .eqIfPresent(ExpressDO::getCode, reqVO.getCode()) .likeIfPresent(ExpressDO::getName, reqVO.getName()) .orderByDesc(ExpressDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-biz/src/main/java/co/yixiang/yshop/module/express/dal/redis/RedisKeyConstants.java ================================================ package co.yixiang.yshop.module.express.dal.redis; import co.yixiang.yshop.framework.redis.core.RedisKeyDefine; import co.yixiang.yshop.module.express.kdniao.model.dto.KdniaoApiBaseDTO; import static co.yixiang.yshop.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING; /** * System Redis Key 枚举类 * * @author yshop */ public interface RedisKeyConstants { RedisKeyDefine YSHOP_EXPRESS_CACHE_KEY = new RedisKeyDefine("快递鸟配置", "yshop_express_cache:", // STRING, KdniaoApiBaseDTO.class, RedisKeyDefine.TimeoutTypeEnum.FOREVER); } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-biz/src/main/java/co/yixiang/yshop/module/express/dal/redis/express/ExpressRedisDAO.java ================================================ package co.yixiang.yshop.module.express.dal.redis.express; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import co.yixiang.yshop.module.express.kdniao.model.dto.KdniaoApiBaseDTO; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Repository; import jakarta.annotation.Resource; import static co.yixiang.yshop.module.express.dal.redis.RedisKeyConstants.YSHOP_EXPRESS_CACHE_KEY; /** * {@link KdniaoApiBaseDTO} 的 RedisDAO * * @author yshop */ @Repository public class ExpressRedisDAO { @Resource private StringRedisTemplate stringRedisTemplate; public KdniaoApiBaseDTO get() { String redisKey = formatKey(); return JsonUtils.parseObject(stringRedisTemplate.opsForValue().get(redisKey), KdniaoApiBaseDTO.class); } public void set(KdniaoApiBaseDTO apiBaseDTO) { String redisKey = formatKey(); stringRedisTemplate.opsForValue().set(redisKey, JsonUtils.toJsonString(apiBaseDTO)); } public void delete() { String redisKey = formatKey(); stringRedisTemplate.delete(redisKey); } private static String formatKey() { return String.format(YSHOP_EXPRESS_CACHE_KEY.getKeyTemplate()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-biz/src/main/java/co/yixiang/yshop/module/express/service/express/ExpressService.java ================================================ package co.yixiang.yshop.module.express.service.express; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.express.controller.admin.express.vo.ExpressCreateReqVO; import co.yixiang.yshop.module.express.controller.admin.express.vo.ExpressExportReqVO; import co.yixiang.yshop.module.express.controller.admin.express.vo.ExpressPageReqVO; import co.yixiang.yshop.module.express.controller.admin.express.vo.ExpressUpdateReqVO; import co.yixiang.yshop.module.express.dal.dataobject.express.ExpressDO; import jakarta.validation.Valid; import java.util.List; /** * 快递公司 Service 接口 * * @author yshop */ public interface ExpressService { /** * 创建快递公司 * * @param createReqVO 创建信息 * @return 编号 */ Integer createExpress(@Valid ExpressCreateReqVO createReqVO); /** * 更新快递公司 * * @param updateReqVO 更新信息 */ void updateExpress(@Valid ExpressUpdateReqVO updateReqVO); /** * 删除快递公司 * * @param id 编号 */ void deleteExpress(Integer id); /** * 获得快递公司 * * @param id 编号 * @return 快递公司 */ ExpressDO getExpress(Integer id); /** * 获得快递公司列表 * * @return 快递公司列表 */ List getExpressList(); /** * 获得快递公司分页 * * @param pageReqVO 分页查询 * @return 快递公司分页 */ PageResult getExpressPage(ExpressPageReqVO pageReqVO); /** * 获得快递公司列表, 用于 Excel 导出 * * @param exportReqVO 查询条件 * @return 快递公司列表 */ List getExpressList(ExpressExportReqVO exportReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-express/yshop-module-express-biz/src/main/java/co/yixiang/yshop/module/express/service/express/ExpressServiceImpl.java ================================================ package co.yixiang.yshop.module.express.service.express; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.express.controller.admin.express.vo.ExpressCreateReqVO; import co.yixiang.yshop.module.express.controller.admin.express.vo.ExpressExportReqVO; import co.yixiang.yshop.module.express.controller.admin.express.vo.ExpressPageReqVO; import co.yixiang.yshop.module.express.controller.admin.express.vo.ExpressUpdateReqVO; import co.yixiang.yshop.module.express.convert.express.ExpressConvert; import co.yixiang.yshop.module.express.dal.dataobject.express.ExpressDO; import co.yixiang.yshop.module.express.dal.mysql.express.ExpressMapper; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.express.enums.ErrorCodeConstants.EXPRESS_NOT_EXISTS; /** * 快递公司 Service 实现类 * * @author yshop */ @Service @Validated public class ExpressServiceImpl implements ExpressService { @Resource private ExpressMapper expressMapper; @Override public Integer createExpress(ExpressCreateReqVO createReqVO) { // 插入 ExpressDO express = ExpressConvert.INSTANCE.convert(createReqVO); expressMapper.insert(express); // 返回 return express.getId(); } @Override public void updateExpress(ExpressUpdateReqVO updateReqVO) { // 校验存在 validateExpressExists(updateReqVO.getId()); // 更新 ExpressDO updateObj = ExpressConvert.INSTANCE.convert(updateReqVO); expressMapper.updateById(updateObj); } @Override public void deleteExpress(Integer id) { // 校验存在 validateExpressExists(id); // 删除 expressMapper.deleteById(id); } private void validateExpressExists(Integer id) { if (expressMapper.selectById(id) == null) { throw exception(EXPRESS_NOT_EXISTS); } } @Override public ExpressDO getExpress(Integer id) { return expressMapper.selectById(id); } @Override public List getExpressList() { return expressMapper.selectList(); } @Override public PageResult getExpressPage(ExpressPageReqVO pageReqVO) { return expressMapper.selectPage(pageReqVO); } @Override public List getExpressList(ExpressExportReqVO exportReqVO) { return expressMapper.selectList(exportReqVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/pom.xml ================================================ co.yixiang.boot yshop ${revision} 4.0.0 yshop-module-infra-api yshop-module-infra-biz yshop-module-infra pom ${project.artifactId} infra 模块,主要提供两块能力: 1. 我们放基础设施的运维与管理,支撑上层的通用与核心业务。 例如说:定时任务的管理、服务器的信息等等 2. 研发工具,提升研发效率与质量。 例如说:代码生成器、接口文档等等 ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-api/pom.xml ================================================ co.yixiang.boot yshop-module-infra ${revision} 4.0.0 yshop-module-infra-api jar ${project.artifactId} infra 模块 API,暴露给其它模块调用 co.yixiang.boot yshop-common org.springframework.boot spring-boot-starter-validation true ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-api/src/main/java/co/yixiang/yshop/module/infra/api/file/FileApi.java ================================================ package co.yixiang.yshop.module.infra.api.file; /** * 文件 API 接口 * * @author yshop */ public interface FileApi { /** * 保存文件,并返回文件的访问路径 * * @param content 文件内容 * @return 文件路径 */ default String createFile(byte[] content) { return createFile(null, null, content); } /** * 保存文件,并返回文件的访问路径 * * @param path 文件路径 * @param content 文件内容 * @return 文件路径 */ default String createFile(String path, byte[] content) { return createFile(null, path, content); } /** * 保存文件,并返回文件的访问路径 * * @param name 文件名称 * @param path 文件路径 * @param content 文件内容 * @return 文件路径 */ String createFile(String name, String path, byte[] content); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-api/src/main/java/co/yixiang/yshop/module/infra/api/logger/ApiAccessLogApi.java ================================================ package co.yixiang.yshop.module.infra.api.logger; import co.yixiang.yshop.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; import jakarta.validation.Valid; /** * API 访问日志的 API 接口 * * @author yshop */ public interface ApiAccessLogApi { /** * 创建 API 访问日志 * * @param createDTO 创建信息 */ void createApiAccessLog(@Valid ApiAccessLogCreateReqDTO createDTO); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-api/src/main/java/co/yixiang/yshop/module/infra/api/logger/ApiErrorLogApi.java ================================================ package co.yixiang.yshop.module.infra.api.logger; import co.yixiang.yshop.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; import jakarta.validation.Valid; /** * API 错误日志的 API 接口 * * @author yshop */ public interface ApiErrorLogApi { /** * 创建 API 错误日志 * * @param createDTO 创建信息 */ void createApiErrorLog(@Valid ApiErrorLogCreateReqDTO createDTO); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-api/src/main/java/co/yixiang/yshop/module/infra/api/logger/dto/ApiAccessLogCreateReqDTO.java ================================================ package co.yixiang.yshop.module.infra.api.logger.dto; import lombok.Data; import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; /** * API 访问日志 * * @author yshop */ @Data public class ApiAccessLogCreateReqDTO { /** * 链路追踪编号 */ private String traceId; /** * 用户编号 */ private Long userId; /** * 用户类型 */ private Integer userType; /** * 应用名 */ @NotNull(message = "应用名不能为空") private String applicationName; /** * 请求方法名 */ @NotNull(message = "http 请求方法不能为空") private String requestMethod; /** * 访问地址 */ @NotNull(message = "访问地址不能为空") private String requestUrl; /** * 请求参数 */ private String requestParams; /** * 响应结果 */ private String responseBody; /** * 用户 IP */ @NotNull(message = "ip 不能为空") private String userIp; /** * 浏览器 UA */ @NotNull(message = "User-Agent 不能为空") private String userAgent; /** * 操作模块 */ private String operateModule; /** * 操作名 */ private String operateName; /** * 操作分类 * * 枚举,参见 OperateTypeEnum 类 */ private Integer operateType; /** * 开始请求时间 */ @NotNull(message = "开始请求时间不能为空") private LocalDateTime beginTime; /** * 结束请求时间 */ @NotNull(message = "结束请求时间不能为空") private LocalDateTime endTime; /** * 执行时长,单位:毫秒 */ @NotNull(message = "执行时长不能为空") private Integer duration; /** * 结果码 */ @NotNull(message = "错误码不能为空") private Integer resultCode; /** * 结果提示 */ private String resultMsg; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-api/src/main/java/co/yixiang/yshop/module/infra/api/logger/dto/ApiErrorLogCreateReqDTO.java ================================================ package co.yixiang.yshop.module.infra.api.logger.dto; import lombok.Data; import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; /** * API 错误日志 * * @author yshop */ @Data public class ApiErrorLogCreateReqDTO { /** * 链路编号 */ private String traceId; /** * 账号编号 */ private Long userId; /** * 用户类型 */ private Integer userType; /** * 应用名 */ @NotNull(message = "应用名不能为空") private String applicationName; /** * 请求方法名 */ @NotNull(message = "http 请求方法不能为空") private String requestMethod; /** * 访问地址 */ @NotNull(message = "访问地址不能为空") private String requestUrl; /** * 请求参数 */ @NotNull(message = "请求参数不能为空") private String requestParams; /** * 用户 IP */ @NotNull(message = "ip 不能为空") private String userIp; /** * 浏览器 UA */ @NotNull(message = "User-Agent 不能为空") private String userAgent; /** * 异常时间 */ @NotNull(message = "异常时间不能为空") private LocalDateTime exceptionTime; /** * 异常名 */ @NotNull(message = "异常名不能为空") private String exceptionName; /** * 异常发生的类全名 */ @NotNull(message = "异常发生的类全名不能为空") private String exceptionClassName; /** * 异常发生的类文件 */ @NotNull(message = "异常发生的类文件不能为空") private String exceptionFileName; /** * 异常发生的方法名 */ @NotNull(message = "异常发生的方法名不能为空") private String exceptionMethodName; /** * 异常发生的方法所在行 */ @NotNull(message = "异常发生的方法所在行不能为空") private Integer exceptionLineNumber; /** * 异常的栈轨迹异常的栈轨迹 */ @NotNull(message = "异常的栈轨迹不能为空") private String exceptionStackTrace; /** * 异常导致的根消息 */ @NotNull(message = "异常导致的根消息不能为空") private String exceptionRootCauseMessage; /** * 异常导致的消息 */ @NotNull(message = "异常导致的消息不能为空") private String exceptionMessage; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-api/src/main/java/co/yixiang/yshop/module/infra/api/websocket/WebSocketSenderApi.java ================================================ package co.yixiang.yshop.module.infra.api.websocket; import co.yixiang.yshop.framework.common.util.json.JsonUtils; /** * WebSocket 发送器的 API 接口 * * 对 WebSocketMessageSender 进行封装,提供给其它模块使用 * * @author yshop */ public interface WebSocketSenderApi { /** * 发送消息给指定用户 * * @param userType 用户类型 * @param userId 用户编号 * @param messageType 消息类型 * @param messageContent 消息内容,JSON 格式 */ void send(Integer userType, Long userId, String messageType, String messageContent); /** * 发送消息给指定用户类型 * * @param userType 用户类型 * @param messageType 消息类型 * @param messageContent 消息内容,JSON 格式 */ void send(Integer userType, String messageType, String messageContent); /** * 发送消息给指定 Session * * @param sessionId Session 编号 * @param messageType 消息类型 * @param messageContent 消息内容,JSON 格式 */ void send(String sessionId, String messageType, String messageContent); default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) { send(userType, userId, messageType, JsonUtils.toJsonString(messageContent)); } default void sendObject(Integer userType, String messageType, Object messageContent) { send(userType, messageType, JsonUtils.toJsonString(messageContent)); } default void sendObject(String sessionId, String messageType, Object messageContent) { send(sessionId, messageType, JsonUtils.toJsonString(messageContent)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-api/src/main/java/co/yixiang/yshop/module/infra/enums/DictTypeConstants.java ================================================ package co.yixiang.yshop.module.infra.enums; /** * Infra 字典类型的枚举类 * * @author yshop */ public interface DictTypeConstants { String JOB_STATUS = "infra_job_status"; // 定时任务状态的枚举 String JOB_LOG_STATUS = "infra_job_log_status"; // 定时任务日志状态的枚举 String API_ERROR_LOG_PROCESS_STATUS = "infra_api_error_log_process_status"; // API 错误日志的处理状态的枚举 String CONFIG_TYPE = "infra_config_type"; // 参数配置类型 String BOOLEAN_STRING = "infra_boolean_string"; // Boolean 是否类型 String OPERATE_TYPE = "infra_operate_type"; // 操作类型 } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-api/src/main/java/co/yixiang/yshop/module/infra/enums/ErrorCodeConstants.java ================================================ package co.yixiang.yshop.module.infra.enums; import co.yixiang.yshop.framework.common.exception.ErrorCode; /** * Infra 错误码枚举类 * * infra 系统,使用 1-001-000-000 段 */ public interface ErrorCodeConstants { // ========== 参数配置 1-001-000-000 ========== ErrorCode CONFIG_NOT_EXISTS = new ErrorCode(1_001_000_001, "参数配置不存在"); ErrorCode CONFIG_KEY_DUPLICATE = new ErrorCode(1_001_000_002, "参数配置 key 重复"); ErrorCode CONFIG_CAN_NOT_DELETE_SYSTEM_TYPE = new ErrorCode(1_001_000_003, "不能删除类型为系统内置的参数配置"); ErrorCode CONFIG_GET_VALUE_ERROR_IF_VISIBLE = new ErrorCode(1_001_000_004, "获取参数配置失败,原因:不允许获取不可见配置"); // ========== 定时任务 1-001-001-000 ========== ErrorCode JOB_NOT_EXISTS = new ErrorCode(1_001_001_000, "定时任务不存在"); ErrorCode JOB_HANDLER_EXISTS = new ErrorCode(1_001_001_001, "定时任务的处理器已经存在"); ErrorCode JOB_CHANGE_STATUS_INVALID = new ErrorCode(1_001_001_002, "只允许修改为开启或者关闭状态"); ErrorCode JOB_CHANGE_STATUS_EQUALS = new ErrorCode(1_001_001_003, "定时任务已经处于该状态,无需修改"); ErrorCode JOB_UPDATE_ONLY_NORMAL_STATUS = new ErrorCode(1_001_001_004, "只有开启状态的任务,才可以修改"); ErrorCode JOB_CRON_EXPRESSION_VALID = new ErrorCode(1_001_001_005, "CRON 表达式不正确"); ErrorCode JOB_HANDLER_BEAN_NOT_EXISTS = new ErrorCode(1_001_001_006, "定时任务的处理器 Bean 不存在"); ErrorCode JOB_HANDLER_BEAN_TYPE_ERROR = new ErrorCode(1_001_001_007, "定时任务的处理器 Bean 类型不正确,未实现 JobHandler 接口"); // ========== API 错误日志 1-001-002-000 ========== ErrorCode API_ERROR_LOG_NOT_FOUND = new ErrorCode(1_001_002_000, "API 错误日志不存在"); ErrorCode API_ERROR_LOG_PROCESSED = new ErrorCode(1_001_002_001, "API 错误日志已处理"); // ========= 文件相关 1-001-003-000 ================= ErrorCode FILE_PATH_EXISTS = new ErrorCode(1_001_003_000, "文件路径已存在"); ErrorCode FILE_NOT_EXISTS = new ErrorCode(1_001_003_001, "文件不存在"); ErrorCode FILE_IS_EMPTY = new ErrorCode(1_001_003_002, "文件为空"); // ========== 代码生成器 1-001-004-000 ========== ErrorCode CODEGEN_TABLE_EXISTS = new ErrorCode(1_001_004_002, "表定义已经存在"); ErrorCode CODEGEN_IMPORT_TABLE_NULL = new ErrorCode(1_001_004_001, "导入的表不存在"); ErrorCode CODEGEN_IMPORT_COLUMNS_NULL = new ErrorCode(1_001_004_002, "导入的字段不存在"); ErrorCode CODEGEN_TABLE_NOT_EXISTS = new ErrorCode(1_001_004_004, "表定义不存在"); ErrorCode CODEGEN_COLUMN_NOT_EXISTS = new ErrorCode(1_001_004_005, "字段义不存在"); ErrorCode CODEGEN_SYNC_COLUMNS_NULL = new ErrorCode(1_001_004_006, "同步的字段不存在"); ErrorCode CODEGEN_SYNC_NONE_CHANGE = new ErrorCode(1_001_004_007, "同步失败,不存在改变"); ErrorCode CODEGEN_TABLE_INFO_TABLE_COMMENT_IS_NULL = new ErrorCode(1_001_004_008, "数据库的表注释未填写"); ErrorCode CODEGEN_TABLE_INFO_COLUMN_COMMENT_IS_NULL = new ErrorCode(1_001_004_009, "数据库的表字段({})注释未填写"); ErrorCode CODEGEN_MASTER_TABLE_NOT_EXISTS = new ErrorCode(1_001_004_010, "主表(id={})定义不存在,请检查"); ErrorCode CODEGEN_SUB_COLUMN_NOT_EXISTS = new ErrorCode(1_001_004_011, "子表的字段(id={})不存在,请检查"); ErrorCode CODEGEN_MASTER_GENERATION_FAIL_NO_SUB_TABLE = new ErrorCode(1_001_004_012, "主表生成代码失败,原因:它没有子表"); // ========== 文件配置 1-001-006-000 ========== ErrorCode FILE_CONFIG_NOT_EXISTS = new ErrorCode(1_001_006_000, "文件配置不存在"); ErrorCode FILE_CONFIG_DELETE_FAIL_MASTER = new ErrorCode(1_001_006_001, "该文件配置不允许删除,原因:它是主配置,删除会导致无法上传文件"); // ========== 数据源配置 1-001-007-000 ========== ErrorCode DATA_SOURCE_CONFIG_NOT_EXISTS = new ErrorCode(1_001_007_000, "数据源配置不存在"); ErrorCode DATA_SOURCE_CONFIG_NOT_OK = new ErrorCode(1_001_007_001, "数据源配置不正确,无法进行连接"); // ========== 学生 1-001-201-000 ========== ErrorCode DEMO01_CONTACT_NOT_EXISTS = new ErrorCode(1_001_201_000, "示例联系人不存在"); ErrorCode DEMO02_CATEGORY_NOT_EXISTS = new ErrorCode(1_001_201_001, "示例分类不存在"); ErrorCode DEMO02_CATEGORY_EXITS_CHILDREN = new ErrorCode(1_001_201_002, "存在存在子示例分类,无法删除"); ErrorCode DEMO02_CATEGORY_PARENT_NOT_EXITS = new ErrorCode(1_001_201_003,"父级示例分类不存在"); ErrorCode DEMO02_CATEGORY_PARENT_ERROR = new ErrorCode(1_001_201_004, "不能设置自己为父示例分类"); ErrorCode DEMO02_CATEGORY_NAME_DUPLICATE = new ErrorCode(1_001_201_005, "已经存在该名字的示例分类"); ErrorCode DEMO02_CATEGORY_PARENT_IS_CHILD = new ErrorCode(1_001_201_006, "不能设置自己的子示例分类为父示例分类"); ErrorCode DEMO03_STUDENT_NOT_EXISTS = new ErrorCode(1_001_201_007, "学生不存在"); ErrorCode DEMO03_GRADE_NOT_EXISTS = new ErrorCode(1_001_201_008, "学生班级不存在"); ErrorCode DEMO03_GRADE_EXISTS = new ErrorCode(1_001_201_009, "学生班级已存在"); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/pom.xml ================================================ co.yixiang.boot yshop-module-infra ${revision} 4.0.0 yshop-module-infra-biz jar ${project.artifactId} infra 模块,主要提供两块能力: 1. 我们放基础设施的运维与管理,支撑上层的通用与核心业务。 例如说:定时任务的管理、服务器的信息等等 2. 研发工具,提升研发效率与质量。 例如说:代码生成器、接口文档等等 co.yixiang.boot yshop-module-system-api ${revision} co.yixiang.boot yshop-module-infra-api ${revision} co.yixiang.boot yshop-spring-boot-starter-biz-tenant co.yixiang.boot yshop-spring-boot-starter-security co.yixiang.boot yshop-spring-boot-starter-websocket co.yixiang.boot yshop-spring-boot-starter-mybatis com.baomidou mybatis-plus-generator co.yixiang.boot yshop-spring-boot-starter-redis co.yixiang.boot yshop-spring-boot-starter-job co.yixiang.boot yshop-spring-boot-starter-mq co.yixiang.boot yshop-spring-boot-starter-test test co.yixiang.boot yshop-spring-boot-starter-excel org.apache.velocity velocity-engine-core co.yixiang.boot yshop-spring-boot-starter-monitor de.codecentric spring-boot-admin-starter-server commons-net commons-net com.jcraft jsch io.minio minio org.apache.tika tika-core ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/api/file/FileApiImpl.java ================================================ package co.yixiang.yshop.module.infra.api.file; import co.yixiang.yshop.module.infra.service.file.FileService; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; /** * 文件 API 实现类 * * @author yshop */ @Service @Validated public class FileApiImpl implements FileApi { @Resource private FileService fileService; @Override public String createFile(String name, String path, byte[] content) { return fileService.createFile(name, path, content); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/api/logger/ApiAccessLogApiImpl.java ================================================ package co.yixiang.yshop.module.infra.api.logger; import co.yixiang.yshop.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; import co.yixiang.yshop.module.infra.service.logger.ApiAccessLogService; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; /** * API 访问日志的 API 实现类 * * @author yshop */ @Service @Validated public class ApiAccessLogApiImpl implements ApiAccessLogApi { @Resource private ApiAccessLogService apiAccessLogService; @Override public void createApiAccessLog(ApiAccessLogCreateReqDTO createDTO) { apiAccessLogService.createApiAccessLog(createDTO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/api/logger/ApiErrorLogApiImpl.java ================================================ package co.yixiang.yshop.module.infra.api.logger; import co.yixiang.yshop.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; import co.yixiang.yshop.module.infra.service.logger.ApiErrorLogService; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; /** * API 访问日志的 API 接口 * * @author yshop */ @Service @Validated public class ApiErrorLogApiImpl implements ApiErrorLogApi { @Resource private ApiErrorLogService apiErrorLogService; @Override public void createApiErrorLog(ApiErrorLogCreateReqDTO createDTO) { apiErrorLogService.createApiErrorLog(createDTO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/api/websocket/WebSocketSenderApiImpl.java ================================================ package co.yixiang.yshop.module.infra.api.websocket; import co.yixiang.yshop.framework.websocket.core.sender.WebSocketMessageSender; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; /** * WebSocket 发送器的 API 实现类 * * @author yshop */ @Component public class WebSocketSenderApiImpl implements WebSocketSenderApi { @Resource private WebSocketMessageSender webSocketMessageSender; @Override public void send(Integer userType, Long userId, String messageType, String messageContent) { webSocketMessageSender.send(userType, userId, messageType, messageContent); } @Override public void send(Integer userType, String messageType, String messageContent) { webSocketMessageSender.send(userType, messageType, messageContent); } @Override public void send(String sessionId, String messageType, String messageContent) { webSocketMessageSender.send(sessionId, messageType, messageContent); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/codegen/CodegenController.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.codegen; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.ZipUtil; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.CodegenDetailRespVO; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.CodegenPreviewRespVO; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.table.CodegenTableRespVO; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO; import co.yixiang.yshop.module.infra.convert.codegen.CodegenConvert; import co.yixiang.yshop.module.infra.dal.dataobject.codegen.CodegenColumnDO; import co.yixiang.yshop.module.infra.dal.dataobject.codegen.CodegenTableDO; import co.yixiang.yshop.module.infra.service.codegen.CodegenService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; import java.util.Map; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; import static co.yixiang.yshop.module.infra.framework.file.core.utils.FileTypeUtils.writeAttachment; @Tag(name = "管理后台 - 代码生成器") @RestController @RequestMapping("/infra/codegen") @Validated public class CodegenController { @Resource private CodegenService codegenService; @GetMapping("/db/table/list") @Operation(summary = "获得数据库自带的表定义列表", description = "会过滤掉已经导入 Codegen 的表") @Parameters({ @Parameter(name = "dataSourceConfigId", description = "数据源配置的编号", required = true, example = "1"), @Parameter(name = "name", description = "表名,模糊匹配", example = "yshop"), @Parameter(name = "comment", description = "描述,模糊匹配", example = "yshop") }) @PreAuthorize("@ss.hasPermission('infra:codegen:query')") public CommonResult> getDatabaseTableList( @RequestParam(value = "dataSourceConfigId") Long dataSourceConfigId, @RequestParam(value = "name", required = false) String name, @RequestParam(value = "comment", required = false) String comment) { return success(codegenService.getDatabaseTableList(dataSourceConfigId, name, comment)); } @GetMapping("/table/list") @Operation(summary = "获得表定义列表") @Parameter(name = "dataSourceConfigId", description = "数据源配置的编号", required = true, example = "1") @PreAuthorize("@ss.hasPermission('infra:codegen:query')") public CommonResult> getCodegenTableList(@RequestParam(value = "dataSourceConfigId") Long dataSourceConfigId) { List list = codegenService.getCodegenTableList(dataSourceConfigId); return success(BeanUtils.toBean(list, CodegenTableRespVO.class)); } @GetMapping("/table/page") @Operation(summary = "获得表定义分页") @PreAuthorize("@ss.hasPermission('infra:codegen:query')") public CommonResult> getCodegenTablePage(@Valid CodegenTablePageReqVO pageReqVO) { PageResult pageResult = codegenService.getCodegenTablePage(pageReqVO); return success(BeanUtils.toBean(pageResult, CodegenTableRespVO.class)); } @GetMapping("/detail") @Operation(summary = "获得表和字段的明细") @Parameter(name = "tableId", description = "表编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('infra:codegen:query')") public CommonResult getCodegenDetail(@RequestParam("tableId") Long tableId) { CodegenTableDO table = codegenService.getCodegenTable(tableId); List columns = codegenService.getCodegenColumnListByTableId(tableId); // 拼装返回 return success(CodegenConvert.INSTANCE.convert(table, columns)); } @Operation(summary = "基于数据库的表结构,创建代码生成器的表和字段定义") @PostMapping("/create-list") @PreAuthorize("@ss.hasPermission('infra:codegen:create')") public CommonResult> createCodegenList(@Valid @RequestBody CodegenCreateListReqVO reqVO) { return success(codegenService.createCodegenList(getLoginUserId(), reqVO)); } @Operation(summary = "更新数据库的表和字段定义") @PutMapping("/update") @PreAuthorize("@ss.hasPermission('infra:codegen:update')") public CommonResult updateCodegen(@Valid @RequestBody CodegenUpdateReqVO updateReqVO) { codegenService.updateCodegen(updateReqVO); return success(true); } @Operation(summary = "基于数据库的表结构,同步数据库的表和字段定义") @PutMapping("/sync-from-db") @Parameter(name = "tableId", description = "表编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('infra:codegen:update')") public CommonResult syncCodegenFromDB(@RequestParam("tableId") Long tableId) { codegenService.syncCodegenFromDB(tableId); return success(true); } @Operation(summary = "删除数据库的表和字段定义") @DeleteMapping("/delete") @Parameter(name = "tableId", description = "表编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('infra:codegen:delete')") public CommonResult deleteCodegen(@RequestParam("tableId") Long tableId) { codegenService.deleteCodegen(tableId); return success(true); } @Operation(summary = "预览生成代码") @GetMapping("/preview") @Parameter(name = "tableId", description = "表编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('infra:codegen:preview')") public CommonResult> previewCodegen(@RequestParam("tableId") Long tableId) { Map codes = codegenService.generationCodes(tableId); return success(CodegenConvert.INSTANCE.convert(codes)); } @Operation(summary = "下载生成代码") @GetMapping("/download") @Parameter(name = "tableId", description = "表编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('infra:codegen:download')") public void downloadCodegen(@RequestParam("tableId") Long tableId, HttpServletResponse response) throws IOException { // 生成代码 Map codes = codegenService.generationCodes(tableId); // 构建 zip 包 String[] paths = codes.keySet().toArray(new String[0]); ByteArrayInputStream[] ins = codes.values().stream().map(IoUtil::toUtf8Stream).toArray(ByteArrayInputStream[]::new); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ZipUtil.zip(outputStream, paths, ins); // 输出 writeAttachment(response, "codegen.zip", outputStream.toByteArray()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/codegen/vo/CodegenCreateListReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.codegen.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotNull; import java.util.List; @Schema(description = "管理后台 - 基于数据库的表结构,创建代码生成器的表和字段定义 Request VO") @Data public class CodegenCreateListReqVO { @Schema(description = "数据源配置的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "数据源配置的编号不能为空") private Long dataSourceConfigId; @Schema(description = "表名数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2, 3]") @NotNull(message = "表名数组不能为空") private List tableNames; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/codegen/vo/CodegenDetailRespVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.codegen.vo; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.column.CodegenColumnRespVO; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.table.CodegenTableRespVO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.List; @Schema(description = "管理后台 - 代码生成表和字段的明细 Response VO") @Data public class CodegenDetailRespVO { @Schema(description = "表定义") private CodegenTableRespVO table; @Schema(description = "字段定义") private List columns; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/codegen/vo/CodegenPreviewRespVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.codegen.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "管理后台 - 代码生成预览 Response VO,注意,每个文件都是一个该对象") @Data public class CodegenPreviewRespVO { @Schema(description = "文件路径", requiredMode = Schema.RequiredMode.REQUIRED, example = "java/co.yixiang.yshop/adminserver/modules/system/controller/test/SysTestDemoController.java") private String filePath; @Schema(description = "代码", requiredMode = Schema.RequiredMode.REQUIRED, example = "Hello World") private String code; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/codegen/vo/CodegenUpdateReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.codegen.vo; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.column.CodegenColumnSaveReqVO; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.table.CodegenTableSaveReqVO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import java.util.List; @Schema(description = "管理后台 - 代码生成表和字段的修改 Request VO") @Data public class CodegenUpdateReqVO { @Valid // 校验内嵌的字段 @NotNull(message = "表定义不能为空") private CodegenTableSaveReqVO table; @Valid // 校验内嵌的字段 @NotNull(message = "字段定义不能为空") private List columns; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/codegen/vo/column/CodegenColumnRespVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.codegen.vo.column; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 代码生成字段定义 Response VO") @Data public class CodegenColumnRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long id; @Schema(description = "表编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long tableId; @Schema(description = "字段名", requiredMode = Schema.RequiredMode.REQUIRED, example = "user_age") private String columnName; @Schema(description = "字段类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "int(11)") private String dataType; @Schema(description = "字段描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "年龄") private String columnComment; @Schema(description = "是否允许为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") private Boolean nullable; @Schema(description = "是否主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") private Boolean primaryKey; @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") private Integer ordinalPosition; @Schema(description = "Java 属性类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "userAge") private String javaType; @Schema(description = "Java 属性名", requiredMode = Schema.RequiredMode.REQUIRED, example = "Integer") private String javaField; @Schema(description = "字典类型", example = "sys_gender") private String dictType; @Schema(description = "数据示例", example = "1024") private String example; @Schema(description = "是否为 Create 创建操作的字段", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") private Boolean createOperation; @Schema(description = "是否为 Update 更新操作的字段", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") private Boolean updateOperation; @Schema(description = "是否为 List 查询操作的字段", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") private Boolean listOperation; @Schema(description = "List 查询操作的条件类型,参见 CodegenColumnListConditionEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "LIKE") private String listOperationCondition; @Schema(description = "是否为 List 查询操作的返回字段", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") private Boolean listOperationResult; @Schema(description = "显示类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "input") private String htmlType; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/codegen/vo/column/CodegenColumnSaveReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.codegen.vo.column; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 代码生成字段定义创建/修改 Request VO") @Data public class CodegenColumnSaveReqVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long id; @Schema(description = "表编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "表编号不能为空") private Long tableId; @Schema(description = "字段名", requiredMode = Schema.RequiredMode.REQUIRED, example = "user_age") @NotNull(message = "字段名不能为空") private String columnName; @Schema(description = "字段类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "int(11)") @NotNull(message = "字段类型不能为空") private String dataType; @Schema(description = "字段描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "年龄") @NotNull(message = "字段描述不能为空") private String columnComment; @Schema(description = "是否允许为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") @NotNull(message = "是否允许为空不能为空") private Boolean nullable; @Schema(description = "是否主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") @NotNull(message = "是否主键不能为空") private Boolean primaryKey; @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") @NotNull(message = "排序不能为空") private Integer ordinalPosition; @Schema(description = "Java 属性类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "userAge") @NotNull(message = "Java 属性类型不能为空") private String javaType; @Schema(description = "Java 属性名", requiredMode = Schema.RequiredMode.REQUIRED, example = "Integer") @NotNull(message = "Java 属性名不能为空") private String javaField; @Schema(description = "字典类型", example = "sys_gender") private String dictType; @Schema(description = "数据示例", example = "1024") private String example; @Schema(description = "是否为 Create 创建操作的字段", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") @NotNull(message = "是否为 Create 创建操作的字段不能为空") private Boolean createOperation; @Schema(description = "是否为 Update 更新操作的字段", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") @NotNull(message = "是否为 Update 更新操作的字段不能为空") private Boolean updateOperation; @Schema(description = "是否为 List 查询操作的字段", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") @NotNull(message = "是否为 List 查询操作的字段不能为空") private Boolean listOperation; @Schema(description = "List 查询操作的条件类型,参见 CodegenColumnListConditionEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "LIKE") @NotNull(message = "List 查询操作的条件类型不能为空") private String listOperationCondition; @Schema(description = "是否为 List 查询操作的返回字段", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") @NotNull(message = "是否为 List 查询操作的返回字段不能为空") private Boolean listOperationResult; @Schema(description = "显示类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "input") @NotNull(message = "显示类型不能为空") private String htmlType; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/codegen/vo/table/CodegenTablePageReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.codegen.vo.table; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 表定义分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class CodegenTablePageReqVO extends PageParam { @Schema(description = "表名称,模糊匹配", example = "yshop") private String tableName; @Schema(description = "表描述,模糊匹配", example = "yshop") private String tableComment; @Schema(description = "实体,模糊匹配", example = "Yshop") private String className; @Schema(description = "创建时间", example = "[2022-07-01 00:00:00,2022-07-01 23:59:59]") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/codegen/vo/table/CodegenTableRespVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.codegen.vo.table; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 代码生成表定义 Response VO") @Data public class CodegenTableRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long id; @Schema(description = "生成场景,参见 CodegenSceneEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer scene; @Schema(description = "表名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String tableName; @Schema(description = "表描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String tableComment; @Schema(description = "备注", example = "我是备注") private String remark; @Schema(description = "模块名", requiredMode = Schema.RequiredMode.REQUIRED, example = "system") private String moduleName; @Schema(description = "业务名", requiredMode = Schema.RequiredMode.REQUIRED, example = "codegen") private String businessName; @Schema(description = "类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "CodegenTable") private String className; @Schema(description = "类描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "代码生成器的表定义") private String classComment; @Schema(description = "作者", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String author; @Schema(description = "模板类型,参见 CodegenTemplateTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer templateType; @Schema(description = "前端类型,参见 CodegenFrontTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") private Integer frontType; @Schema(description = "父菜单编号", example = "1024") private Long parentMenuId; @Schema(description = "主表的编号", example = "2048") private Long masterTableId; @Schema(description = "子表关联主表的字段编号", example = "4096") private Long subJoinColumnId; @Schema(description = "主表与子表是否一对多", example = "4096") private Boolean subJoinMany; @Schema(description = "树表的父字段编号", example = "8192") private Long treeParentColumnId; @Schema(description = "树表的名字字段编号", example = "16384") private Long treeNameColumnId; @Schema(description = "主键编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Integer dataSourceConfigId; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime updateTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/codegen/vo/table/CodegenTableSaveReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.codegen.vo.table; import cn.hutool.core.util.ObjectUtil; import co.yixiang.yshop.module.infra.enums.codegen.CodegenSceneEnum; import co.yixiang.yshop.module.infra.enums.codegen.CodegenTemplateTypeEnum; import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 代码生成表定义创建/修改 Response VO") @Data public class CodegenTableSaveReqVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long id; @Schema(description = "生成场景,参见 CodegenSceneEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "导入类型不能为空") private Integer scene; @Schema(description = "表名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @NotNull(message = "表名称不能为空") private String tableName; @Schema(description = "表描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @NotNull(message = "表描述不能为空") private String tableComment; @Schema(description = "备注", example = "我是备注") private String remark; @Schema(description = "模块名", requiredMode = Schema.RequiredMode.REQUIRED, example = "system") @NotNull(message = "模块名不能为空") private String moduleName; @Schema(description = "业务名", requiredMode = Schema.RequiredMode.REQUIRED, example = "codegen") @NotNull(message = "业务名不能为空") private String businessName; @Schema(description = "类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "CodegenTable") @NotNull(message = "类名称不能为空") private String className; @Schema(description = "类描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "代码生成器的表定义") @NotNull(message = "类描述不能为空") private String classComment; @Schema(description = "作者", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @NotNull(message = "作者不能为空") private String author; @Schema(description = "模板类型,参见 CodegenTemplateTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "模板类型不能为空") private Integer templateType; @Schema(description = "前端类型,参见 CodegenFrontTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") @NotNull(message = "前端类型不能为空") private Integer frontType; @Schema(description = "父菜单编号", example = "1024") private Long parentMenuId; @Schema(description = "主表的编号", example = "2048") private Long masterTableId; @Schema(description = "子表关联主表的字段编号", example = "4096") private Long subJoinColumnId; @Schema(description = "主表与子表是否一对多", example = "4096") private Boolean subJoinMany; @Schema(description = "树表的父字段编号", example = "8192") private Long treeParentColumnId; @Schema(description = "树表的名字字段编号", example = "16384") private Long treeNameColumnId; @AssertTrue(message = "上级菜单不能为空,请前往 [修改生成配置 -> 生成信息] 界面,设置“上级菜单”字段") @JsonIgnore public boolean isParentMenuIdValid() { // 生成场景为管理后台时,必须设置上级菜单,不然生成的菜单 SQL 是无父级菜单的 return ObjectUtil.notEqual(getScene(), CodegenSceneEnum.ADMIN.getScene()) || getParentMenuId() != null; } @AssertTrue(message = "关联的父表信息不全") @JsonIgnore public boolean isSubValid() { return ObjectUtil.notEqual(getTemplateType(), CodegenTemplateTypeEnum.SUB) || (ObjectUtil.isAllNotEmpty(masterTableId, subJoinColumnId, subJoinMany)); } @AssertTrue(message = "关联的树表信息不全") @JsonIgnore public boolean isTreeValid() { return ObjectUtil.notEqual(templateType, CodegenTemplateTypeEnum.TREE) || (ObjectUtil.isAllNotEmpty(treeParentColumnId, treeNameColumnId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/codegen/vo/table/DatabaseTableRespVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.codegen.vo.table; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "管理后台 - 数据库的表定义 Response VO") @Data public class DatabaseTableRespVO { @Schema(description = "表名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yuanma") private String name; @Schema(description = "表描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String comment; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/config/ConfigController.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.config; import co.yixiang.yshop.framework.apilog.core.annotation.ApiAccessLog; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.infra.controller.admin.config.vo.*; import co.yixiang.yshop.module.infra.convert.config.ConfigConvert; import co.yixiang.yshop.module.infra.dal.dataobject.config.ConfigDO; import co.yixiang.yshop.module.infra.enums.ErrorCodeConstants; import co.yixiang.yshop.module.infra.service.config.ConfigService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import java.io.IOException; import java.util.List; import static co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 参数配置") @RestController @RequestMapping("/infra/config") @Validated public class ConfigController { @Resource private ConfigService configService; @PostMapping("/create") @Operation(summary = "创建参数配置") @PreAuthorize("@ss.hasPermission('infra:config:create')") public CommonResult createConfig(@Valid @RequestBody ConfigSaveReqVO createReqVO) { return success(configService.createConfig(createReqVO)); } @PutMapping("/update") @Operation(summary = "修改参数配置") @PreAuthorize("@ss.hasPermission('infra:config:update')") public CommonResult updateConfig(@Valid @RequestBody ConfigSaveReqVO updateReqVO) { configService.updateConfig(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除参数配置") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('infra:config:delete')") public CommonResult deleteConfig(@RequestParam("id") Long id) { configService.deleteConfig(id); return success(true); } @GetMapping(value = "/get") @Operation(summary = "获得参数配置") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('infra:config:query')") public CommonResult getConfig(@RequestParam("id") Long id) { return success(ConfigConvert.INSTANCE.convert(configService.getConfig(id))); } @GetMapping(value = "/get-value-by-key") @Operation(summary = "根据参数键名查询参数值", description = "不可见的配置,不允许返回给前端") @Parameter(name = "key", description = "参数键", required = true, example = "yshop.biz.username") public CommonResult getConfigKey(@RequestParam("key") String key) { ConfigDO config = configService.getConfigByKey(key); if (config == null) { return success(null); } if (!config.getVisible()) { throw exception(ErrorCodeConstants.CONFIG_GET_VALUE_ERROR_IF_VISIBLE); } return success(config.getValue()); } @GetMapping("/page") @Operation(summary = "获取参数配置分页") @PreAuthorize("@ss.hasPermission('infra:config:query')") public CommonResult> getConfigPage(@Valid ConfigPageReqVO pageReqVO) { PageResult page = configService.getConfigPage(pageReqVO); return success(ConfigConvert.INSTANCE.convertPage(page)); } @GetMapping("/export") @Operation(summary = "导出参数配置") @PreAuthorize("@ss.hasPermission('infra:config:export')") @ApiAccessLog(operateType = EXPORT) public void exportConfig(@Valid ConfigPageReqVO exportReqVO, HttpServletResponse response) throws IOException { exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = configService.getConfigPage(exportReqVO).getList(); // 输出 ExcelUtils.write(response, "参数配置.xls", "数据", ConfigRespVO.class, ConfigConvert.INSTANCE.convertList(list)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/config/vo/ConfigPageReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.config.vo; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 参数配置分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ConfigPageReqVO extends PageParam { @Schema(description = "数据源名称,模糊匹配", example = "名称") private String name; @Schema(description = "参数键名,模糊匹配", example = "yshop.db.username") private String key; @Schema(description = "参数类型,参见 SysConfigTypeEnum 枚举", example = "1") private Integer type; @Schema(description = "创建时间", example = "[2022-07-01 00:00:00,2022-07-01 23:59:59]") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/config/vo/ConfigRespVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.config.vo; import co.yixiang.yshop.framework.excel.core.annotations.DictFormat; import co.yixiang.yshop.framework.excel.core.convert.DictConvert; import co.yixiang.yshop.module.infra.enums.DictTypeConstants; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 参数配置信息 Response VO") @Data @ExcelIgnoreUnannotated public class ConfigRespVO { @Schema(description = "参数配置序号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @ExcelProperty("参数配置序号") private Long id; @Schema(description = "参数分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "biz") @ExcelProperty("参数分类") private String category; @Schema(description = "参数名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "数据库名") @ExcelProperty("参数名称") private String name; @Schema(description = "参数键名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop.db.username") @ExcelProperty("参数键名") private String key; @Schema(description = "参数键值", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @ExcelProperty("参数键值") private String value; @Schema(description = "参数类型,参见 SysConfigTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty(value = "参数类型", converter = DictConvert.class) @DictFormat(DictTypeConstants.CONFIG_TYPE) private Integer type; @Schema(description = "是否可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") @ExcelProperty(value = "是否可见", converter = DictConvert.class) @DictFormat(DictTypeConstants.BOOLEAN_STRING) private Boolean visible; @Schema(description = "备注", example = "备注一下很帅气!") @ExcelProperty("备注") private String remark; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") @ExcelProperty("创建时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/config/vo/ConfigSaveReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.config.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @Schema(description = "管理后台 - 参数配置创建/修改 Request VO") @Data public class ConfigSaveReqVO { @Schema(description = "参数配置序号", example = "1024") private Long id; @Schema(description = "参数分组", requiredMode = Schema.RequiredMode.REQUIRED, example = "biz") @NotEmpty(message = "参数分组不能为空") @Size(max = 50, message = "参数名称不能超过 50 个字符") private String category; @Schema(description = "参数名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "数据库名") @NotBlank(message = "参数名称不能为空") @Size(max = 100, message = "参数名称不能超过 100 个字符") private String name; @Schema(description = "参数键名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop.db.username") @NotBlank(message = "参数键名长度不能为空") @Size(max = 100, message = "参数键名长度不能超过 100 个字符") private String key; @Schema(description = "参数键值", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotBlank(message = "参数键值不能为空") @Size(max = 500, message = "参数键值长度不能超过 500 个字符") private String value; @Schema(description = "是否可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") @NotNull(message = "是否可见不能为空") private Boolean visible; @Schema(description = "备注", example = "备注一下很帅气!") private String remark; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/db/DataSourceConfigController.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.db; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.infra.controller.admin.db.vo.DataSourceConfigRespVO; import co.yixiang.yshop.module.infra.controller.admin.db.vo.DataSourceConfigSaveReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.db.DataSourceConfigDO; import co.yixiang.yshop.module.infra.service.db.DataSourceConfigService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 数据源配置") @RestController @RequestMapping("/infra/data-source-config") @Validated public class DataSourceConfigController { @Resource private DataSourceConfigService dataSourceConfigService; @PostMapping("/create") @Operation(summary = "创建数据源配置") @PreAuthorize("@ss.hasPermission('infra:data-source-config:create')") public CommonResult createDataSourceConfig(@Valid @RequestBody DataSourceConfigSaveReqVO createReqVO) { return success(dataSourceConfigService.createDataSourceConfig(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新数据源配置") @PreAuthorize("@ss.hasPermission('infra:data-source-config:update')") public CommonResult updateDataSourceConfig(@Valid @RequestBody DataSourceConfigSaveReqVO updateReqVO) { dataSourceConfigService.updateDataSourceConfig(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除数据源配置") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('infra:data-source-config:delete')") public CommonResult deleteDataSourceConfig(@RequestParam("id") Long id) { dataSourceConfigService.deleteDataSourceConfig(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得数据源配置") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('infra:data-source-config:query')") public CommonResult getDataSourceConfig(@RequestParam("id") Long id) { DataSourceConfigDO config = dataSourceConfigService.getDataSourceConfig(id); return success(BeanUtils.toBean(config, DataSourceConfigRespVO.class)); } @GetMapping("/list") @Operation(summary = "获得数据源配置列表") @PreAuthorize("@ss.hasPermission('infra:data-source-config:query')") public CommonResult> getDataSourceConfigList() { List list = dataSourceConfigService.getDataSourceConfigList(); return success(BeanUtils.toBean(list, DataSourceConfigRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/db/vo/DataSourceConfigRespVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.db.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 数据源配置 Response VO") @Data public class DataSourceConfigRespVO { @Schema(description = "主键编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Integer id; @Schema(description = "数据源名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "test") private String name; @Schema(description = "数据源连接", requiredMode = Schema.RequiredMode.REQUIRED, example = "jdbc:mysql://127.0.0.1:3306/yixiang-drink") private String url; @Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "root") private String username; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/db/vo/DataSourceConfigSaveReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.db.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 数据源配置创建/修改 Request VO") @Data public class DataSourceConfigSaveReqVO { @Schema(description = "主键编号", example = "1024") private Long id; @Schema(description = "数据源名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "test") @NotNull(message = "数据源名称不能为空") private String name; @Schema(description = "数据源连接", requiredMode = Schema.RequiredMode.REQUIRED, example = "jdbc:mysql://127.0.0.1:3306/yixiang-drink") @NotNull(message = "数据源连接不能为空") private String url; @Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "root") @NotNull(message = "用户名不能为空") private String username; @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") @NotNull(message = "密码不能为空") private String password; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/demo/demo01/Demo01ContactController.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.demo.demo01; import co.yixiang.yshop.framework.apilog.core.annotation.ApiAccessLog; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.infra.controller.admin.demo.demo01.vo.Demo01ContactPageReqVO; import co.yixiang.yshop.module.infra.controller.admin.demo.demo01.vo.Demo01ContactRespVO; import co.yixiang.yshop.module.infra.controller.admin.demo.demo01.vo.Demo01ContactSaveReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo01.Demo01ContactDO; import co.yixiang.yshop.module.infra.service.demo.demo01.Demo01ContactService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.io.IOException; import java.util.List; import static co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 示例联系人") @RestController @RequestMapping("/infra/demo01-contact") @Validated public class Demo01ContactController { @Resource private Demo01ContactService demo01ContactService; @PostMapping("/create") @Operation(summary = "创建示例联系人") @PreAuthorize("@ss.hasPermission('infra:demo01-contact:create')") public CommonResult createDemo01Contact(@Valid @RequestBody Demo01ContactSaveReqVO createReqVO) { return success(demo01ContactService.createDemo01Contact(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新示例联系人") @PreAuthorize("@ss.hasPermission('infra:demo01-contact:update')") public CommonResult updateDemo01Contact(@Valid @RequestBody Demo01ContactSaveReqVO updateReqVO) { demo01ContactService.updateDemo01Contact(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除示例联系人") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('infra:demo01-contact:delete')") public CommonResult deleteDemo01Contact(@RequestParam("id") Long id) { demo01ContactService.deleteDemo01Contact(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得示例联系人") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('infra:demo01-contact:query')") public CommonResult getDemo01Contact(@RequestParam("id") Long id) { Demo01ContactDO demo01Contact = demo01ContactService.getDemo01Contact(id); return success(BeanUtils.toBean(demo01Contact, Demo01ContactRespVO.class)); } @GetMapping("/page") @Operation(summary = "获得示例联系人分页") @PreAuthorize("@ss.hasPermission('infra:demo01-contact:query')") public CommonResult> getDemo01ContactPage(@Valid Demo01ContactPageReqVO pageReqVO) { PageResult pageResult = demo01ContactService.getDemo01ContactPage(pageReqVO); return success(BeanUtils.toBean(pageResult, Demo01ContactRespVO.class)); } @GetMapping("/export-excel") @Operation(summary = "导出示例联系人 Excel") @PreAuthorize("@ss.hasPermission('infra:demo01-contact:export')") @ApiAccessLog(operateType = EXPORT) public void exportDemo01ContactExcel(@Valid Demo01ContactPageReqVO pageReqVO, HttpServletResponse response) throws IOException { pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = demo01ContactService.getDemo01ContactPage(pageReqVO).getList(); // 导出 Excel ExcelUtils.write(response, "示例联系人.xls", "数据", Demo01ContactRespVO.class, BeanUtils.toBean(list, Demo01ContactRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/demo/demo01/vo/Demo01ContactPageReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.demo.demo01.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 示例联系人分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class Demo01ContactPageReqVO extends PageParam { @Schema(description = "名字", example = "张三") private String name; @Schema(description = "性别", example = "1") private Integer sex; @Schema(description = "创建时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/demo/demo01/vo/Demo01ContactRespVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.demo.demo01.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.util.*; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import com.alibaba.excel.annotation.*; import co.yixiang.yshop.framework.excel.core.annotations.DictFormat; import co.yixiang.yshop.framework.excel.core.convert.DictConvert; @Schema(description = "管理后台 - 示例联系人 Response VO") @Data @ExcelIgnoreUnannotated public class Demo01ContactRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "21555") @ExcelProperty("编号") private Long id; @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") @ExcelProperty("名字") private String name; @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty(value = "性别", converter = DictConvert.class) @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 private Integer sex; @Schema(description = "出生年", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("出生年") private LocalDateTime birthday; @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "你说的对") @ExcelProperty("简介") private String description; @Schema(description = "头像") @ExcelProperty("头像") private String avatar; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("创建时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/demo/demo01/vo/Demo01ContactSaveReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.demo.demo01.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; @Schema(description = "管理后台 - 示例联系人新增/修改 Request VO") @Data public class Demo01ContactSaveReqVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "21555") private Long id; @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") @NotEmpty(message = "名字不能为空") private String name; @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "性别不能为空") private Integer sex; @Schema(description = "出生年", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "出生年不能为空") private LocalDateTime birthday; @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "你说的对") @NotEmpty(message = "简介不能为空") private String description; @Schema(description = "头像") private String avatar; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/demo/demo02/Demo02CategoryController.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.demo.demo02; import co.yixiang.yshop.framework.apilog.core.annotation.ApiAccessLog; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.infra.controller.admin.demo.demo02.vo.Demo02CategoryListReqVO; import co.yixiang.yshop.module.infra.controller.admin.demo.demo02.vo.Demo02CategoryRespVO; import co.yixiang.yshop.module.infra.controller.admin.demo.demo02.vo.Demo02CategorySaveReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo02.Demo02CategoryDO; import co.yixiang.yshop.module.infra.service.demo.demo02.Demo02CategoryService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.io.IOException; import java.util.List; import static co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 示例分类") @RestController @RequestMapping("/infra/demo02-category") @Validated public class Demo02CategoryController { @Resource private Demo02CategoryService demo02CategoryService; @PostMapping("/create") @Operation(summary = "创建示例分类") @PreAuthorize("@ss.hasPermission('infra:demo02-category:create')") public CommonResult createDemo02Category(@Valid @RequestBody Demo02CategorySaveReqVO createReqVO) { return success(demo02CategoryService.createDemo02Category(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新示例分类") @PreAuthorize("@ss.hasPermission('infra:demo02-category:update')") public CommonResult updateDemo02Category(@Valid @RequestBody Demo02CategorySaveReqVO updateReqVO) { demo02CategoryService.updateDemo02Category(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除示例分类") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('infra:demo02-category:delete')") public CommonResult deleteDemo02Category(@RequestParam("id") Long id) { demo02CategoryService.deleteDemo02Category(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得示例分类") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('infra:demo02-category:query')") public CommonResult getDemo02Category(@RequestParam("id") Long id) { Demo02CategoryDO demo02Category = demo02CategoryService.getDemo02Category(id); return success(BeanUtils.toBean(demo02Category, Demo02CategoryRespVO.class)); } @GetMapping("/list") @Operation(summary = "获得示例分类列表") @PreAuthorize("@ss.hasPermission('infra:demo02-category:query')") public CommonResult> getDemo02CategoryList(@Valid Demo02CategoryListReqVO listReqVO) { List list = demo02CategoryService.getDemo02CategoryList(listReqVO); return success(BeanUtils.toBean(list, Demo02CategoryRespVO.class)); } @GetMapping("/export-excel") @Operation(summary = "导出示例分类 Excel") @PreAuthorize("@ss.hasPermission('infra:demo02-category:export')") @ApiAccessLog(operateType = EXPORT) public void exportDemo02CategoryExcel(@Valid Demo02CategoryListReqVO listReqVO, HttpServletResponse response) throws IOException { List list = demo02CategoryService.getDemo02CategoryList(listReqVO); // 导出 Excel ExcelUtils.write(response, "示例分类.xls", "数据", Demo02CategoryRespVO.class, BeanUtils.toBean(list, Demo02CategoryRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryListReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.demo.demo02.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 示例分类列表 Request VO") @Data public class Demo02CategoryListReqVO { @Schema(description = "名字", example = "yshop") private String name; @Schema(description = "父级编号", example = "6080") private Long parentId; @Schema(description = "创建时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryRespVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.demo.demo02.vo; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 示例分类 Response VO") @Data @ExcelIgnoreUnannotated public class Demo02CategoryRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10304") @ExcelProperty("编号") private Long id; @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @ExcelProperty("名字") private String name; @Schema(description = "父级编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6080") @ExcelProperty("父级编号") private Long parentId; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("创建时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/demo/demo02/vo/Demo02CategorySaveReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.demo.demo02.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 示例分类新增/修改 Request VO") @Data public class Demo02CategorySaveReqVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10304") private Long id; @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @NotEmpty(message = "名字不能为空") private String name; @Schema(description = "父级编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6080") @NotNull(message = "父级编号不能为空") private Long parentId; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/demo/demo03/Demo03StudentController.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.demo.demo03; import co.yixiang.yshop.framework.apilog.core.annotation.ApiAccessLog; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.infra.controller.admin.demo.demo03.vo.Demo03StudentPageReqVO; import co.yixiang.yshop.module.infra.controller.admin.demo.demo03.vo.Demo03StudentRespVO; import co.yixiang.yshop.module.infra.controller.admin.demo.demo03.vo.Demo03StudentSaveReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO; import co.yixiang.yshop.module.infra.service.demo.demo03.Demo03StudentService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.io.IOException; import java.util.List; import static co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 学生") @RestController @RequestMapping("/infra/demo03-student") @Validated public class Demo03StudentController { @Resource private Demo03StudentService demo03StudentService; @PostMapping("/create") @Operation(summary = "创建学生") @PreAuthorize("@ss.hasPermission('infra:demo03-student:create')") public CommonResult createDemo03Student(@Valid @RequestBody Demo03StudentSaveReqVO createReqVO) { return success(demo03StudentService.createDemo03Student(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新学生") @PreAuthorize("@ss.hasPermission('infra:demo03-student:update')") public CommonResult updateDemo03Student(@Valid @RequestBody Demo03StudentSaveReqVO updateReqVO) { demo03StudentService.updateDemo03Student(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除学生") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('infra:demo03-student:delete')") public CommonResult deleteDemo03Student(@RequestParam("id") Long id) { demo03StudentService.deleteDemo03Student(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得学生") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") public CommonResult getDemo03Student(@RequestParam("id") Long id) { Demo03StudentDO demo03Student = demo03StudentService.getDemo03Student(id); return success(BeanUtils.toBean(demo03Student, Demo03StudentRespVO.class)); } @GetMapping("/page") @Operation(summary = "获得学生分页") @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") public CommonResult> getDemo03StudentPage(@Valid Demo03StudentPageReqVO pageReqVO) { PageResult pageResult = demo03StudentService.getDemo03StudentPage(pageReqVO); return success(BeanUtils.toBean(pageResult, Demo03StudentRespVO.class)); } @GetMapping("/export-excel") @Operation(summary = "导出学生 Excel") @PreAuthorize("@ss.hasPermission('infra:demo03-student:export')") @ApiAccessLog(operateType = EXPORT) public void exportDemo03StudentExcel(@Valid Demo03StudentPageReqVO pageReqVO, HttpServletResponse response) throws IOException { pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = demo03StudentService.getDemo03StudentPage(pageReqVO).getList(); // 导出 Excel ExcelUtils.write(response, "学生.xls", "数据", Demo03StudentRespVO.class, BeanUtils.toBean(list, Demo03StudentRespVO.class)); } // ==================== 子表(学生课程) ==================== @GetMapping("/demo03-course/page") @Operation(summary = "获得学生课程分页") @Parameter(name = "studentId", description = "学生编号") @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") public CommonResult> getDemo03CoursePage(PageParam pageReqVO, @RequestParam("studentId") Long studentId) { return success(demo03StudentService.getDemo03CoursePage(pageReqVO, studentId)); } @PostMapping("/demo03-course/create") @Operation(summary = "创建学生课程") @PreAuthorize("@ss.hasPermission('infra:demo03-student:create')") public CommonResult createDemo03Course(@Valid @RequestBody Demo03CourseDO demo03Course) { return success(demo03StudentService.createDemo03Course(demo03Course)); } @PutMapping("/demo03-course/update") @Operation(summary = "更新学生课程") @PreAuthorize("@ss.hasPermission('infra:demo03-student:update')") public CommonResult updateDemo03Course(@Valid @RequestBody Demo03CourseDO demo03Course) { demo03StudentService.updateDemo03Course(demo03Course); return success(true); } @DeleteMapping("/demo03-course/delete") @Parameter(name = "id", description = "编号", required = true) @Operation(summary = "删除学生课程") @PreAuthorize("@ss.hasPermission('infra:demo03-student:delete')") public CommonResult deleteDemo03Course(@RequestParam("id") Long id) { demo03StudentService.deleteDemo03Course(id); return success(true); } @GetMapping("/demo03-course/get") @Operation(summary = "获得学生课程") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") public CommonResult getDemo03Course(@RequestParam("id") Long id) { return success(demo03StudentService.getDemo03Course(id)); } @GetMapping("/demo03-course/list-by-student-id") @Operation(summary = "获得学生课程列表") @Parameter(name = "studentId", description = "学生编号") @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") public CommonResult> getDemo03CourseListByStudentId(@RequestParam("studentId") Long studentId) { return success(demo03StudentService.getDemo03CourseListByStudentId(studentId)); } // ==================== 子表(学生班级) ==================== @GetMapping("/demo03-grade/page") @Operation(summary = "获得学生班级分页") @Parameter(name = "studentId", description = "学生编号") @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") public CommonResult> getDemo03GradePage(PageParam pageReqVO, @RequestParam("studentId") Long studentId) { return success(demo03StudentService.getDemo03GradePage(pageReqVO, studentId)); } @PostMapping("/demo03-grade/create") @Operation(summary = "创建学生班级") @PreAuthorize("@ss.hasPermission('infra:demo03-student:create')") public CommonResult createDemo03Grade(@Valid @RequestBody Demo03GradeDO demo03Grade) { return success(demo03StudentService.createDemo03Grade(demo03Grade)); } @PutMapping("/demo03-grade/update") @Operation(summary = "更新学生班级") @PreAuthorize("@ss.hasPermission('infra:demo03-student:update')") public CommonResult updateDemo03Grade(@Valid @RequestBody Demo03GradeDO demo03Grade) { demo03StudentService.updateDemo03Grade(demo03Grade); return success(true); } @DeleteMapping("/demo03-grade/delete") @Parameter(name = "id", description = "编号", required = true) @Operation(summary = "删除学生班级") @PreAuthorize("@ss.hasPermission('infra:demo03-student:delete')") public CommonResult deleteDemo03Grade(@RequestParam("id") Long id) { demo03StudentService.deleteDemo03Grade(id); return success(true); } @GetMapping("/demo03-grade/get") @Operation(summary = "获得学生班级") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") public CommonResult getDemo03Grade(@RequestParam("id") Long id) { return success(demo03StudentService.getDemo03Grade(id)); } @GetMapping("/demo03-grade/get-by-student-id") @Operation(summary = "获得学生班级") @Parameter(name = "studentId", description = "学生编号") @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") public CommonResult getDemo03GradeByStudentId(@RequestParam("studentId") Long studentId) { return success(demo03StudentService.getDemo03GradeByStudentId(studentId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/demo/demo03/package-info.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.demo.demo03; ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/demo/demo03/vo/Demo03StudentPageReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.demo.demo03.vo; import lombok.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 学生分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class Demo03StudentPageReqVO extends PageParam { @Schema(description = "名字", example = "yshop") private String name; @Schema(description = "性别") private Integer sex; @Schema(description = "简介", example = "随便") private String description; @Schema(description = "创建时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/demo/demo03/vo/Demo03StudentRespVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.demo.demo03.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; import com.alibaba.excel.annotation.*; import co.yixiang.yshop.framework.excel.core.annotations.DictFormat; import co.yixiang.yshop.framework.excel.core.convert.DictConvert; @Schema(description = "管理后台 - 学生 Response VO") @Data @ExcelIgnoreUnannotated public class Demo03StudentRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8525") @ExcelProperty("编号") private Long id; @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @ExcelProperty("名字") private String name; @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty(value = "性别", converter = DictConvert.class) @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 private Integer sex; @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("出生日期") private LocalDateTime birthday; @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "随便") @ExcelProperty("简介") private String description; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("创建时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/demo/demo03/vo/Demo03StudentSaveReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.demo.demo03.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import jakarta.validation.constraints.*; import java.time.LocalDateTime; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; @Schema(description = "管理后台 - 学生新增/修改 Request VO") @Data public class Demo03StudentSaveReqVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8525") private Long id; @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @NotEmpty(message = "名字不能为空") private String name; @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "性别不能为空") private Integer sex; @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "出生日期不能为空") private LocalDateTime birthday; @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "随便") @NotEmpty(message = "简介不能为空") private String description; private List demo03Courses; private Demo03GradeDO demo03Grade; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/file/FileConfigController.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.file; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; import co.yixiang.yshop.module.infra.controller.admin.file.vo.config.FileConfigRespVO; import co.yixiang.yshop.module.infra.controller.admin.file.vo.config.FileConfigSaveReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.file.FileConfigDO; import co.yixiang.yshop.module.infra.service.file.FileConfigService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 文件配置") @RestController @RequestMapping("/infra/file-config") @Validated public class FileConfigController { @Resource private FileConfigService fileConfigService; @PostMapping("/create") @Operation(summary = "创建文件配置") @PreAuthorize("@ss.hasPermission('infra:file-config:create')") public CommonResult createFileConfig(@Valid @RequestBody FileConfigSaveReqVO createReqVO) { return success(fileConfigService.createFileConfig(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新文件配置") @PreAuthorize("@ss.hasPermission('infra:file-config:update')") public CommonResult updateFileConfig(@Valid @RequestBody FileConfigSaveReqVO updateReqVO) { fileConfigService.updateFileConfig(updateReqVO); return success(true); } @PutMapping("/update-master") @Operation(summary = "更新文件配置为 Master") @PreAuthorize("@ss.hasPermission('infra:file-config:update')") public CommonResult updateFileConfigMaster(@RequestParam("id") Long id) { fileConfigService.updateFileConfigMaster(id); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除文件配置") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('infra:file-config:delete')") public CommonResult deleteFileConfig(@RequestParam("id") Long id) { fileConfigService.deleteFileConfig(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得文件配置") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('infra:file-config:query')") public CommonResult getFileConfig(@RequestParam("id") Long id) { FileConfigDO config = fileConfigService.getFileConfig(id); return success(BeanUtils.toBean(config, FileConfigRespVO.class)); } @GetMapping("/page") @Operation(summary = "获得文件配置分页") @PreAuthorize("@ss.hasPermission('infra:file-config:query')") public CommonResult> getFileConfigPage(@Valid FileConfigPageReqVO pageVO) { PageResult pageResult = fileConfigService.getFileConfigPage(pageVO); return success(BeanUtils.toBean(pageResult, FileConfigRespVO.class)); } @GetMapping("/test") @Operation(summary = "测试文件配置是否正确") @PreAuthorize("@ss.hasPermission('infra:file-config:query')") public CommonResult testFileConfig(@RequestParam("id") Long id) throws Exception { String url = fileConfigService.testFileConfig(id); return success(url); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/file/FileController.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.file; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.URLUtil; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.infra.controller.admin.file.vo.file.*; import co.yixiang.yshop.module.infra.dal.dataobject.file.FileDO; import co.yixiang.yshop.module.infra.service.file.FileService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.annotation.security.PermitAll; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import static co.yixiang.yshop.module.infra.framework.file.core.utils.FileTypeUtils.writeAttachment; @Tag(name = "管理后台 - 文件存储") @RestController @RequestMapping("/infra/file") @Validated @Slf4j public class FileController { @Resource private FileService fileService; @PostMapping("/upload") @Operation(summary = "上传文件", description = "模式一:后端上传文件") public CommonResult uploadFile(FileUploadReqVO uploadReqVO) throws Exception { MultipartFile file = uploadReqVO.getFile(); String path = uploadReqVO.getPath(); return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream()))); } @GetMapping("/presigned-url") @Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器") public CommonResult getFilePresignedUrl(@RequestParam("path") String path) throws Exception { return success(fileService.getFilePresignedUrl(path)); } @PostMapping("/create") @Operation(summary = "创建文件", description = "模式二:前端上传文件:配合 presigned-url 接口,记录上传了上传的文件") public CommonResult createFile(@Valid @RequestBody FileCreateReqVO createReqVO) { return success(fileService.createFile(createReqVO)); } @DeleteMapping("/delete") @Operation(summary = "删除文件") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('infra:file:delete')") public CommonResult deleteFile(@RequestParam("id") Long id) throws Exception { fileService.deleteFile(id); return success(true); } @GetMapping("/{configId}/get/**") @PermitAll @Operation(summary = "下载文件") @Parameter(name = "configId", description = "配置编号", required = true) public void getFileContent(HttpServletRequest request, HttpServletResponse response, @PathVariable("configId") Long configId) throws Exception { // 获取请求的路径 String path = StrUtil.subAfter(request.getRequestURI(), "/get/", false); if (StrUtil.isEmpty(path)) { throw new IllegalArgumentException("结尾的 path 路径必须传递"); } // 解码,解决中文路径的问题 https://gitee.com/zhijiantianya/yixiang-drink/pulls/807/ path = URLUtil.decode(path); // 读取内容 byte[] content = fileService.getFileContent(configId, path); if (content == null) { log.warn("[getFileContent][configId({}) path({}) 文件不存在]", configId, path); response.setStatus(HttpStatus.NOT_FOUND.value()); return; } writeAttachment(response, path, content); } @GetMapping("/page") @Operation(summary = "获得文件分页") @PreAuthorize("@ss.hasPermission('infra:file:query')") public CommonResult> getFilePage(@Valid FilePageReqVO pageVO) { PageResult pageResult = fileService.getFilePage(pageVO); return success(BeanUtils.toBean(pageResult, FileRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/file/vo/config/FileConfigPageReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.file.vo.config; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 文件配置分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class FileConfigPageReqVO extends PageParam { @Schema(description = "配置名", example = "S3 - 阿里云") private String name; @Schema(description = "存储器", example = "1") private Integer storage; @Schema(description = "创建时间", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/file/vo/config/FileConfigRespVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.file.vo.config; import co.yixiang.yshop.module.infra.framework.file.core.client.FileClientConfig; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 文件配置 Response VO") @Data public class FileConfigRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long id; @Schema(description = "配置名", requiredMode = Schema.RequiredMode.REQUIRED, example = "S3 - 阿里云") private String name; @Schema(description = "存储器,参见 FileStorageEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer storage; @Schema(description = "是否为主配置", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") private Boolean master; @Schema(description = "存储配置", requiredMode = Schema.RequiredMode.REQUIRED) private FileClientConfig config; @Schema(description = "备注", example = "我是备注") private String remark; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/file/vo/config/FileConfigSaveReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.file.vo.config; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotNull; import java.util.Map; @Schema(description = "管理后台 - 文件配置创建/修改 Request VO") @Data public class FileConfigSaveReqVO { @Schema(description = "编号", example = "1") private Long id; @Schema(description = "配置名", requiredMode = Schema.RequiredMode.REQUIRED, example = "S3 - 阿里云") @NotNull(message = "配置名不能为空") private String name; @Schema(description = "存储器,参见 FileStorageEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "存储器不能为空") private Integer storage; @Schema(description = "存储配置,配置是动态参数,所以使用 Map 接收", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "存储配置不能为空") private Map config; @Schema(description = "备注", example = "我是备注") private String remark; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/file/vo/file/FileCreateReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.file.vo.file; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; @Schema(description = "管理后台 - 文件创建 Request VO") @Data public class FileCreateReqVO { @NotNull(message = "文件配置编号不能为空") @Schema(description = "文件配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11") private Long configId; @NotNull(message = "文件路径不能为空") @Schema(description = "文件路径", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop.jpg") private String path; @NotNull(message = "原文件名不能为空") @Schema(description = "原文件名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop.jpg") private String name; @NotNull(message = "文件 URL不能为空") @Schema(description = "文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.yixiang.co/yshop.jpg") private String url; @Schema(description = "文件 MIME 类型", example = "application/octet-stream") private String type; @Schema(description = "文件大小", example = "2048", requiredMode = Schema.RequiredMode.REQUIRED) private Integer size; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/file/vo/file/FilePageReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.file.vo.file; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 文件分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class FilePageReqVO extends PageParam { @Schema(description = "文件路径,模糊匹配", example = "yshop") private String path; @Schema(description = "文件类型,模糊匹配", example = "jpg") private String type; @Schema(description = "创建时间", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/file/vo/file/FilePresignedUrlRespVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.file.vo.file; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @AllArgsConstructor @NoArgsConstructor @Schema(description = "管理后台 - 文件预签名地址 Response VO") @Data public class FilePresignedUrlRespVO { @Schema(description = "配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11") private Long configId; @Schema(description = "文件上传 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://s3.cn-south-1.qiniucs.com/yixiang-drink/758d3a5387507358c7236de4c8f96de1c7f5097ff6a7722b34772fb7b76b140f.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=3TvrJ70gl2Gt6IBe7_IZT1F6i_k0iMuRtyEv4EyS%2F20240217%2Fcn-south-1%2Fs3%2Faws4_request&X-Amz-Date=20240217T123222Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=a29f33770ab79bf523ccd4034d0752ac545f3c2a3b17baa1eb4e280cfdccfda5") private String uploadUrl; /** * 为什么要返回 url 字段? * * 前端上传完文件后,需要使用该 URL 进行访问 */ @Schema(description = "文件访问 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://test.yshop.yixiang.co/758d3a5387507358c7236de4c8f96de1c7f5097ff6a7722b34772fb7b76b140f.png") private String url; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/file/vo/file/FileRespVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.file.vo.file; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 文件 Response VO,不返回 content 字段,太大") @Data public class FileRespVO { @Schema(description = "文件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11") private Long configId; @Schema(description = "文件路径", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop.jpg") private String path; @Schema(description = "原文件名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop.jpg") private String name; @Schema(description = "文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.yixiang.co/yshop.jpg") private String url; @Schema(description = "文件MIME类型", example = "application/octet-stream") private String type; @Schema(description = "文件大小", example = "2048", requiredMode = Schema.RequiredMode.REQUIRED) private Integer size; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.file.vo.file; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.springframework.web.multipart.MultipartFile; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 上传文件 Request VO") @Data public class FileUploadReqVO { @Schema(description = "文件附件", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "文件附件不能为空") private MultipartFile file; @Schema(description = "文件附件", example = "yshopyuanma.png") private String path; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/job/JobController.http ================================================ ### 请求 /infra/job/sync 接口 => 成功 POST {{baseUrl}}/infra/job/sync Content-Type: application/json tenant-id: {{adminTenentId}} Authorization: Bearer {{token}} ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/job/JobController.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.job; import co.yixiang.yshop.framework.apilog.core.annotation.ApiAccessLog; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.framework.quartz.core.util.CronUtils; import co.yixiang.yshop.module.infra.controller.admin.job.vo.job.JobPageReqVO; import co.yixiang.yshop.module.infra.controller.admin.job.vo.job.JobRespVO; import co.yixiang.yshop.module.infra.controller.admin.job.vo.job.JobSaveReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.job.JobDO; import co.yixiang.yshop.module.infra.service.job.JobService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.quartz.SchedulerException; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.io.IOException; import java.time.LocalDateTime; import java.util.Collections; import java.util.List; import static co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 定时任务") @RestController @RequestMapping("/infra/job") @Validated public class JobController { @Resource private JobService jobService; @PostMapping("/create") @Operation(summary = "创建定时任务") @PreAuthorize("@ss.hasPermission('infra:job:create')") public CommonResult createJob(@Valid @RequestBody JobSaveReqVO createReqVO) throws SchedulerException { return success(jobService.createJob(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新定时任务") @PreAuthorize("@ss.hasPermission('infra:job:update')") public CommonResult updateJob(@Valid @RequestBody JobSaveReqVO updateReqVO) throws SchedulerException { jobService.updateJob(updateReqVO); return success(true); } @PutMapping("/update-status") @Operation(summary = "更新定时任务的状态") @Parameters({ @Parameter(name = "id", description = "编号", required = true, example = "1024"), @Parameter(name = "status", description = "状态", required = true, example = "1"), }) @PreAuthorize("@ss.hasPermission('infra:job:update')") public CommonResult updateJobStatus(@RequestParam(value = "id") Long id, @RequestParam("status") Integer status) throws SchedulerException { jobService.updateJobStatus(id, status); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除定时任务") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('infra:job:delete')") public CommonResult deleteJob(@RequestParam("id") Long id) throws SchedulerException { jobService.deleteJob(id); return success(true); } @PutMapping("/trigger") @Operation(summary = "触发定时任务") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('infra:job:trigger')") public CommonResult triggerJob(@RequestParam("id") Long id) throws SchedulerException { jobService.triggerJob(id); return success(true); } @PostMapping("/sync") @Operation(summary = "同步定时任务") @PreAuthorize("@ss.hasPermission('infra:job:create')") public CommonResult syncJob() throws SchedulerException { jobService.syncJob(); return success(true); } @GetMapping("/get") @Operation(summary = "获得定时任务") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('infra:job:query')") public CommonResult getJob(@RequestParam("id") Long id) { JobDO job = jobService.getJob(id); return success(BeanUtils.toBean(job, JobRespVO.class)); } @GetMapping("/page") @Operation(summary = "获得定时任务分页") @PreAuthorize("@ss.hasPermission('infra:job:query')") public CommonResult> getJobPage(@Valid JobPageReqVO pageVO) { PageResult pageResult = jobService.getJobPage(pageVO); return success(BeanUtils.toBean(pageResult, JobRespVO.class)); } @GetMapping("/export-excel") @Operation(summary = "导出定时任务 Excel") @PreAuthorize("@ss.hasPermission('infra:job:export')") @ApiAccessLog(operateType = EXPORT) public void exportJobExcel(@Valid JobPageReqVO exportReqVO, HttpServletResponse response) throws IOException { exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = jobService.getJobPage(exportReqVO).getList(); // 导出 Excel ExcelUtils.write(response, "定时任务.xls", "数据", JobRespVO.class, BeanUtils.toBean(list, JobRespVO.class)); } @GetMapping("/get_next_times") @Operation(summary = "获得定时任务的下 n 次执行时间") @Parameters({ @Parameter(name = "id", description = "编号", required = true, example = "1024"), @Parameter(name = "count", description = "数量", example = "5") }) @PreAuthorize("@ss.hasPermission('infra:job:query')") public CommonResult> getJobNextTimes( @RequestParam("id") Long id, @RequestParam(value = "count", required = false, defaultValue = "5") Integer count) { JobDO job = jobService.getJob(id); if (job == null) { return success(Collections.emptyList()); } return success(CronUtils.getNextTimes(job.getCronExpression(), count)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/job/JobLogController.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.job; import co.yixiang.yshop.framework.apilog.core.annotation.ApiAccessLog; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; import co.yixiang.yshop.module.infra.controller.admin.job.vo.log.JobLogRespVO; import co.yixiang.yshop.module.infra.dal.dataobject.job.JobLogDO; import co.yixiang.yshop.module.infra.service.job.JobLogService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.io.IOException; import java.util.List; import static co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 定时任务日志") @RestController @RequestMapping("/infra/job-log") @Validated public class JobLogController { @Resource private JobLogService jobLogService; @GetMapping("/get") @Operation(summary = "获得定时任务日志") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('infra:job:query')") public CommonResult getJobLog(@RequestParam("id") Long id) { JobLogDO jobLog = jobLogService.getJobLog(id); return success(BeanUtils.toBean(jobLog, JobLogRespVO.class)); } @GetMapping("/page") @Operation(summary = "获得定时任务日志分页") @PreAuthorize("@ss.hasPermission('infra:job:query')") public CommonResult> getJobLogPage(@Valid JobLogPageReqVO pageVO) { PageResult pageResult = jobLogService.getJobLogPage(pageVO); return success(BeanUtils.toBean(pageResult, JobLogRespVO.class)); } @GetMapping("/export-excel") @Operation(summary = "导出定时任务日志 Excel") @PreAuthorize("@ss.hasPermission('infra:job:export')") @ApiAccessLog(operateType = EXPORT) public void exportJobLogExcel(@Valid JobLogPageReqVO exportReqVO, HttpServletResponse response) throws IOException { exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = jobLogService.getJobLogPage(exportReqVO).getList(); // 导出 Excel ExcelUtils.write(response, "任务日志.xls", "数据", JobLogRespVO.class, BeanUtils.toBean(list, JobLogRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/job/vo/job/JobPageReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.job.vo.job; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @Schema(description = "管理后台 - 定时任务分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class JobPageReqVO extends PageParam { @Schema(description = "任务名称,模糊匹配", example = "测试任务") private String name; @Schema(description = "任务状态,参见 JobStatusEnum 枚举", example = "1") private Integer status; @Schema(description = "处理器的名字,模糊匹配", example = "sysUserSessionTimeoutJob") private String handlerName; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/job/vo/job/JobRespVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.job.vo.job; import co.yixiang.yshop.framework.excel.core.annotations.DictFormat; import co.yixiang.yshop.framework.excel.core.convert.DictConvert; import co.yixiang.yshop.module.infra.enums.DictTypeConstants; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; @Schema(description = "管理后台 - 定时任务 Response VO") @Data @ExcelIgnoreUnannotated public class JobRespVO { @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @ExcelProperty("任务编号") private Long id; @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试任务") @ExcelProperty("任务名称") private String name; @Schema(description = "任务状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty(value = "任务状态", converter = DictConvert.class) @DictFormat(DictTypeConstants.JOB_STATUS) private Integer status; @Schema(description = "处理器的名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "sysUserSessionTimeoutJob") @ExcelProperty("处理器的名字") private String handlerName; @Schema(description = "处理器的参数", example = "yshop") @ExcelProperty("处理器的参数") private String handlerParam; @Schema(description = "CRON 表达式", requiredMode = Schema.RequiredMode.REQUIRED, example = "0/10 * * * * ? *") @ExcelProperty("CRON 表达式") private String cronExpression; @Schema(description = "重试次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") @NotNull(message = "重试次数不能为空") private Integer retryCount; @Schema(description = "重试间隔", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") private Integer retryInterval; @Schema(description = "监控超时时间", example = "1000") @ExcelProperty("监控超时时间") private Integer monitorTimeout; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("创建时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/job/vo/job/JobSaveReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.job.vo.job; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 定时任务创建/修改 Request VO") @Data public class JobSaveReqVO { @Schema(description = "任务编号", example = "1024") private Long id; @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试任务") @NotEmpty(message = "任务名称不能为空") private String name; @Schema(description = "处理器的名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "sysUserSessionTimeoutJob") @NotEmpty(message = "处理器的名字不能为空") private String handlerName; @Schema(description = "处理器的参数", example = "yshop") private String handlerParam; @Schema(description = "CRON 表达式", requiredMode = Schema.RequiredMode.REQUIRED, example = "0/10 * * * * ? *") @NotEmpty(message = "CRON 表达式不能为空") private String cronExpression; @Schema(description = "重试次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") @NotNull(message = "重试次数不能为空") private Integer retryCount; @Schema(description = "重试间隔", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") @NotNull(message = "重试间隔不能为空") private Integer retryInterval; @Schema(description = "监控超时时间", example = "1000") private Integer monitorTimeout; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/job/vo/log/JobLogPageReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.job.vo.log; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 定时任务日志分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class JobLogPageReqVO extends PageParam { @Schema(description = "任务编号", example = "10") private Long jobId; @Schema(description = "处理器的名字,模糊匹配") private String handlerName; @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @Schema(description = "开始执行时间") private LocalDateTime beginTime; @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @Schema(description = "结束执行时间") private LocalDateTime endTime; @Schema(description = "任务状态,参见 JobLogStatusEnum 枚举") private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/job/vo/log/JobLogRespVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.job.vo.log; import co.yixiang.yshop.framework.excel.core.annotations.DictFormat; import co.yixiang.yshop.framework.excel.core.convert.DictConvert; import co.yixiang.yshop.module.infra.enums.DictTypeConstants; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 定时任务日志 Response VO") @Data @ExcelIgnoreUnannotated public class JobLogRespVO { @Schema(description = "日志编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @ExcelProperty("日志编号") private Long id; @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @ExcelProperty("任务编号") private Long jobId; @Schema(description = "处理器的名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "sysUserSessionTimeoutJob") @ExcelProperty("处理器的名字") private String handlerName; @Schema(description = "处理器的参数", example = "yshop") @ExcelProperty("处理器的参数") private String handlerParam; @Schema(description = "第几次执行", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty("第几次执行") private Integer executeIndex; @Schema(description = "开始执行时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("开始执行时间") private LocalDateTime beginTime; @Schema(description = "结束执行时间") @ExcelProperty("结束执行时间") private LocalDateTime endTime; @Schema(description = "执行时长", example = "123") @ExcelProperty("执行时长") private Integer duration; @Schema(description = "任务状态,参见 JobLogStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty(value = "任务状态", converter = DictConvert.class) @DictFormat(DictTypeConstants.JOB_STATUS) private Integer status; @Schema(description = "结果数据", example = "执行成功") @ExcelProperty("结果数据") private String result; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("创建时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/logger/ApiAccessLogController.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.logger; import co.yixiang.yshop.framework.apilog.core.annotation.ApiAccessLog; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; import co.yixiang.yshop.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogRespVO; import co.yixiang.yshop.module.infra.dal.dataobject.logger.ApiAccessLogDO; import co.yixiang.yshop.module.infra.service.logger.ApiAccessLogService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import java.io.IOException; import java.util.List; import static co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - API 访问日志") @RestController @RequestMapping("/infra/api-access-log") @Validated public class ApiAccessLogController { @Resource private ApiAccessLogService apiAccessLogService; @GetMapping("/page") @Operation(summary = "获得API 访问日志分页") @PreAuthorize("@ss.hasPermission('infra:api-access-log:query')") public CommonResult> getApiAccessLogPage(@Valid ApiAccessLogPageReqVO pageReqVO) { PageResult pageResult = apiAccessLogService.getApiAccessLogPage(pageReqVO); return success(BeanUtils.toBean(pageResult, ApiAccessLogRespVO.class)); } @GetMapping("/export-excel") @Operation(summary = "导出API 访问日志 Excel") @PreAuthorize("@ss.hasPermission('infra:api-access-log:export')") @ApiAccessLog(operateType = EXPORT) public void exportApiAccessLogExcel(@Valid ApiAccessLogPageReqVO exportReqVO, HttpServletResponse response) throws IOException { exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = apiAccessLogService.getApiAccessLogPage(exportReqVO).getList(); // 导出 Excel ExcelUtils.write(response, "API 访问日志.xls", "数据", ApiAccessLogRespVO.class, BeanUtils.toBean(list, ApiAccessLogRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/logger/ApiErrorLogController.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.logger; import co.yixiang.yshop.framework.apilog.core.annotation.ApiAccessLog; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; import co.yixiang.yshop.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogRespVO; import co.yixiang.yshop.module.infra.dal.dataobject.logger.ApiErrorLogDO; import co.yixiang.yshop.module.infra.service.logger.ApiErrorLogService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import java.io.IOException; import java.util.List; import static co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @Tag(name = "管理后台 - API 错误日志") @RestController @RequestMapping("/infra/api-error-log") @Validated public class ApiErrorLogController { @Resource private ApiErrorLogService apiErrorLogService; @PutMapping("/update-status") @Operation(summary = "更新 API 错误日志的状态") @Parameters({ @Parameter(name = "id", description = "编号", required = true, example = "1024"), @Parameter(name = "processStatus", description = "处理状态", required = true, example = "1") }) @PreAuthorize("@ss.hasPermission('infra:api-error-log:update-status')") public CommonResult updateApiErrorLogProcess(@RequestParam("id") Long id, @RequestParam("processStatus") Integer processStatus) { apiErrorLogService.updateApiErrorLogProcess(id, processStatus, getLoginUserId()); return success(true); } @GetMapping("/page") @Operation(summary = "获得 API 错误日志分页") @PreAuthorize("@ss.hasPermission('infra:api-error-log:query')") public CommonResult> getApiErrorLogPage(@Valid ApiErrorLogPageReqVO pageReqVO) { PageResult pageResult = apiErrorLogService.getApiErrorLogPage(pageReqVO); return success(BeanUtils.toBean(pageResult, ApiErrorLogRespVO.class)); } @GetMapping("/export-excel") @Operation(summary = "导出 API 错误日志 Excel") @PreAuthorize("@ss.hasPermission('infra:api-error-log:export')") @ApiAccessLog(operateType = EXPORT) public void exportApiErrorLogExcel(@Valid ApiErrorLogPageReqVO exportReqVO, HttpServletResponse response) throws IOException { exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = apiErrorLogService.getApiErrorLogPage(exportReqVO).getList(); // 导出 Excel ExcelUtils.write(response, "API 错误日志.xls", "数据", ApiErrorLogRespVO.class, BeanUtils.toBean(list, ApiErrorLogRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogPageReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.logger.vo.apiaccesslog; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - API 访问日志分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ApiAccessLogPageReqVO extends PageParam { @Schema(description = "用户编号", example = "666") private Long userId; @Schema(description = "用户类型", example = "2") private Integer userType; @Schema(description = "应用名", example = "dashboard") private String applicationName; @Schema(description = "请求地址,模糊匹配", example = "/xxx/yyy") private String requestUrl; @Schema(description = "开始时间", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] beginTime; @Schema(description = "执行时长,大于等于,单位:毫秒", example = "100") private Integer duration; @Schema(description = "结果码", example = "0") private Integer resultCode; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.logger.vo.apiaccesslog; import co.yixiang.yshop.framework.excel.core.annotations.DictFormat; import co.yixiang.yshop.framework.excel.core.convert.DictConvert; import co.yixiang.yshop.module.system.enums.DictTypeConstants; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - API 访问日志 Response VO") @Data @ExcelIgnoreUnannotated public class ApiAccessLogRespVO { @Schema(description = "日志主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @ExcelProperty("日志主键") private Long id; @Schema(description = "链路追踪编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "66600cb6-7852-11eb-9439-0242ac130002") @ExcelProperty("链路追踪编号") private String traceId; @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") @ExcelProperty("用户编号") private Long userId; @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") @ExcelProperty(value = "用户类型", converter = DictConvert.class) @DictFormat(DictTypeConstants.USER_TYPE) private Integer userType; @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "dashboard") @ExcelProperty("应用名") private String applicationName; @Schema(description = "请求方法名", requiredMode = Schema.RequiredMode.REQUIRED, example = "GET") @ExcelProperty("请求方法名") private String requestMethod; @Schema(description = "请求地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "/xxx/yyy") @ExcelProperty("请求地址") private String requestUrl; @Schema(description = "请求参数") @ExcelProperty("请求参数") private String requestParams; @Schema(description = "响应结果") @ExcelProperty("响应结果") private String responseBody; @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") @ExcelProperty("用户 IP") private String userIp; @Schema(description = "浏览器 UA", requiredMode = Schema.RequiredMode.REQUIRED, example = "Mozilla/5.0") @ExcelProperty("浏览器 UA") private String userAgent; @Schema(description = "操作模块", requiredMode = Schema.RequiredMode.REQUIRED, example = "商品模块") @ExcelProperty("操作模块") private String operateModule; @Schema(description = "操作名", requiredMode = Schema.RequiredMode.REQUIRED, example = "创建商品") @ExcelProperty("操作名") private String operateName; @Schema(description = "操作分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty(value = "操作分类", converter = DictConvert.class) @DictFormat(co.yixiang.yshop.module.infra.enums.DictTypeConstants.OPERATE_TYPE) private Integer operateType; @Schema(description = "开始请求时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("开始请求时间") private LocalDateTime beginTime; @Schema(description = "结束请求时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("结束请求时间") private LocalDateTime endTime; @Schema(description = "执行时长", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") @ExcelProperty("执行时长") private Integer duration; @Schema(description = "结果码", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") @ExcelProperty("结果码") private Integer resultCode; @Schema(description = "结果提示", example = "yshop,牛逼!") @ExcelProperty("结果提示") private String resultMsg; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogPageReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.logger.vo.apierrorlog; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - API 错误日志分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ApiErrorLogPageReqVO extends PageParam { @Schema(description = "用户编号", example = "666") private Long userId; @Schema(description = "用户类型", example = "1") private Integer userType; @Schema(description = "应用名", example = "dashboard") private String applicationName; @Schema(description = "请求地址", example = "/xx/yy") private String requestUrl; @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @Schema(description = "异常发生时间") private LocalDateTime[] exceptionTime; @Schema(description = "处理状态", example = "0") private Integer processStatus; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.logger.vo.apierrorlog; import co.yixiang.yshop.framework.excel.core.annotations.DictFormat; import co.yixiang.yshop.framework.excel.core.convert.DictConvert; import co.yixiang.yshop.module.infra.enums.DictTypeConstants; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - API 错误日志 Response VO") @Data @ExcelIgnoreUnannotated public class ApiErrorLogRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @ExcelProperty("编号") private Integer id; @Schema(description = "链路追踪编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "66600cb6-7852-11eb-9439-0242ac130002") @ExcelProperty("链路追踪编号") private String traceId; @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") @ExcelProperty("用户编号") private Integer userId; @Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty(value = "用户类型", converter = DictConvert.class) @DictFormat(co.yixiang.yshop.module.system.enums.DictTypeConstants.USER_TYPE) private Integer userType; @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "dashboard") @ExcelProperty("应用名") private String applicationName; @Schema(description = "请求方法名", requiredMode = Schema.RequiredMode.REQUIRED, example = "GET") @ExcelProperty("请求方法名") private String requestMethod; @Schema(description = "请求地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "/xx/yy") @ExcelProperty("请求地址") private String requestUrl; @Schema(description = "请求参数", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("请求参数") private String requestParams; @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") @ExcelProperty("用户 IP") private String userIp; @Schema(description = "浏览器 UA", requiredMode = Schema.RequiredMode.REQUIRED, example = "Mozilla/5.0") @ExcelProperty("浏览器 UA") private String userAgent; @Schema(description = "异常发生时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("异常发生时间") private LocalDateTime exceptionTime; @Schema(description = "异常名", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("异常名") private String exceptionName; @Schema(description = "异常导致的消息", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("异常导致的消息") private String exceptionMessage; @Schema(description = "异常导致的根消息", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("异常导致的根消息") private String exceptionRootCauseMessage; @Schema(description = "异常的栈轨迹", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("异常的栈轨迹") private String exceptionStackTrace; @Schema(description = "异常发生的类全名", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("异常发生的类全名") private String exceptionClassName; @Schema(description = "异常发生的类文件", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("异常发生的类文件") private String exceptionFileName; @Schema(description = "异常发生的方法名", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("异常发生的方法名") private String exceptionMethodName; @Schema(description = "异常发生的方法所在行", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("异常发生的方法所在行") private Integer exceptionLineNumber; @Schema(description = "处理状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") @ExcelProperty(value = "处理状态", converter = DictConvert.class) @DictFormat(DictTypeConstants.API_ERROR_LOG_PROCESS_STATUS) private Integer processStatus; @Schema(description = "处理时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("处理时间") private LocalDateTime processTime; @Schema(description = "处理用户编号", example = "233") @ExcelProperty("处理用户编号") private Integer processUserId; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("创建时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/redis/RedisController.http ================================================ ### 请求 /infra/redis/get-monitor-info 接口 => 成功 GET {{baseUrl}}/infra/redis/get-monitor-info Authorization: Bearer {{token}} tenant-id: {{adminTenentId}} ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/redis/RedisController.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.redis; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.module.infra.controller.admin.redis.vo.RedisMonitorRespVO; import co.yixiang.yshop.module.infra.convert.redis.RedisConvert; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.data.redis.connection.RedisServerCommands; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import jakarta.annotation.Resource; import java.util.Properties; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - Redis 监控") @RestController @RequestMapping("/infra/redis") public class RedisController { @Resource private StringRedisTemplate stringRedisTemplate; @GetMapping("/get-monitor-info") @Operation(summary = "获得 Redis 监控信息") @PreAuthorize("@ss.hasPermission('infra:redis:get-monitor-info')") public CommonResult getRedisMonitorInfo() { // 获得 Redis 统计信息 Properties info = stringRedisTemplate.execute((RedisCallback) RedisServerCommands::info); Long dbSize = stringRedisTemplate.execute(RedisServerCommands::dbSize); Properties commandStats = stringRedisTemplate.execute(( RedisCallback) connection -> connection.serverCommands().info("commandstats")); assert commandStats != null; // 断言,避免警告 // 拼接结果返回 return success(RedisConvert.INSTANCE.build(info, dbSize, commandStats)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/admin/redis/vo/RedisMonitorRespVO.java ================================================ package co.yixiang.yshop.module.infra.controller.admin.redis.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import java.util.List; import java.util.Properties; @Schema(description = "管理后台 - Redis 监控信息 Response VO") @Data @Builder @AllArgsConstructor public class RedisMonitorRespVO { @Schema(description = "Redis info 指令结果,具体字段,查看 Redis 文档", requiredMode = Schema.RequiredMode.REQUIRED) private Properties info; @Schema(description = "Redis key 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long dbSize; @Schema(description = "CommandStat 数组", requiredMode = Schema.RequiredMode.REQUIRED) private List commandStats; @Schema(description = "Redis 命令统计结果") @Data @Builder @AllArgsConstructor public static class CommandStat { @Schema(description = "Redis 命令", requiredMode = Schema.RequiredMode.REQUIRED, example = "get") private String command; @Schema(description = "调用次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long calls; @Schema(description = "消耗 CPU 秒数", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") private Long usec; } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/app/file/AppFileController.java ================================================ package co.yixiang.yshop.module.infra.controller.app.file; import cn.hutool.core.io.IoUtil; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.module.infra.controller.app.file.vo.AppFileUploadReqVO; import co.yixiang.yshop.module.infra.service.file.FileService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; 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.RestController; import org.springframework.web.multipart.MultipartFile; import jakarta.annotation.Resource; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "用户 App - 文件存储") @RestController @RequestMapping("/infra/file") @Validated @Slf4j public class AppFileController { @Resource private FileService fileService; @PostMapping("/upload") @Operation(summary = "上传文件") public CommonResult uploadFile(AppFileUploadReqVO uploadReqVO) throws Exception { MultipartFile file = uploadReqVO.getFile(); String path = uploadReqVO.getPath(); return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream()))); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/controller/app/file/vo/AppFileUploadReqVO.java ================================================ package co.yixiang.yshop.module.infra.controller.app.file.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.springframework.web.multipart.MultipartFile; import jakarta.validation.constraints.NotNull; @Schema(description = "用户 App - 上传文件 Request VO") @Data public class AppFileUploadReqVO { @Schema(description = "文件附件", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "文件附件不能为空") private MultipartFile file; @Schema(description = "文件附件", example = "yshopyuanma.png") private String path; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/convert/codegen/CodegenConvert.java ================================================ package co.yixiang.yshop.module.infra.convert.codegen; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.CodegenDetailRespVO; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.CodegenPreviewRespVO; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.column.CodegenColumnRespVO; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.table.CodegenTableRespVO; import co.yixiang.yshop.module.infra.dal.dataobject.codegen.CodegenColumnDO; import co.yixiang.yshop.module.infra.dal.dataobject.codegen.CodegenTableDO; import com.baomidou.mybatisplus.generator.config.po.TableField; import com.baomidou.mybatisplus.generator.config.po.TableInfo; import org.apache.ibatis.type.JdbcType; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings; import org.mapstruct.Named; import org.mapstruct.factory.Mappers; import java.util.List; import java.util.Map; @Mapper public interface CodegenConvert { CodegenConvert INSTANCE = Mappers.getMapper(CodegenConvert.class); // ========== TableInfo 相关 ========== @Mappings({ @Mapping(source = "name", target = "tableName"), @Mapping(source = "comment", target = "tableComment"), }) CodegenTableDO convert(TableInfo bean); List convertList(List list); @Mappings({ @Mapping(source = "name", target = "columnName"), @Mapping(source = "metaInfo.jdbcType", target = "dataType", qualifiedByName = "getDataType"), @Mapping(source = "comment", target = "columnComment"), @Mapping(source = "metaInfo.nullable", target = "nullable"), @Mapping(source = "keyFlag", target = "primaryKey"), @Mapping(source = "columnType.type", target = "javaType"), @Mapping(source = "propertyName", target = "javaField"), }) CodegenColumnDO convert(TableField bean); @Named("getDataType") default String getDataType(JdbcType jdbcType) { return jdbcType.name(); } // ========== 其它 ========== default CodegenDetailRespVO convert(CodegenTableDO table, List columns) { CodegenDetailRespVO respVO = new CodegenDetailRespVO(); respVO.setTable(BeanUtils.toBean(table, CodegenTableRespVO.class)); respVO.setColumns(BeanUtils.toBean(columns, CodegenColumnRespVO.class)); return respVO; } default List convert(Map codes) { return CollectionUtils.convertList(codes.entrySet(), entry -> new CodegenPreviewRespVO().setFilePath(entry.getKey()).setCode(entry.getValue())); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/convert/config/ConfigConvert.java ================================================ package co.yixiang.yshop.module.infra.convert.config; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.infra.controller.admin.config.vo.ConfigRespVO; import co.yixiang.yshop.module.infra.controller.admin.config.vo.ConfigSaveReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.config.ConfigDO; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.factory.Mappers; import java.util.List; @Mapper public interface ConfigConvert { ConfigConvert INSTANCE = Mappers.getMapper(ConfigConvert.class); PageResult convertPage(PageResult page); List convertList(List list); @Mapping(source = "configKey", target = "key") ConfigRespVO convert(ConfigDO bean); @Mapping(source = "key", target = "configKey") ConfigDO convert(ConfigSaveReqVO bean); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/convert/file/FileConfigConvert.java ================================================ package co.yixiang.yshop.module.infra.convert.file; import co.yixiang.yshop.module.infra.controller.admin.file.vo.config.FileConfigSaveReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.file.FileConfigDO; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.factory.Mappers; /** * 文件配置 Convert * * @author yshop */ @Mapper public interface FileConfigConvert { FileConfigConvert INSTANCE = Mappers.getMapper(FileConfigConvert.class); @Mapping(target = "config", ignore = true) FileConfigDO convert(FileConfigSaveReqVO bean); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/convert/redis/RedisConvert.java ================================================ package co.yixiang.yshop.module.infra.convert.redis; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.module.infra.controller.admin.redis.vo.RedisMonitorRespVO; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import java.util.ArrayList; import java.util.Properties; @Mapper public interface RedisConvert { RedisConvert INSTANCE = Mappers.getMapper(RedisConvert.class); default RedisMonitorRespVO build(Properties info, Long dbSize, Properties commandStats) { RedisMonitorRespVO respVO = RedisMonitorRespVO.builder().info(info).dbSize(dbSize) .commandStats(new ArrayList<>(commandStats.size())).build(); commandStats.forEach((key, value) -> { respVO.getCommandStats().add(RedisMonitorRespVO.CommandStat.builder() .command(StrUtil.subAfter((String) key, "cmdstat_", false)) .calls(Long.valueOf(StrUtil.subBetween((String) value, "calls=", ","))) .usec(Long.valueOf(StrUtil.subBetween((String) value, "usec=", ","))) .build()); }); return respVO; } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/dataobject/codegen/CodegenColumnDO.java ================================================ package co.yixiang.yshop.module.infra.dal.dataobject.codegen; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.infra.enums.codegen.CodegenColumnHtmlTypeEnum; import co.yixiang.yshop.module.infra.enums.codegen.CodegenColumnListConditionEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.generator.config.po.TableField; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; /** * 代码生成 column 字段定义 * * @author yshop */ @TableName(value = "infra_codegen_column", autoResultMap = true) @KeySequence("infra_codegen_column_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @Accessors(chain = true) @EqualsAndHashCode(callSuper = true) public class CodegenColumnDO extends BaseDO { /** * ID 编号 */ @TableId private Long id; /** * 表编号 *

* 关联 {@link CodegenTableDO#getId()} */ private Long tableId; // ========== 表相关字段 ========== /** * 字段名 * * 关联 {@link TableField#getName()} */ private String columnName; /** * 数据库字段类型 * * 关联 {@link TableField.MetaInfo#getJdbcType()} */ private String dataType; /** * 字段描述 * * 关联 {@link TableField#getComment()} */ private String columnComment; /** * 是否允许为空 * * 关联 {@link TableField.MetaInfo#isNullable()} */ private Boolean nullable; /** * 是否主键 * * 关联 {@link TableField#isKeyFlag()} */ private Boolean primaryKey; /** * 排序 */ private Integer ordinalPosition; // ========== Java 相关字段 ========== /** * Java 属性类型 * * 例如说 String、Boolean 等等 * * 关联 {@link TableField#getColumnType()} */ private String javaType; /** * Java 属性名 * * 关联 {@link TableField#getPropertyName()} */ private String javaField; /** * 字典类型 *

* 关联 DictTypeDO 的 type 属性 */ private String dictType; /** * 数据示例,主要用于生成 Swagger 注解的 example 字段 */ private String example; // ========== CRUD 相关字段 ========== /** * 是否为 Create 创建操作的字段 */ private Boolean createOperation; /** * 是否为 Update 更新操作的字段 */ private Boolean updateOperation; /** * 是否为 List 查询操作的字段 */ private Boolean listOperation; /** * List 查询操作的条件类型 *

* 枚举 {@link CodegenColumnListConditionEnum} */ private String listOperationCondition; /** * 是否为 List 查询操作的返回字段 */ private Boolean listOperationResult; // ========== UI 相关字段 ========== /** * 显示类型 *

* 枚举 {@link CodegenColumnHtmlTypeEnum} */ private String htmlType; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/dataobject/codegen/CodegenTableDO.java ================================================ package co.yixiang.yshop.module.infra.dal.dataobject.codegen; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.infra.dal.dataobject.db.DataSourceConfigDO; import co.yixiang.yshop.module.infra.enums.codegen.CodegenFrontTypeEnum; import co.yixiang.yshop.module.infra.enums.codegen.CodegenSceneEnum; import co.yixiang.yshop.module.infra.enums.codegen.CodegenTemplateTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.generator.config.po.TableInfo; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; /** * 代码生成 table 表定义 * * @author yshop */ @TableName(value = "infra_codegen_table", autoResultMap = true) @KeySequence("infra_codegen_table_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @Accessors(chain = true) @EqualsAndHashCode(callSuper = true) public class CodegenTableDO extends BaseDO { /** * ID 编号 */ @TableId private Long id; /** * 数据源编号 * * 关联 {@link DataSourceConfigDO#getId()} */ private Long dataSourceConfigId; /** * 生成场景 * * 枚举 {@link CodegenSceneEnum} */ private Integer scene; // ========== 表相关字段 ========== /** * 表名称 * * 关联 {@link TableInfo#getName()} */ private String tableName; /** * 表描述 * * 关联 {@link TableInfo#getComment()} */ private String tableComment; /** * 备注 */ private String remark; // ========== 类相关字段 ========== /** * 模块名,即一级目录 * * 例如说,system、infra、tool 等等 */ private String moduleName; /** * 业务名,即二级目录 * * 例如说,user、permission、dict 等等 */ private String businessName; /** * 类名称(首字母大写) * * 例如说,SysUser、SysMenu、SysDictData 等等 */ private String className; /** * 类描述 */ private String classComment; /** * 作者 */ private String author; // ========== 生成相关字段 ========== /** * 模板类型 * * 枚举 {@link CodegenTemplateTypeEnum} */ private Integer templateType; /** * 代码生成的前端类型 * * 枚举 {@link CodegenFrontTypeEnum} */ private Integer frontType; // ========== 菜单相关字段 ========== /** * 父菜单编号 * * 关联 MenuDO 的 id 属性 */ private Long parentMenuId; // ========== 主子表相关字段 ========== /** * 主表的编号 * * 关联 {@link CodegenTableDO#getId()} */ private Long masterTableId; /** * 【自己】子表关联主表的字段编号 * * 关联 {@link CodegenColumnDO#getId()} */ private Long subJoinColumnId; /** * 主表与子表是否一对多 * * true:一对多 * false:一对一 */ private Boolean subJoinMany; // ========== 树表相关字段 ========== /** * 树表的父字段编号 * * 关联 {@link CodegenColumnDO#getId()} */ private Long treeParentColumnId; /** * 树表的名字字段编号 * * 名字的用途:新增或修改时,select 框展示的字段 * * 关联 {@link CodegenColumnDO#getId()} */ private Long treeNameColumnId; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/dataobject/config/ConfigDO.java ================================================ package co.yixiang.yshop.module.infra.dal.dataobject.config; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.infra.enums.config.ConfigTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; /** * 参数配置表 * * @author yshop */ @TableName("infra_config") @KeySequence("infra_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ConfigDO extends BaseDO { /** * 参数主键 */ @TableId private Long id; /** * 参数分类 */ private String category; /** * 参数名称 */ private String name; /** * 参数键名 * * 支持多 DB 类型时,无法直接使用 key + @TableField("config_key") 来实现转换,原因是 "config_key" AS key 而存在报错 */ private String configKey; /** * 参数键值 */ private String value; /** * 参数类型 * * 枚举 {@link ConfigTypeEnum} */ private Integer type; /** * 是否可见 * * 不可见的参数,一般是敏感参数,前端不可获取 */ private Boolean visible; /** * 备注 */ private String remark; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/dataobject/db/DataSourceConfigDO.java ================================================ package co.yixiang.yshop.module.infra.dal.dataobject.db; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.framework.mybatis.core.type.EncryptTypeHandler; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; /** * 数据源配置 * * @author yshop */ @TableName(value = "infra_data_source_config", autoResultMap = true) @KeySequence("infra_data_source_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data public class DataSourceConfigDO extends BaseDO { /** * 主键编号 - Master 数据源 */ public static final Long ID_MASTER = 0L; /** * 主键编号 */ private Long id; /** * 连接名 */ private String name; /** * 数据源连接 */ private String url; /** * 用户名 */ private String username; /** * 密码 */ @TableField(typeHandler = EncryptTypeHandler.class) private String password; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/dataobject/demo/demo01/Demo01ContactDO.java ================================================ package co.yixiang.yshop.module.infra.dal.dataobject.demo.demo01; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; import java.time.LocalDateTime; /** * 示例联系人 DO * * @author yshop */ @TableName("yshop_demo01_contact") @KeySequence("yshop_demo01_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class Demo01ContactDO extends BaseDO { /** * 编号 */ @TableId private Long id; /** * 名字 */ private String name; /** * 性别 * * 枚举 {@link TODO system_user_sex 对应的类} */ private Integer sex; /** * 出生年 */ private LocalDateTime birthday; /** * 简介 */ private String description; /** * 头像 */ private String avatar; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/dataobject/demo/demo02/Demo02CategoryDO.java ================================================ package co.yixiang.yshop.module.infra.dal.dataobject.demo.demo02; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; /** * 示例分类 DO * * @author yshop */ @TableName("yshop_demo02_category") @KeySequence("yshop_demo02_category_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class Demo02CategoryDO extends BaseDO { public static final Long PARENT_ID_ROOT = 0L; /** * 编号 */ @TableId private Long id; /** * 名字 */ private String name; /** * 父级编号 */ private Long parentId; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/dataobject/demo/demo03/Demo03CourseDO.java ================================================ package co.yixiang.yshop.module.infra.dal.dataobject.demo.demo03; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; /** * 学生课程 DO * * @author yshop */ @TableName("yshop_demo03_course") @KeySequence("yshop_demo03_course_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class Demo03CourseDO extends BaseDO { /** * 编号 */ @TableId private Long id; /** * 学生编号 */ private Long studentId; /** * 名字 */ private String name; /** * 分数 */ private Integer score; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/dataobject/demo/demo03/Demo03GradeDO.java ================================================ package co.yixiang.yshop.module.infra.dal.dataobject.demo.demo03; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; /** * 学生班级 DO * * @author yshop */ @TableName("yshop_demo03_grade") @KeySequence("yshop_demo03_grade_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class Demo03GradeDO extends BaseDO { /** * 编号 */ @TableId private Long id; /** * 学生编号 */ private Long studentId; /** * 名字 */ private String name; /** * 班主任 */ private String teacher; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/dataobject/demo/demo03/Demo03StudentDO.java ================================================ package co.yixiang.yshop.module.infra.dal.dataobject.demo.demo03; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; import java.time.LocalDateTime; /** * 学生 DO * * @author yshop */ @TableName("yshop_demo03_student") @KeySequence("yshop_demo03_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class Demo03StudentDO extends BaseDO { /** * 编号 */ @TableId private Long id; /** * 名字 */ private String name; /** * 性别 * * 枚举 {@link TODO system_user_sex 对应的类} */ private Integer sex; /** * 出生日期 */ private LocalDateTime birthday; /** * 简介 */ private String description; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/dataobject/file/FileConfigDO.java ================================================ package co.yixiang.yshop.module.infra.dal.dataobject.file; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.infra.framework.file.core.client.FileClientConfig; import co.yixiang.yshop.module.infra.framework.file.core.client.db.DBFileClientConfig; import co.yixiang.yshop.module.infra.framework.file.core.client.ftp.FtpFileClientConfig; import co.yixiang.yshop.module.infra.framework.file.core.client.local.LocalFileClientConfig; import co.yixiang.yshop.module.infra.framework.file.core.client.s3.S3FileClientConfig; import co.yixiang.yshop.module.infra.framework.file.core.client.sftp.SftpFileClientConfig; import co.yixiang.yshop.module.infra.framework.file.core.enums.FileStorageEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; import com.fasterxml.jackson.core.type.TypeReference; import lombok.*; /** * 文件配置表 * * @author yshop */ @TableName(value = "infra_file_config", autoResultMap = true) @KeySequence("infra_file_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class FileConfigDO extends BaseDO { /** * 配置编号,数据库自增 */ private Long id; /** * 配置名 */ private String name; /** * 存储器 * * 枚举 {@link FileStorageEnum} */ private Integer storage; /** * 备注 */ private String remark; /** * 是否为主配置 * * 由于我们可以配置多个文件配置,默认情况下,使用主配置进行文件的上传 */ private Boolean master; /** * 支付渠道配置 */ @TableField(typeHandler = FileClientConfigTypeHandler.class) private FileClientConfig config; public static class FileClientConfigTypeHandler extends AbstractJsonTypeHandler { @Override protected Object parse(String json) { FileClientConfig config = JsonUtils.parseObjectQuietly(json, new TypeReference<>() {}); if (config != null) { return config; } // 兼容老版本的包路径 String className = JsonUtils.parseObject(json, "@class", String.class); className = StrUtil.subAfter(className, ".", true); switch (className) { case "DBFileClientConfig": return JsonUtils.parseObject2(json, DBFileClientConfig.class); case "FtpFileClientConfig": return JsonUtils.parseObject2(json, FtpFileClientConfig.class); case "LocalFileClientConfig": return JsonUtils.parseObject2(json, LocalFileClientConfig.class); case "SftpFileClientConfig": return JsonUtils.parseObject2(json, SftpFileClientConfig.class); case "S3FileClientConfig": return JsonUtils.parseObject2(json, S3FileClientConfig.class); default: throw new IllegalArgumentException("未知的 FileClientConfig 类型:" + json); } } @Override protected String toJson(Object obj) { return JsonUtils.toJsonString(obj); } } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/dataobject/file/FileContentDO.java ================================================ package co.yixiang.yshop.module.infra.dal.dataobject.file; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.infra.framework.file.core.client.db.DBFileClient; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; /** * 文件内容表 * * 专门用于存储 {@link DBFileClient} 的文件内容 * * @author yshop */ @TableName("infra_file_content") @KeySequence("infra_file_content_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class FileContentDO extends BaseDO { /** * 编号,数据库自增 */ @TableId(type = IdType.INPUT) private String id; /** * 配置编号 * * 关联 {@link FileConfigDO#getId()} */ private Long configId; /** * 路径,即文件名 */ private String path; /** * 文件内容 */ private byte[] content; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/dataobject/file/FileDO.java ================================================ package co.yixiang.yshop.module.infra.dal.dataobject.file; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; /** * 文件表 * 每次文件上传,都会记录一条记录到该表中 * * @author yshop */ @TableName("infra_file") @KeySequence("infra_file_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class FileDO extends BaseDO { /** * 编号,数据库自增 */ private Long id; /** * 配置编号 * * 关联 {@link FileConfigDO#getId()} */ private Long configId; /** * 原文件名 */ private String name; /** * 路径,即文件名 */ private String path; /** * 访问地址 */ private String url; /** * 文件的 MIME 类型,例如 "application/octet-stream" */ private String type; /** * 文件大小 */ private Integer size; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/dataobject/job/JobDO.java ================================================ package co.yixiang.yshop.module.infra.dal.dataobject.job; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.infra.enums.job.JobStatusEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; /** * 定时任务 DO * * @author yshop */ @TableName("infra_job") @KeySequence("infra_job_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class JobDO extends BaseDO { /** * 任务编号 */ @TableId private Long id; /** * 任务名称 */ private String name; /** * 任务状态 * * 枚举 {@link JobStatusEnum} */ private Integer status; /** * 处理器的名字 */ private String handlerName; /** * 处理器的参数 */ private String handlerParam; /** * CRON 表达式 */ private String cronExpression; // ========== 重试相关字段 ========== /** * 重试次数 * 如果不重试,则设置为 0 */ private Integer retryCount; /** * 重试间隔,单位:毫秒 * 如果没有间隔,则设置为 0 */ private Integer retryInterval; // ========== 监控相关字段 ========== /** * 监控超时时间,单位:毫秒 * 为空时,表示不监控 * * 注意,这里的超时的目的,不是进行任务的取消,而是告警任务的执行时间过长 */ private Integer monitorTimeout; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/dataobject/job/JobLogDO.java ================================================ package co.yixiang.yshop.module.infra.dal.dataobject.job; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.framework.quartz.core.handler.JobHandler; import co.yixiang.yshop.module.infra.enums.job.JobLogStatusEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; import java.time.LocalDateTime; /** * 定时任务的执行日志 * * @author yshop */ @TableName("infra_job_log") @KeySequence("infra_job_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class JobLogDO extends BaseDO { /** * 日志编号 */ private Long id; /** * 任务编号 * * 关联 {@link JobDO#getId()} */ private Long jobId; /** * 处理器的名字 * * 冗余字段 {@link JobDO#getHandlerName()} */ private String handlerName; /** * 处理器的参数 * * 冗余字段 {@link JobDO#getHandlerParam()} */ private String handlerParam; /** * 第几次执行 * * 用于区分是不是重试执行。如果是重试执行,则 index 大于 1 */ private Integer executeIndex; /** * 开始执行时间 */ private LocalDateTime beginTime; /** * 结束执行时间 */ private LocalDateTime endTime; /** * 执行时长,单位:毫秒 */ private Integer duration; /** * 状态 * * 枚举 {@link JobLogStatusEnum} */ private Integer status; /** * 结果数据 * * 成功时,使用 {@link JobHandler#execute(String)} 的结果 * 失败时,使用 {@link JobHandler#execute(String)} 的异常堆栈 */ private String result; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/dataobject/logger/ApiAccessLogDO.java ================================================ package co.yixiang.yshop.module.infra.dal.dataobject.logger; import co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; import java.time.LocalDateTime; /** * API 访问日志 * * @author yshop */ @TableName("infra_api_access_log") @KeySequence(value = "infra_api_access_log_seq") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class ApiAccessLogDO extends BaseDO { /** * {@link #requestParams} 的最大长度 */ public static final Integer REQUEST_PARAMS_MAX_LENGTH = 8000; /** * {@link #resultMsg} 的最大长度 */ public static final Integer RESULT_MSG_MAX_LENGTH = 512; /** * 编号 */ @TableId private Long id; /** * 链路追踪编号 * * 一般来说,通过链路追踪编号,可以将访问日志,错误日志,链路追踪日志,logger 打印日志等,结合在一起,从而进行排错。 */ private String traceId; /** * 用户编号 */ private Long userId; /** * 用户类型 * * 枚举 {@link UserTypeEnum} */ private Integer userType; /** * 应用名 * * 目前读取 `spring.application.name` 配置项 */ private String applicationName; // ========== 请求相关字段 ========== /** * 请求方法名 */ private String requestMethod; /** * 访问地址 */ private String requestUrl; /** * 请求参数 * * query: Query String * body: Quest Body */ private String requestParams; /** * 响应结果 */ private String responseBody; /** * 用户 IP */ private String userIp; /** * 浏览器 UA */ private String userAgent; // ========== 执行相关字段 ========== /** * 操作模块 */ private String operateModule; /** * 操作名 */ private String operateName; /** * 操作分类 * * 枚举 {@link OperateTypeEnum} */ private Integer operateType; /** * 开始请求时间 */ private LocalDateTime beginTime; /** * 结束请求时间 */ private LocalDateTime endTime; /** * 执行时长,单位:毫秒 */ private Integer duration; /** * 结果码 * * 目前使用的 {@link CommonResult#getCode()} 属性 */ private Integer resultCode; /** * 结果提示 * * 目前使用的 {@link CommonResult#getMsg()} 属性 */ private String resultMsg; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/dataobject/logger/ApiErrorLogDO.java ================================================ package co.yixiang.yshop.module.infra.dal.dataobject.logger; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.infra.enums.logger.ApiErrorLogProcessStatusEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; import java.time.LocalDateTime; /** * API 异常数据 * * @author yshop */ @TableName("infra_api_error_log") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor @KeySequence(value = "infra_api_error_log_seq") public class ApiErrorLogDO extends BaseDO { /** * {@link #requestParams} 的最大长度 */ public static final Integer REQUEST_PARAMS_MAX_LENGTH = 8000; /** * 编号 */ @TableId private Long id; /** * 用户编号 */ private Long userId; /** * 链路追踪编号 * * 一般来说,通过链路追踪编号,可以将访问日志,错误日志,链路追踪日志,logger 打印日志等,结合在一起,从而进行排错。 */ private String traceId; /** * 用户类型 * * 枚举 {@link UserTypeEnum} */ private Integer userType; /** * 应用名 * * 目前读取 spring.application.name */ private String applicationName; // ========== 请求相关字段 ========== /** * 请求方法名 */ private String requestMethod; /** * 访问地址 */ private String requestUrl; /** * 请求参数 * * query: Query String * body: Quest Body */ private String requestParams; /** * 用户 IP */ private String userIp; /** * 浏览器 UA */ private String userAgent; // ========== 异常相关字段 ========== /** * 异常发生时间 */ private LocalDateTime exceptionTime; /** * 异常名 * * {@link Throwable#getClass()} 的类全名 */ private String exceptionName; /** * 异常导致的消息 * * {@link cn.hutool.core.exceptions.ExceptionUtil#getMessage(Throwable)} */ private String exceptionMessage; /** * 异常导致的根消息 * * {@link cn.hutool.core.exceptions.ExceptionUtil#getRootCauseMessage(Throwable)} */ private String exceptionRootCauseMessage; /** * 异常的栈轨迹 * * {@link org.apache.commons.lang3.exception.ExceptionUtils#getStackTrace(Throwable)} */ private String exceptionStackTrace; /** * 异常发生的类全名 * * {@link StackTraceElement#getClassName()} */ private String exceptionClassName; /** * 异常发生的类文件 * * {@link StackTraceElement#getFileName()} */ private String exceptionFileName; /** * 异常发生的方法名 * * {@link StackTraceElement#getMethodName()} */ private String exceptionMethodName; /** * 异常发生的方法所在行 * * {@link StackTraceElement#getLineNumber()} */ private Integer exceptionLineNumber; // ========== 处理相关字段 ========== /** * 处理状态 * * 枚举 {@link ApiErrorLogProcessStatusEnum} */ private Integer processStatus; /** * 处理时间 */ private LocalDateTime processTime; /** * 处理用户编号 * * 关联 co.yixiang.yshop.adminserver.modules.system.dal.dataobject.user.SysUserDO.SysUserDO#getId() */ private Long processUserId; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/mysql/codegen/CodegenColumnMapper.java ================================================ package co.yixiang.yshop.module.infra.dal.mysql.codegen; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.infra.dal.dataobject.codegen.CodegenColumnDO; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface CodegenColumnMapper extends BaseMapperX { default List selectListByTableId(Long tableId) { return selectList(new LambdaQueryWrapperX() .eq(CodegenColumnDO::getTableId, tableId) .orderByAsc(CodegenColumnDO::getId)); } default void deleteListByTableId(Long tableId) { delete(new LambdaQueryWrapperX() .eq(CodegenColumnDO::getTableId, tableId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/mysql/codegen/CodegenTableMapper.java ================================================ package co.yixiang.yshop.module.infra.dal.mysql.codegen; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.codegen.CodegenTableDO; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface CodegenTableMapper extends BaseMapperX { default CodegenTableDO selectByTableNameAndDataSourceConfigId(String tableName, Long dataSourceConfigId) { return selectOne(CodegenTableDO::getTableName, tableName, CodegenTableDO::getDataSourceConfigId, dataSourceConfigId); } default PageResult selectPage(CodegenTablePageReqVO pageReqVO) { return selectPage(pageReqVO, new LambdaQueryWrapperX() .likeIfPresent(CodegenTableDO::getTableName, pageReqVO.getTableName()) .likeIfPresent(CodegenTableDO::getTableComment, pageReqVO.getTableComment()) .likeIfPresent(CodegenTableDO::getClassName, pageReqVO.getClassName()) .betweenIfPresent(CodegenTableDO::getCreateTime, pageReqVO.getCreateTime()) .orderByDesc(CodegenTableDO::getUpdateTime) ); } default List selectListByDataSourceConfigId(Long dataSourceConfigId) { return selectList(CodegenTableDO::getDataSourceConfigId, dataSourceConfigId); } default List selectListByTemplateTypeAndMasterTableId(Integer templateType, Long masterTableId) { return selectList(CodegenTableDO::getTemplateType, templateType, CodegenTableDO::getMasterTableId, masterTableId); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/mysql/config/ConfigMapper.java ================================================ package co.yixiang.yshop.module.infra.dal.mysql.config; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.infra.controller.admin.config.vo.ConfigPageReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.config.ConfigDO; import org.apache.ibatis.annotations.Mapper; @Mapper public interface ConfigMapper extends BaseMapperX { default ConfigDO selectByKey(String key) { return selectOne(ConfigDO::getConfigKey, key); } default PageResult selectPage(ConfigPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(ConfigDO::getName, reqVO.getName()) .likeIfPresent(ConfigDO::getConfigKey, reqVO.getKey()) .eqIfPresent(ConfigDO::getType, reqVO.getType()) .betweenIfPresent(ConfigDO::getCreateTime, reqVO.getCreateTime())); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/mysql/db/DataSourceConfigMapper.java ================================================ package co.yixiang.yshop.module.infra.dal.mysql.db; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.infra.dal.dataobject.db.DataSourceConfigDO; import org.apache.ibatis.annotations.Mapper; /** * 数据源配置 Mapper * * @author yshop */ @Mapper public interface DataSourceConfigMapper extends BaseMapperX { } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/mysql/demo/demo01/Demo01ContactMapper.java ================================================ package co.yixiang.yshop.module.infra.dal.mysql.demo.demo01; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.infra.controller.admin.demo.demo01.vo.Demo01ContactPageReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo01.Demo01ContactDO; import org.apache.ibatis.annotations.Mapper; /** * 示例联系人 Mapper * * @author yshop */ @Mapper public interface Demo01ContactMapper extends BaseMapperX { default PageResult selectPage(Demo01ContactPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(Demo01ContactDO::getName, reqVO.getName()) .eqIfPresent(Demo01ContactDO::getSex, reqVO.getSex()) .betweenIfPresent(Demo01ContactDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(Demo01ContactDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/mysql/demo/demo02/Demo02CategoryMapper.java ================================================ package co.yixiang.yshop.module.infra.dal.mysql.demo.demo02; import java.util.*; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.infra.controller.admin.demo.demo02.vo.Demo02CategoryListReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo02.Demo02CategoryDO; import org.apache.ibatis.annotations.Mapper; /** * 示例分类 Mapper * * @author yshop */ @Mapper public interface Demo02CategoryMapper extends BaseMapperX { default List selectList(Demo02CategoryListReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .likeIfPresent(Demo02CategoryDO::getName, reqVO.getName()) .eqIfPresent(Demo02CategoryDO::getParentId, reqVO.getParentId()) .betweenIfPresent(Demo02CategoryDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(Demo02CategoryDO::getId)); } default Demo02CategoryDO selectByParentIdAndName(Long parentId, String name) { return selectOne(Demo02CategoryDO::getParentId, parentId, Demo02CategoryDO::getName, name); } default Long selectCountByParentId(Long parentId) { return selectCount(Demo02CategoryDO::getParentId, parentId); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/mysql/demo/demo03/Demo03CourseMapper.java ================================================ package co.yixiang.yshop.module.infra.dal.mysql.demo.demo03; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; import org.apache.ibatis.annotations.Mapper; import java.util.List; /** * 学生课程 Mapper * * @author yshop */ @Mapper public interface Demo03CourseMapper extends BaseMapperX { default PageResult selectPage(PageParam reqVO, Long studentId) { return selectPage(reqVO, new LambdaQueryWrapperX() .eq(Demo03CourseDO::getStudentId, studentId) .orderByDesc(Demo03CourseDO::getId)); } default List selectListByStudentId(Long studentId) { return selectList(Demo03CourseDO::getStudentId, studentId); } default int deleteByStudentId(Long studentId) { return delete(Demo03CourseDO::getStudentId, studentId); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/mysql/demo/demo03/Demo03GradeMapper.java ================================================ package co.yixiang.yshop.module.infra.dal.mysql.demo.demo03; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; import org.apache.ibatis.annotations.Mapper; /** * 学生班级 Mapper * * @author yshop */ @Mapper public interface Demo03GradeMapper extends BaseMapperX { default PageResult selectPage(PageParam reqVO, Long studentId) { return selectPage(reqVO, new LambdaQueryWrapperX() .eq(Demo03GradeDO::getStudentId, studentId) .orderByDesc(Demo03GradeDO::getId)); } default Demo03GradeDO selectByStudentId(Long studentId) { return selectOne(Demo03GradeDO::getStudentId, studentId); } default int deleteByStudentId(Long studentId) { return delete(Demo03GradeDO::getStudentId, studentId); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/mysql/demo/demo03/Demo03StudentMapper.java ================================================ package co.yixiang.yshop.module.infra.dal.mysql.demo.demo03; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO; import org.apache.ibatis.annotations.Mapper; import co.yixiang.yshop.module.infra.controller.admin.demo.demo03.vo.*; /** * 学生 Mapper * * @author yshop */ @Mapper public interface Demo03StudentMapper extends BaseMapperX { default PageResult selectPage(Demo03StudentPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(Demo03StudentDO::getName, reqVO.getName()) .eqIfPresent(Demo03StudentDO::getSex, reqVO.getSex()) .eqIfPresent(Demo03StudentDO::getDescription, reqVO.getDescription()) .betweenIfPresent(Demo03StudentDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(Demo03StudentDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/mysql/file/FileConfigMapper.java ================================================ package co.yixiang.yshop.module.infra.dal.mysql.file; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.file.FileConfigDO; import org.apache.ibatis.annotations.Mapper; @Mapper public interface FileConfigMapper extends BaseMapperX { default PageResult selectPage(FileConfigPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(FileConfigDO::getName, reqVO.getName()) .eqIfPresent(FileConfigDO::getStorage, reqVO.getStorage()) .betweenIfPresent(FileConfigDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(FileConfigDO::getId)); } default FileConfigDO selectByMaster() { return selectOne(FileConfigDO::getMaster, true); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/mysql/file/FileContentMapper.java ================================================ package co.yixiang.yshop.module.infra.dal.mysql.file; import co.yixiang.yshop.module.infra.dal.dataobject.file.FileContentDO; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface FileContentMapper extends BaseMapper { default void deleteByConfigIdAndPath(Long configId, String path) { this.delete(new LambdaQueryWrapper() .eq(FileContentDO::getConfigId, configId) .eq(FileContentDO::getPath, path)); } default List selectListByConfigIdAndPath(Long configId, String path) { return selectList(new LambdaQueryWrapper() .eq(FileContentDO::getConfigId, configId) .eq(FileContentDO::getPath, path)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/mysql/file/FileMapper.java ================================================ package co.yixiang.yshop.module.infra.dal.mysql.file; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.infra.controller.admin.file.vo.file.FilePageReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.file.FileDO; import org.apache.ibatis.annotations.Mapper; /** * 文件操作 Mapper * * @author yshop */ @Mapper public interface FileMapper extends BaseMapperX { default PageResult selectPage(FilePageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(FileDO::getPath, reqVO.getPath()) .likeIfPresent(FileDO::getType, reqVO.getType()) .betweenIfPresent(FileDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(FileDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/mysql/job/JobLogMapper.java ================================================ package co.yixiang.yshop.module.infra.dal.mysql.job; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.job.JobLogDO; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.time.LocalDateTime; /** * 任务日志 Mapper * * @author yshop */ @Mapper public interface JobLogMapper extends BaseMapperX { default PageResult selectPage(JobLogPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(JobLogDO::getJobId, reqVO.getJobId()) .likeIfPresent(JobLogDO::getHandlerName, reqVO.getHandlerName()) .geIfPresent(JobLogDO::getBeginTime, reqVO.getBeginTime()) .leIfPresent(JobLogDO::getEndTime, reqVO.getEndTime()) .eqIfPresent(JobLogDO::getStatus, reqVO.getStatus()) .orderByDesc(JobLogDO::getId) // ID 倒序 ); } /** * 物理删除指定时间之前的日志 * * @param createTime 最大时间 * @param limit 删除条数,防止一次删除太多 * @return 删除条数 */ @Delete("DELETE FROM infra_job_log WHERE create_time < #{createTime} LIMIT #{limit}") Integer deleteByCreateTimeLt(@Param("createTime") LocalDateTime createTime, @Param("limit") Integer limit); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/mysql/job/JobMapper.java ================================================ package co.yixiang.yshop.module.infra.dal.mysql.job; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.infra.controller.admin.job.vo.job.JobPageReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.job.JobDO; import org.apache.ibatis.annotations.Mapper; /** * 定时任务 Mapper * * @author yshop */ @Mapper public interface JobMapper extends BaseMapperX { default JobDO selectByHandlerName(String handlerName) { return selectOne(JobDO::getHandlerName, handlerName); } default PageResult selectPage(JobPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(JobDO::getName, reqVO.getName()) .eqIfPresent(JobDO::getStatus, reqVO.getStatus()) .likeIfPresent(JobDO::getHandlerName, reqVO.getHandlerName()) ); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/mysql/logger/ApiAccessLogMapper.java ================================================ package co.yixiang.yshop.module.infra.dal.mysql.logger; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.logger.ApiAccessLogDO; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.time.LocalDateTime; /** * API 访问日志 Mapper * * @author yshop */ @Mapper public interface ApiAccessLogMapper extends BaseMapperX { default PageResult selectPage(ApiAccessLogPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(ApiAccessLogDO::getUserId, reqVO.getUserId()) .eqIfPresent(ApiAccessLogDO::getUserType, reqVO.getUserType()) .eqIfPresent(ApiAccessLogDO::getApplicationName, reqVO.getApplicationName()) .likeIfPresent(ApiAccessLogDO::getRequestUrl, reqVO.getRequestUrl()) .betweenIfPresent(ApiAccessLogDO::getBeginTime, reqVO.getBeginTime()) .geIfPresent(ApiAccessLogDO::getDuration, reqVO.getDuration()) .eqIfPresent(ApiAccessLogDO::getResultCode, reqVO.getResultCode()) .orderByDesc(ApiAccessLogDO::getId) ); } /** * 物理删除指定时间之前的日志 * * @param createTime 最大时间 * @param limit 删除条数,防止一次删除太多 * @return 删除条数 */ @Delete("DELETE FROM infra_api_access_log WHERE create_time < #{createTime} LIMIT #{limit}") Integer deleteByCreateTimeLt(@Param("createTime") LocalDateTime createTime, @Param("limit") Integer limit); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/dal/mysql/logger/ApiErrorLogMapper.java ================================================ package co.yixiang.yshop.module.infra.dal.mysql.logger; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.logger.ApiErrorLogDO; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.time.LocalDateTime; /** * API 错误日志 Mapper * * @author yshop */ @Mapper public interface ApiErrorLogMapper extends BaseMapperX { default PageResult selectPage(ApiErrorLogPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(ApiErrorLogDO::getUserId, reqVO.getUserId()) .eqIfPresent(ApiErrorLogDO::getUserType, reqVO.getUserType()) .eqIfPresent(ApiErrorLogDO::getApplicationName, reqVO.getApplicationName()) .likeIfPresent(ApiErrorLogDO::getRequestUrl, reqVO.getRequestUrl()) .betweenIfPresent(ApiErrorLogDO::getExceptionTime, reqVO.getExceptionTime()) .eqIfPresent(ApiErrorLogDO::getProcessStatus, reqVO.getProcessStatus()) .orderByDesc(ApiErrorLogDO::getId) ); } /** * 物理删除指定时间之前的日志 * * @param createTime 最大时间 * @param limit 删除条数,防止一次删除太多 * @return 删除条数 */ @Delete("DELETE FROM infra_api_error_log WHERE create_time < #{createTime} LIMIT #{limit}") Integer deleteByCreateTimeLt(@Param("createTime") LocalDateTime createTime, @Param("limit")Integer limit); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/enums/codegen/CodegenColumnHtmlTypeEnum.java ================================================ package co.yixiang.yshop.module.infra.enums.codegen; import lombok.AllArgsConstructor; import lombok.Getter; /** * 代码生成器的字段 HTML 展示枚举 */ @AllArgsConstructor @Getter public enum CodegenColumnHtmlTypeEnum { INPUT("input"), // 文本框 TEXTAREA("textarea"), // 文本域 SELECT("select"), // 下拉框 RADIO("radio"), // 单选框 CHECKBOX("checkbox"), // 复选框 DATETIME("datetime"), // 日期控件 IMAGE_UPLOAD("imageUpload"), // 上传图片 FILE_UPLOAD("fileUpload"), // 上传文件 EDITOR("editor"), // 富文本控件 ; /** * 条件 */ private final String type; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/enums/codegen/CodegenColumnListConditionEnum.java ================================================ package co.yixiang.yshop.module.infra.enums.codegen; import lombok.AllArgsConstructor; import lombok.Getter; /** * 代码生成器的字段过滤条件枚举 */ @AllArgsConstructor @Getter public enum CodegenColumnListConditionEnum { EQ("="), NE("!="), GT(">"), GTE(">="), LT("<"), LTE("<="), LIKE("LIKE"), BETWEEN("BETWEEN"); /** * 条件 */ private final String condition; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/enums/codegen/CodegenFrontTypeEnum.java ================================================ package co.yixiang.yshop.module.infra.enums.codegen; import lombok.AllArgsConstructor; import lombok.Getter; /** * 代码生成的前端类型枚举 * * @author yshop */ @AllArgsConstructor @Getter public enum CodegenFrontTypeEnum { VUE2(10), // Vue2 Element UI 标准模版 VUE3(20), // Vue3 Element Plus 标准模版 VUE3_SCHEMA(21), // Vue3 Element Plus Schema 模版 VUE3_VBEN(30), // Vue3 VBEN 模版 ; /** * 类型 */ private final Integer type; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/enums/codegen/CodegenSceneEnum.java ================================================ package co.yixiang.yshop.module.infra.enums.codegen; import lombok.AllArgsConstructor; import lombok.Getter; import static cn.hutool.core.util.ArrayUtil.*; /** * 代码生成的场景枚举 * * @author yshop */ @AllArgsConstructor @Getter public enum CodegenSceneEnum { ADMIN(1, "管理后台", "admin", ""), APP(2, "用户 APP", "app", "App"); /** * 场景 */ private final Integer scene; /** * 场景名 */ private final String name; /** * 基础包名 */ private final String basePackage; /** * Controller 和 VO 类的前缀 */ private final String prefixClass; public static CodegenSceneEnum valueOf(Integer scene) { return firstMatch(sceneEnum -> sceneEnum.getScene().equals(scene), values()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/enums/codegen/CodegenTemplateTypeEnum.java ================================================ package co.yixiang.yshop.module.infra.enums.codegen; import co.yixiang.yshop.framework.common.util.object.ObjectUtils; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.Objects; /** * 代码生成模板类型 * * @author yshop */ @AllArgsConstructor @Getter public enum CodegenTemplateTypeEnum { ONE(1), // 单表(增删改查) TREE(2), // 树表(增删改查) MASTER_NORMAL(10), // 主子表 - 主表 - 普通模式 MASTER_ERP(11), // 主子表 - 主表 - ERP 模式 MASTER_INNER(12), // 主子表 - 主表 - 内嵌模式 SUB(15), // 主子表 - 子表 ; /** * 类型 */ private final Integer type; /** * 是否为主表 * * @param type 类型 * @return 是否主表 */ public static boolean isMaster(Integer type) { return ObjectUtils.equalsAny(type, MASTER_NORMAL.type, MASTER_ERP.type, MASTER_INNER.type); } /** * 是否为树表 * * @param type 类型 * @return 是否树表 */ public static boolean isTree(Integer type) { return Objects.equals(type, TREE.type); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/enums/config/ConfigTypeEnum.java ================================================ package co.yixiang.yshop.module.infra.enums.config; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor public enum ConfigTypeEnum { /** * 系统配置 */ SYSTEM(1), /** * 自定义配置 */ CUSTOM(2); private final Integer type; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/enums/job/JobLogStatusEnum.java ================================================ package co.yixiang.yshop.module.infra.enums.job; import lombok.AllArgsConstructor; import lombok.Getter; /** * 任务日志的状态枚举 * * @author yshop */ @Getter @AllArgsConstructor public enum JobLogStatusEnum { RUNNING(0), // 运行中 SUCCESS(1), // 成功 FAILURE(2); // 失败 /** * 状态 */ private final Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/enums/job/JobStatusEnum.java ================================================ package co.yixiang.yshop.module.infra.enums.job; import com.google.common.collect.Sets; import lombok.AllArgsConstructor; import lombok.Getter; import org.quartz.impl.jdbcjobstore.Constants; import java.util.Collections; import java.util.Set; /** * 任务状态的枚举 * * @author yshop */ @Getter @AllArgsConstructor public enum JobStatusEnum { /** * 初始化中 */ INIT(0, Collections.emptySet()), /** * 开启 */ NORMAL(1, Sets.newHashSet(Constants.STATE_WAITING, Constants.STATE_ACQUIRED, Constants.STATE_BLOCKED)), /** * 暂停 */ STOP(2, Sets.newHashSet(Constants.STATE_PAUSED, Constants.STATE_PAUSED_BLOCKED)); /** * 状态 */ private final Integer status; /** * 对应的 Quartz 触发器的状态集合 */ private final Set quartzStates; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/enums/logger/ApiErrorLogProcessStatusEnum.java ================================================ package co.yixiang.yshop.module.infra.enums.logger; import lombok.AllArgsConstructor; import lombok.Getter; /** * API 异常数据的处理状态 * * @author yshop */ @AllArgsConstructor @Getter public enum ApiErrorLogProcessStatusEnum { INIT(0, "未处理"), DONE(1, "已处理"), IGNORE(2, "已忽略"); /** * 状态 */ private final Integer status; /** * 资源类型名 */ private final String name; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/codegen/config/CodegenConfiguration.java ================================================ package co.yixiang.yshop.module.infra.framework.codegen.config; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(CodegenProperties.class) public class CodegenConfiguration { } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/codegen/config/CodegenProperties.java ================================================ package co.yixiang.yshop.module.infra.framework.codegen.config; import co.yixiang.yshop.module.infra.enums.codegen.CodegenFrontTypeEnum; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.util.Collection; @ConfigurationProperties(prefix = "yshop.codegen") @Validated @Data public class CodegenProperties { /** * 生成的 Java 代码的基础包 */ @NotNull(message = "Java 代码的基础包不能为空") private String basePackage; /** * 数据库名数组 */ @NotEmpty(message = "数据库不能为空") private Collection dbSchemas; /** * 代码生成的前端类型(默认) * * 枚举 {@link CodegenFrontTypeEnum#getType()} */ @NotNull(message = "代码生成的前端类型不能为空") private Integer frontType; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/file/config/YshopFileAutoConfiguration.java ================================================ package co.yixiang.yshop.module.infra.framework.file.config; import co.yixiang.yshop.module.infra.framework.file.core.client.FileClientFactory; import co.yixiang.yshop.module.infra.framework.file.core.client.FileClientFactoryImpl; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 文件配置类 * * @author yshop */ @Configuration(proxyBeanMethods = false) public class YshopFileAutoConfiguration { @Bean public FileClientFactory fileClientFactory() { return new FileClientFactoryImpl(); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/file/core/client/AbstractFileClient.java ================================================ package co.yixiang.yshop.module.infra.framework.file.core.client; import cn.hutool.core.util.StrUtil; import lombok.extern.slf4j.Slf4j; /** * 文件客户端的抽象类,提供模板方法,减少子类的冗余代码 * * @author yshop */ @Slf4j public abstract class AbstractFileClient implements FileClient { /** * 配置编号 */ private final Long id; /** * 文件配置 */ protected Config config; public AbstractFileClient(Long id, Config config) { this.id = id; this.config = config; } /** * 初始化 */ public final void init() { doInit(); log.debug("[init][配置({}) 初始化完成]", config); } /** * 自定义初始化 */ protected abstract void doInit(); public final void refresh(Config config) { // 判断是否更新 if (config.equals(this.config)) { return; } log.info("[refresh][配置({})发生变化,重新初始化]", config); this.config = config; // 初始化 this.init(); } @Override public Long getId() { return id; } /** * 格式化文件的 URL 访问地址 * 使用场景:local、ftp、db,通过 FileController 的 getFile 来获取文件内容 * * @param domain 自定义域名 * @param path 文件路径 * @return URL 访问地址 */ protected String formatFileUrl(String domain, String path) { return StrUtil.format("{}/admin-api/infra/file/{}/get/{}", domain, getId(), path); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/file/core/client/FileClient.java ================================================ package co.yixiang.yshop.module.infra.framework.file.core.client; import co.yixiang.yshop.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO; /** * 文件客户端 * * @author yshop */ public interface FileClient { /** * 获得客户端编号 * * @return 客户端编号 */ Long getId(); /** * 上传文件 * * @param content 文件流 * @param path 相对路径 * @return 完整路径,即 HTTP 访问地址 * @throws Exception 上传文件时,抛出 Exception 异常 */ String upload(byte[] content, String path, String type) throws Exception; /** * 删除文件 * * @param path 相对路径 * @throws Exception 删除文件时,抛出 Exception 异常 */ void delete(String path) throws Exception; /** * 获得文件的内容 * * @param path 相对路径 * @return 文件的内容 */ byte[] getContent(String path) throws Exception; /** * 获得文件预签名地址 * * @param path 相对路径 * @return 文件预签名地址 */ default FilePresignedUrlRespDTO getPresignedObjectUrl(String path) throws Exception { throw new UnsupportedOperationException("不支持的操作"); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/file/core/client/FileClientConfig.java ================================================ package co.yixiang.yshop.module.infra.framework.file.core.client; import com.fasterxml.jackson.annotation.JsonTypeInfo; /** * 文件客户端的配置 * 不同实现的客户端,需要不同的配置,通过子类来定义 * * @author yshop */ @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) // @JsonTypeInfo 注解的作用,Jackson 多态 // 1. 序列化到时数据库时,增加 @class 属性。 // 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型 public interface FileClientConfig { } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/file/core/client/FileClientFactory.java ================================================ package co.yixiang.yshop.module.infra.framework.file.core.client; import co.yixiang.yshop.module.infra.framework.file.core.enums.FileStorageEnum; public interface FileClientFactory { /** * 获得文件客户端 * * @param configId 配置编号 * @return 文件客户端 */ FileClient getFileClient(Long configId); /** * 创建文件客户端 * * @param configId 配置编号 * @param storage 存储器的枚举 {@link FileStorageEnum} * @param config 文件配置 */ void createOrUpdateFileClient(Long configId, Integer storage, Config config); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/file/core/client/FileClientFactoryImpl.java ================================================ package co.yixiang.yshop.module.infra.framework.file.core.client; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ReflectUtil; import co.yixiang.yshop.module.infra.framework.file.core.enums.FileStorageEnum; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * 文件客户端的工厂实现类 * * @author yshop */ @Slf4j public class FileClientFactoryImpl implements FileClientFactory { /** * 文件客户端 Map * key:配置编号 */ private final ConcurrentMap> clients = new ConcurrentHashMap<>(); @Override public FileClient getFileClient(Long configId) { AbstractFileClient client = clients.get(configId); if (client == null) { log.error("[getFileClient][配置编号({}) 找不到客户端]", configId); } return client; } @Override @SuppressWarnings("unchecked") public void createOrUpdateFileClient(Long configId, Integer storage, Config config) { AbstractFileClient client = (AbstractFileClient) clients.get(configId); if (client == null) { client = this.createFileClient(configId, storage, config); client.init(); clients.put(client.getId(), client); } else { client.refresh(config); } } @SuppressWarnings("unchecked") private AbstractFileClient createFileClient( Long configId, Integer storage, Config config) { FileStorageEnum storageEnum = FileStorageEnum.getByStorage(storage); Assert.notNull(storageEnum, String.format("文件配置(%s) 为空", storageEnum)); // 创建客户端 return (AbstractFileClient) ReflectUtil.newInstance(storageEnum.getClientClass(), configId, config); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/file/core/client/db/DBFileClient.java ================================================ package co.yixiang.yshop.module.infra.framework.file.core.client.db; import cn.hutool.core.collection.CollUtil; import cn.hutool.extra.spring.SpringUtil; import co.yixiang.yshop.module.infra.dal.dataobject.file.FileContentDO; import co.yixiang.yshop.module.infra.dal.mysql.file.FileContentMapper; import co.yixiang.yshop.module.infra.framework.file.core.client.AbstractFileClient; import java.util.Comparator; import java.util.List; /** * 基于 DB 存储的文件客户端的配置类 * * @author yshop */ public class DBFileClient extends AbstractFileClient { private FileContentMapper fileContentMapper; public DBFileClient(Long id, DBFileClientConfig config) { super(id, config); } @Override protected void doInit() { fileContentMapper = SpringUtil.getBean(FileContentMapper.class); } @Override public String upload(byte[] content, String path, String type) { FileContentDO contentDO = new FileContentDO().setConfigId(getId()) .setPath(path).setContent(content); fileContentMapper.insert(contentDO); // 拼接返回路径 return super.formatFileUrl(config.getDomain(), path); } @Override public void delete(String path) { fileContentMapper.deleteByConfigIdAndPath(getId(), path); } @Override public byte[] getContent(String path) { List list = fileContentMapper.selectListByConfigIdAndPath(getId(), path); if (CollUtil.isEmpty(list)) { return null; } // 排序后,拿 id 最大的,即最后上传的 list.sort(Comparator.comparing(FileContentDO::getId)); return CollUtil.getLast(list).getContent(); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/file/core/client/db/DBFileClientConfig.java ================================================ package co.yixiang.yshop.module.infra.framework.file.core.client.db; import co.yixiang.yshop.module.infra.framework.file.core.client.FileClientConfig; import lombok.Data; import org.hibernate.validator.constraints.URL; import jakarta.validation.constraints.NotEmpty; /** * 基于 DB 存储的文件客户端的配置类 * * @author yshop */ @Data public class DBFileClientConfig implements FileClientConfig { /** * 自定义域名 */ @NotEmpty(message = "domain 不能为空") @URL(message = "domain 必须是 URL 格式") private String domain; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/file/core/client/ftp/FtpFileClient.java ================================================ package co.yixiang.yshop.module.infra.framework.file.core.client.ftp; import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.CharsetUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.ftp.Ftp; import cn.hutool.extra.ftp.FtpException; import cn.hutool.extra.ftp.FtpMode; import co.yixiang.yshop.module.infra.framework.file.core.client.AbstractFileClient; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; /** * Ftp 文件客户端 * * @author yshop */ public class FtpFileClient extends AbstractFileClient { private Ftp ftp; public FtpFileClient(Long id, FtpFileClientConfig config) { super(id, config); } @Override protected void doInit() { // 把配置的 \ 替换成 /, 如果路径配置 \a\test, 替换成 /a/test, 替换方法已经处理 null 情况 config.setBasePath(StrUtil.replace(config.getBasePath(), StrUtil.BACKSLASH, StrUtil.SLASH)); // ftp的路径是 / 结尾 if (!config.getBasePath().endsWith(StrUtil.SLASH)) { config.setBasePath(config.getBasePath() + StrUtil.SLASH); } // 初始化 Ftp 对象 this.ftp = new Ftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(), CharsetUtil.CHARSET_UTF_8, null, null, FtpMode.valueOf(config.getMode())); } @Override public String upload(byte[] content, String path, String type) { // 执行写入 String filePath = getFilePath(path); String fileName = FileUtil.getName(filePath); String dir = StrUtil.removeSuffix(filePath, fileName); ftp.reconnectIfTimeout(); boolean success = ftp.upload(dir, fileName, new ByteArrayInputStream(content)); if (!success) { throw new FtpException(StrUtil.format("上传文件到目标目录 ({}) 失败", filePath)); } // 拼接返回路径 return super.formatFileUrl(config.getDomain(), path); } @Override public void delete(String path) { String filePath = getFilePath(path); ftp.reconnectIfTimeout(); ftp.delFile(filePath); } @Override public byte[] getContent(String path) { String filePath = getFilePath(path); String fileName = FileUtil.getName(filePath); String dir = StrUtil.removeSuffix(filePath, fileName); ByteArrayOutputStream out = new ByteArrayOutputStream(); ftp.reconnectIfTimeout(); ftp.download(dir, fileName, out); return out.toByteArray(); } private String getFilePath(String path) { return config.getBasePath() + path; } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/file/core/client/ftp/FtpFileClientConfig.java ================================================ package co.yixiang.yshop.module.infra.framework.file.core.client.ftp; import co.yixiang.yshop.module.infra.framework.file.core.client.FileClientConfig; import lombok.Data; import org.hibernate.validator.constraints.URL; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; /** * Ftp 文件客户端的配置类 * * @author yshop */ @Data public class FtpFileClientConfig implements FileClientConfig { /** * 基础路径 */ @NotEmpty(message = "基础路径不能为空") private String basePath; /** * 自定义域名 */ @NotEmpty(message = "domain 不能为空") @URL(message = "domain 必须是 URL 格式") private String domain; /** * 主机地址 */ @NotEmpty(message = "host 不能为空") private String host; /** * 主机端口 */ @NotNull(message = "port 不能为空") private Integer port; /** * 用户名 */ @NotEmpty(message = "用户名不能为空") private String username; /** * 密码 */ @NotEmpty(message = "密码不能为空") private String password; /** * 连接模式 * * 使用 {@link cn.hutool.extra.ftp.FtpMode} 对应的字符串 */ @NotEmpty(message = "连接模式不能为空") private String mode; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/file/core/client/local/LocalFileClient.java ================================================ package co.yixiang.yshop.module.infra.framework.file.core.client.local; import cn.hutool.core.io.FileUtil; import co.yixiang.yshop.module.infra.framework.file.core.client.AbstractFileClient; import java.io.File; /** * 本地文件客户端 * * @author yshop */ public class LocalFileClient extends AbstractFileClient { public LocalFileClient(Long id, LocalFileClientConfig config) { super(id, config); } @Override protected void doInit() { // 补全风格。例如说 Linux 是 /,Windows 是 \ if (!config.getBasePath().endsWith(File.separator)) { config.setBasePath(config.getBasePath() + File.separator); } } @Override public String upload(byte[] content, String path, String type) { // 执行写入 String filePath = getFilePath(path); FileUtil.writeBytes(content, filePath); // 拼接返回路径 return super.formatFileUrl(config.getDomain(), path); } @Override public void delete(String path) { String filePath = getFilePath(path); FileUtil.del(filePath); } @Override public byte[] getContent(String path) { String filePath = getFilePath(path); return FileUtil.readBytes(filePath); } private String getFilePath(String path) { return config.getBasePath() + path; } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/file/core/client/local/LocalFileClientConfig.java ================================================ package co.yixiang.yshop.module.infra.framework.file.core.client.local; import co.yixiang.yshop.module.infra.framework.file.core.client.FileClientConfig; import lombok.Data; import org.hibernate.validator.constraints.URL; import jakarta.validation.constraints.NotEmpty; /** * 本地文件客户端的配置类 * * @author yshop */ @Data public class LocalFileClientConfig implements FileClientConfig { /** * 基础路径 */ @NotEmpty(message = "基础路径不能为空") private String basePath; /** * 自定义域名 */ @NotEmpty(message = "domain 不能为空") @URL(message = "domain 必须是 URL 格式") private String domain; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/file/core/client/s3/FilePresignedUrlRespDTO.java ================================================ package co.yixiang.yshop.module.infra.framework.file.core.client.s3; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * 文件预签名地址 Response DTO * * @author owen */ @Data @AllArgsConstructor @NoArgsConstructor public class FilePresignedUrlRespDTO { /** * 文件上传 URL(用于上传) * * 例如说: */ private String uploadUrl; /** * 文件 URL(用于读取、下载等) */ private String url; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/file/core/client/s3/S3FileClient.java ================================================ package co.yixiang.yshop.module.infra.framework.file.core.client.s3; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpUtil; import co.yixiang.yshop.module.infra.framework.file.core.client.AbstractFileClient; import io.minio.*; import io.minio.http.Method; import java.io.ByteArrayInputStream; import java.util.concurrent.TimeUnit; /** * 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务 *

* S3 协议的客户端,采用亚马逊提供的 software.amazon.awssdk.s3 库 * * @author yshop */ public class S3FileClient extends AbstractFileClient { private MinioClient client; public S3FileClient(Long id, S3FileClientConfig config) { super(id, config); } @Override protected void doInit() { // 补全 domain if (StrUtil.isEmpty(config.getDomain())) { config.setDomain(buildDomain()); } // 初始化客户端 client = MinioClient.builder() .endpoint(buildEndpointURL()) // Endpoint URL .region(buildRegion()) // Region .credentials(config.getAccessKey(), config.getAccessSecret()) // 认证密钥 .build(); } /** * 基于 endpoint 构建调用云服务的 URL 地址 * * @return URI 地址 */ private String buildEndpointURL() { // 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIO if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) { return config.getEndpoint(); } return StrUtil.format("https://{}", config.getEndpoint()); } /** * 基于 bucket + endpoint 构建访问的 Domain 地址 * * @return Domain 地址 */ private String buildDomain() { // 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIO if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) { return StrUtil.format("{}/{}", config.getEndpoint(), config.getBucket()); } // 阿里云、腾讯云、华为云都适合。七牛云比较特殊,必须有自定义域名 return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint()); } /** * 基于 bucket 构建 region 地区 * * @return region 地区 */ private String buildRegion() { // 阿里云必须有 region,否则会报错 if (config.getEndpoint().contains(S3FileClientConfig.ENDPOINT_ALIYUN)) { return StrUtil.subBefore(config.getEndpoint(), '.', false) .replaceAll("-internal", "")// 去除内网 Endpoint 的后缀 .replaceAll("https://", ""); } // 腾讯云必须有 region,否则会报错 if (config.getEndpoint().contains(S3FileClientConfig.ENDPOINT_TENCENT)) { return StrUtil.subAfter(config.getEndpoint(), "cos.", false) .replaceAll("." + S3FileClientConfig.ENDPOINT_TENCENT, ""); // 去除 Endpoint } return null; } @Override public String upload(byte[] content, String path, String type) throws Exception { // 执行上传 client.putObject(PutObjectArgs.builder() .bucket(config.getBucket()) // bucket 必须传递 .contentType(type) .object(path) // 相对路径作为 key .stream(new ByteArrayInputStream(content), content.length, -1) // 文件内容 .build()); // 拼接返回路径 return config.getDomain() + "/" + path; } @Override public void delete(String path) throws Exception { client.removeObject(RemoveObjectArgs.builder() .bucket(config.getBucket()) // bucket 必须传递 .object(path) // 相对路径作为 key .build()); } @Override public byte[] getContent(String path) throws Exception { GetObjectResponse response = client.getObject(GetObjectArgs.builder() .bucket(config.getBucket()) // bucket 必须传递 .object(path) // 相对路径作为 key .build()); return IoUtil.readBytes(response); } @Override public FilePresignedUrlRespDTO getPresignedObjectUrl(String path) throws Exception { String uploadUrl = client.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder() .method(Method.PUT) .bucket(config.getBucket()) .object(path) .expiry(10, TimeUnit.MINUTES) // 过期时间(秒数)取值范围:1 秒 ~ 7 天 .build() ); return new FilePresignedUrlRespDTO(uploadUrl, config.getDomain() + "/" + path); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/file/core/client/s3/S3FileClientConfig.java ================================================ package co.yixiang.yshop.module.infra.framework.file.core.client.s3; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.module.infra.framework.file.core.client.FileClientConfig; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import org.hibernate.validator.constraints.URL; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotNull; /** * S3 文件客户端的配置类 * * @author yshop */ @Data public class S3FileClientConfig implements FileClientConfig { public static final String ENDPOINT_QINIU = "qiniucs.com"; public static final String ENDPOINT_ALIYUN = "aliyuncs.com"; public static final String ENDPOINT_TENCENT = "myqcloud.com"; /** * 节点地址 * 1. MinIO:https://www.yixiang.co/Spring-Boot/MinIO 。例如说,http://127.0.0.1:9000 * 2. 阿里云:https://help.aliyun.com/document_detail/31837.html * 3. 腾讯云:https://cloud.tencent.com/document/product/436/6224 * 4. 七牛云:https://developer.qiniu.com/kodo/4088/s3-access-domainname * 5. 华为云:https://developer.huaweicloud.com/endpoint?OBS */ @NotNull(message = "endpoint 不能为空") private String endpoint; /** * 自定义域名 * 1. MinIO:通过 Nginx 配置 * 2. 阿里云:https://help.aliyun.com/document_detail/31836.html * 3. 腾讯云:https://cloud.tencent.com/document/product/436/11142 * 4. 七牛云:https://developer.qiniu.com/kodo/8556/set-the-custom-source-domain-name * 5. 华为云:https://support.huaweicloud.com/usermanual-obs/obs_03_0032.html */ @URL(message = "domain 必须是 URL 格式") private String domain; /** * 存储 Bucket */ @NotNull(message = "bucket 不能为空") private String bucket; /** * 访问 Key * 1. MinIO:https://www.yixiang.co/Spring-Boot/MinIO * 2. 阿里云:https://ram.console.aliyun.com/manage/ak * 3. 腾讯云:https://console.cloud.tencent.com/cam/capi * 4. 七牛云:https://portal.qiniu.com/user/key * 5. 华为云:https://support.huaweicloud.com/qs-obs/obs_qs_0005.html */ @NotNull(message = "accessKey 不能为空") private String accessKey; /** * 访问 Secret */ @NotNull(message = "accessSecret 不能为空") private String accessSecret; @SuppressWarnings("RedundantIfStatement") @AssertTrue(message = "domain 不能为空") @JsonIgnore public boolean isDomainValid() { // 如果是七牛,必须带有 domain if (StrUtil.contains(endpoint, ENDPOINT_QINIU) && StrUtil.isEmpty(domain)) { return false; } return true; } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/file/core/client/sftp/SftpFileClient.java ================================================ package co.yixiang.yshop.module.infra.framework.file.core.client.sftp; import cn.hutool.core.io.FileUtil; import cn.hutool.extra.ssh.Sftp; import co.yixiang.yshop.framework.common.util.io.FileUtils; import co.yixiang.yshop.module.infra.framework.file.core.client.AbstractFileClient; import java.io.File; /** * Sftp 文件客户端 * * @author yshop */ public class SftpFileClient extends AbstractFileClient { private Sftp sftp; public SftpFileClient(Long id, SftpFileClientConfig config) { super(id, config); } @Override protected void doInit() { // 补全风格。例如说 Linux 是 /,Windows 是 \ if (!config.getBasePath().endsWith(File.separator)) { config.setBasePath(config.getBasePath() + File.separator); } // 初始化 Ftp 对象 this.sftp = new Sftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword()); } @Override public String upload(byte[] content, String path, String type) { // 执行写入 String filePath = getFilePath(path); File file = FileUtils.createTempFile(content); sftp.upload(filePath, file); // 拼接返回路径 return super.formatFileUrl(config.getDomain(), path); } @Override public void delete(String path) { String filePath = getFilePath(path); sftp.delFile(filePath); } @Override public byte[] getContent(String path) { String filePath = getFilePath(path); File destFile = FileUtils.createTempFile(); sftp.download(filePath, destFile); return FileUtil.readBytes(destFile); } private String getFilePath(String path) { return config.getBasePath() + path; } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/file/core/client/sftp/SftpFileClientConfig.java ================================================ package co.yixiang.yshop.module.infra.framework.file.core.client.sftp; import co.yixiang.yshop.module.infra.framework.file.core.client.FileClientConfig; import lombok.Data; import org.hibernate.validator.constraints.URL; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; /** * Sftp 文件客户端的配置类 * * @author yshop */ @Data public class SftpFileClientConfig implements FileClientConfig { /** * 基础路径 */ @NotEmpty(message = "基础路径不能为空") private String basePath; /** * 自定义域名 */ @NotEmpty(message = "domain 不能为空") @URL(message = "domain 必须是 URL 格式") private String domain; /** * 主机地址 */ @NotEmpty(message = "host 不能为空") private String host; /** * 主机端口 */ @NotNull(message = "port 不能为空") private Integer port; /** * 用户名 */ @NotEmpty(message = "用户名不能为空") private String username; /** * 密码 */ @NotEmpty(message = "密码不能为空") private String password; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/file/core/enums/FileStorageEnum.java ================================================ package co.yixiang.yshop.module.infra.framework.file.core.enums; import cn.hutool.core.util.ArrayUtil; import co.yixiang.yshop.module.infra.framework.file.core.client.FileClient; import co.yixiang.yshop.module.infra.framework.file.core.client.FileClientConfig; import co.yixiang.yshop.module.infra.framework.file.core.client.db.DBFileClient; import co.yixiang.yshop.module.infra.framework.file.core.client.db.DBFileClientConfig; import co.yixiang.yshop.module.infra.framework.file.core.client.ftp.FtpFileClient; import co.yixiang.yshop.module.infra.framework.file.core.client.ftp.FtpFileClientConfig; import co.yixiang.yshop.module.infra.framework.file.core.client.local.LocalFileClient; import co.yixiang.yshop.module.infra.framework.file.core.client.local.LocalFileClientConfig; import co.yixiang.yshop.module.infra.framework.file.core.client.s3.S3FileClient; import co.yixiang.yshop.module.infra.framework.file.core.client.s3.S3FileClientConfig; import co.yixiang.yshop.module.infra.framework.file.core.client.sftp.SftpFileClient; import co.yixiang.yshop.module.infra.framework.file.core.client.sftp.SftpFileClientConfig; import lombok.AllArgsConstructor; import lombok.Getter; /** * 文件存储器枚举 * * @author yshop */ @AllArgsConstructor @Getter public enum FileStorageEnum { DB(1, DBFileClientConfig.class, DBFileClient.class), LOCAL(10, LocalFileClientConfig.class, LocalFileClient.class), FTP(11, FtpFileClientConfig.class, FtpFileClient.class), SFTP(12, SftpFileClientConfig.class, SftpFileClient.class), S3(20, S3FileClientConfig.class, S3FileClient.class), ; /** * 存储器 */ private final Integer storage; /** * 配置类 */ private final Class configClass; /** * 客户端类 */ private final Class clientClass; public static FileStorageEnum getByStorage(Integer storage) { return ArrayUtil.firstMatch(o -> o.getStorage().equals(storage), values()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/file/core/utils/FileTypeUtils.java ================================================ package co.yixiang.yshop.module.infra.framework.file.core.utils; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.StrUtil; import com.alibaba.ttl.TransmittableThreadLocal; import jakarta.servlet.http.HttpServletResponse; import lombok.SneakyThrows; import org.apache.tika.Tika; import java.io.IOException; import java.net.URLEncoder; /** * 文件类型 Utils * * @author yshop */ public class FileTypeUtils { private static final ThreadLocal TIKA = TransmittableThreadLocal.withInitial(Tika::new); /** * 获得文件的 mineType,对于doc,jar等文件会有误差 * * @param data 文件内容 * @return mineType 无法识别时会返回“application/octet-stream” */ @SneakyThrows public static String getMineType(byte[] data) { return TIKA.get().detect(data); } /** * 已知文件名,获取文件类型,在某些情况下比通过字节数组准确,例如使用jar文件时,通过名字更为准确 * * @param name 文件名 * @return mineType 无法识别时会返回“application/octet-stream” */ public static String getMineType(String name) { return TIKA.get().detect(name); } /** * 在拥有文件和数据的情况下,最好使用此方法,最为准确 * * @param data 文件内容 * @param name 文件名 * @return mineType 无法识别时会返回“application/octet-stream” */ public static String getMineType(byte[] data, String name) { return TIKA.get().detect(data, name); } /** * 返回附件 * * @param response 响应 * @param filename 文件名 * @param content 附件内容 */ public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException { // 设置 header 和 contentType response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8")); String contentType = getMineType(content, filename); response.setContentType(contentType); // 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题 if (StrUtil.containsIgnoreCase(contentType, "video")) { response.setHeader("Content-Length", String.valueOf(content.length - 1)); response.setHeader("Content-Range", String.valueOf(content.length - 1)); response.setHeader("Accept-Ranges", "bytes"); } // 输出附件 IoUtil.write(response.getOutputStream(), false, content); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/monitor/config/AdminServerConfiguration.java ================================================ package co.yixiang.yshop.module.infra.framework.monitor.config; import de.codecentric.boot.admin.server.config.EnableAdminServer; import org.springframework.context.annotation.Configuration; @Configuration(proxyBeanMethods = false) @EnableAdminServer public class AdminServerConfiguration { } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/security/config/SecurityConfiguration.java ================================================ package co.yixiang.yshop.module.infra.framework.security.config; import co.yixiang.yshop.framework.security.config.AuthorizeRequestsCustomizer; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; /** * Infra 模块的 Security 配置 */ @Configuration(proxyBeanMethods = false, value = "infraSecurityConfiguration") public class SecurityConfiguration { @Value("${spring.boot.admin.context-path:''}") private String adminSeverContextPath; @Bean("infraAuthorizeRequestsCustomizer") public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { return new AuthorizeRequestsCustomizer() { @Override public void customize(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { // Swagger 接口文档 registry.requestMatchers("/v3/api-docs/**").permitAll() .requestMatchers("/swagger-ui.html").permitAll() .requestMatchers("/swagger-ui/**").permitAll() .requestMatchers("/swagger-resources/**").permitAll() .requestMatchers("/webjars/**").permitAll() .requestMatchers("/*/api-docs").permitAll(); // Spring Boot Actuator 的安全配置 registry.requestMatchers("/actuator").permitAll() .requestMatchers("/actuator/**").permitAll(); // Druid 监控 registry.requestMatchers("/druid/**").permitAll(); // Spring Boot Admin Server 的安全配置 registry.requestMatchers(adminSeverContextPath).permitAll() .requestMatchers(adminSeverContextPath + "/**").permitAll(); // 文件读取 registry.requestMatchers(buildAdminApi("/infra/file/*/get/**")).permitAll(); } }; } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/framework/web/config/InfraWebConfiguration.java ================================================ package co.yixiang.yshop.module.infra.framework.web.config; import co.yixiang.yshop.framework.swagger.config.YshopSwaggerAutoConfiguration; import org.springdoc.core.models.GroupedOpenApi; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * infra 模块的 web 组件的 Configuration * * @author yshop */ @Configuration(proxyBeanMethods = false) public class InfraWebConfiguration { /** * infra 模块的 API 分组 */ @Bean public GroupedOpenApi infraGroupedOpenApi() { return YshopSwaggerAutoConfiguration.buildGroupedOpenApi("infra"); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/job/job/JobLogCleanJob.java ================================================ package co.yixiang.yshop.module.infra.job.job; import co.yixiang.yshop.framework.quartz.core.handler.JobHandler; import co.yixiang.yshop.framework.tenant.core.aop.TenantIgnore; import co.yixiang.yshop.module.infra.service.job.JobLogService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; /** * 物理删除 N 天前的任务日志的 Job * * @author j-sentinel */ @Slf4j @Component public class JobLogCleanJob implements JobHandler { @Resource private JobLogService jobLogService; /** * 清理超过(14)天的日志 */ private static final Integer JOB_CLEAN_RETAIN_DAY = 14; /** * 每次删除间隔的条数,如果值太高可能会造成数据库的压力过大 */ private static final Integer DELETE_LIMIT = 100; @Override @TenantIgnore public String execute(String param) { Integer count = jobLogService.cleanJobLog(JOB_CLEAN_RETAIN_DAY, DELETE_LIMIT); log.info("[execute][定时执行清理定时任务日志数量 ({}) 个]", count); return String.format("定时执行清理定时任务日志数量 %s 个", count); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/job/logger/AccessLogCleanJob.java ================================================ package co.yixiang.yshop.module.infra.job.logger; import co.yixiang.yshop.framework.quartz.core.handler.JobHandler; import co.yixiang.yshop.framework.tenant.core.aop.TenantIgnore; import co.yixiang.yshop.module.infra.service.logger.ApiAccessLogService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; /** * 物理删除 N 天前的访问日志的 Job * * @author j-sentinel */ @Component @Slf4j public class AccessLogCleanJob implements JobHandler { @Resource private ApiAccessLogService apiAccessLogService; /** * 清理超过(14)天的日志 */ private static final Integer JOB_CLEAN_RETAIN_DAY = 14; /** * 每次删除间隔的条数,如果值太高可能会造成数据库的压力过大 */ private static final Integer DELETE_LIMIT = 100; @Override @TenantIgnore public String execute(String param) { Integer count = apiAccessLogService.cleanAccessLog(JOB_CLEAN_RETAIN_DAY, DELETE_LIMIT); log.info("[execute][定时执行清理访问日志数量 ({}) 个]", count); return String.format("定时执行清理访问日志数量 %s 个", count); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/job/logger/ErrorLogCleanJob.java ================================================ package co.yixiang.yshop.module.infra.job.logger; import co.yixiang.yshop.framework.quartz.core.handler.JobHandler; import co.yixiang.yshop.framework.tenant.core.aop.TenantIgnore; import co.yixiang.yshop.module.infra.service.logger.ApiErrorLogService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; /** * 物理删除 N 天前的错误日志的 Job * * @author j-sentinel */ @Slf4j @Component public class ErrorLogCleanJob implements JobHandler { @Resource private ApiErrorLogService apiErrorLogService; /** * 清理超过(14)天的日志 */ private static final Integer JOB_CLEAN_RETAIN_DAY = 14; /** * 每次删除间隔的条数,如果值太高可能会造成数据库的压力过大 */ private static final Integer DELETE_LIMIT = 100; @Override @TenantIgnore public String execute(String param) { Integer count = apiErrorLogService.cleanErrorLog(JOB_CLEAN_RETAIN_DAY,DELETE_LIMIT); log.info("[execute][定时执行清理错误日志数量 ({}) 个]", count); return String.format("定时执行清理错误日志数量 %s 个", count); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/codegen/CodegenService.java ================================================ package co.yixiang.yshop.module.infra.service.codegen; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO; import co.yixiang.yshop.module.infra.dal.dataobject.codegen.CodegenColumnDO; import co.yixiang.yshop.module.infra.dal.dataobject.codegen.CodegenTableDO; import java.util.List; import java.util.Map; /** * 代码生成 Service 接口 * * @author yshop */ public interface CodegenService { /** * 基于数据库的表结构,创建代码生成器的表定义 * * @param userId 用户编号 * @param reqVO 表信息 * @return 创建的表定义的编号数组 */ List createCodegenList(Long userId, CodegenCreateListReqVO reqVO); /** * 更新数据库的表和字段定义 * * @param updateReqVO 更新信息 */ void updateCodegen(CodegenUpdateReqVO updateReqVO); /** * 基于数据库的表结构,同步数据库的表和字段定义 * * @param tableId 表编号 */ void syncCodegenFromDB(Long tableId); /** * 删除数据库的表和字段定义 * * @param tableId 数据编号 */ void deleteCodegen(Long tableId); /** * 获得表定义列表 * * @param dataSourceConfigId 数据源配置的编号 * @return 表定义列表 */ List getCodegenTableList(Long dataSourceConfigId); /** * 获得表定义分页 * * @param pageReqVO 分页条件 * @return 表定义分页 */ PageResult getCodegenTablePage(CodegenTablePageReqVO pageReqVO); /** * 获得表定义 * * @param id 表编号 * @return 表定义 */ CodegenTableDO getCodegenTable(Long id); /** * 获得指定表的字段定义数组 * * @param tableId 表编号 * @return 字段定义数组 */ List getCodegenColumnListByTableId(Long tableId); /** * 执行指定表的代码生成 * * @param tableId 表编号 * @return 生成结果。key 为文件路径,value 为对应的代码内容 */ Map generationCodes(Long tableId); /** * 获得数据库自带的表定义列表 * * @param dataSourceConfigId 数据源的配置编号 * @param name 表名称 * @param comment 表描述 * @return 表定义列表 */ List getDatabaseTableList(Long dataSourceConfigId, String name, String comment); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/codegen/CodegenServiceImpl.java ================================================ package co.yixiang.yshop.module.infra.service.codegen; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; import co.yixiang.yshop.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO; import co.yixiang.yshop.module.infra.dal.dataobject.codegen.CodegenColumnDO; import co.yixiang.yshop.module.infra.dal.dataobject.codegen.CodegenTableDO; import co.yixiang.yshop.module.infra.dal.mysql.codegen.CodegenColumnMapper; import co.yixiang.yshop.module.infra.dal.mysql.codegen.CodegenTableMapper; import co.yixiang.yshop.module.infra.enums.codegen.CodegenSceneEnum; import co.yixiang.yshop.module.infra.enums.codegen.CodegenTemplateTypeEnum; import co.yixiang.yshop.module.infra.framework.codegen.config.CodegenProperties; import co.yixiang.yshop.module.infra.service.codegen.inner.CodegenBuilder; import co.yixiang.yshop.module.infra.service.codegen.inner.CodegenEngine; import co.yixiang.yshop.module.infra.service.db.DatabaseTableService; import co.yixiang.yshop.module.system.api.user.AdminUserApi; import com.baomidou.mybatisplus.generator.config.po.TableField; import com.baomidou.mybatisplus.generator.config.po.TableInfo; import com.google.common.annotations.VisibleForTesting; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import jakarta.annotation.Resource; import java.util.*; import java.util.function.BiPredicate; import java.util.stream.Collectors; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertMap; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertSet; import static co.yixiang.yshop.module.infra.enums.ErrorCodeConstants.*; /** * 代码生成 Service 实现类 * * @author yshop */ @Service public class CodegenServiceImpl implements CodegenService { @Resource private DatabaseTableService databaseTableService; @Resource private CodegenTableMapper codegenTableMapper; @Resource private CodegenColumnMapper codegenColumnMapper; @Resource private AdminUserApi userApi; @Resource private CodegenBuilder codegenBuilder; @Resource private CodegenEngine codegenEngine; @Resource private CodegenProperties codegenProperties; @Override @Transactional(rollbackFor = Exception.class) public List createCodegenList(Long userId, CodegenCreateListReqVO reqVO) { List ids = new ArrayList<>(reqVO.getTableNames().size()); // 遍历添加。虽然效率会低一点,但是没必要做成完全批量,因为不会这么大量 reqVO.getTableNames().forEach(tableName -> ids.add(createCodegen(userId, reqVO.getDataSourceConfigId(), tableName))); return ids; } private Long createCodegen(Long userId, Long dataSourceConfigId, String tableName) { // 从数据库中,获得数据库表结构 TableInfo tableInfo = databaseTableService.getTable(dataSourceConfigId, tableName); // 导入 return createCodegen0(userId, dataSourceConfigId, tableInfo); } private Long createCodegen0(Long userId, Long dataSourceConfigId, TableInfo tableInfo) { // 校验导入的表和字段非空 validateTableInfo(tableInfo); // 校验是否已经存在 if (codegenTableMapper.selectByTableNameAndDataSourceConfigId(tableInfo.getName(), dataSourceConfigId) != null) { throw exception(CODEGEN_TABLE_EXISTS); } // 构建 CodegenTableDO 对象,插入到 DB 中 CodegenTableDO table = codegenBuilder.buildTable(tableInfo); table.setDataSourceConfigId(dataSourceConfigId); table.setScene(CodegenSceneEnum.ADMIN.getScene()); // 默认配置下,使用管理后台的模板 table.setFrontType(codegenProperties.getFrontType()); table.setAuthor(userApi.getUser(userId).getNickname()); codegenTableMapper.insert(table); // 构建 CodegenColumnDO 数组,插入到 DB 中 List columns = codegenBuilder.buildColumns(table.getId(), tableInfo.getFields()); // 如果没有主键,则使用第一个字段作为主键 if (!tableInfo.isHavePrimaryKey()) { columns.get(0).setPrimaryKey(true); } codegenColumnMapper.insertBatch(columns); return table.getId(); } @VisibleForTesting void validateTableInfo(TableInfo tableInfo) { if (tableInfo == null) { throw exception(CODEGEN_IMPORT_TABLE_NULL); } if (StrUtil.isEmpty(tableInfo.getComment())) { throw exception(CODEGEN_TABLE_INFO_TABLE_COMMENT_IS_NULL); } if (CollUtil.isEmpty(tableInfo.getFields())) { throw exception(CODEGEN_IMPORT_COLUMNS_NULL); } tableInfo.getFields().forEach(field -> { if (StrUtil.isEmpty(field.getComment())) { throw exception(CODEGEN_TABLE_INFO_COLUMN_COMMENT_IS_NULL, field.getName()); } }); } @Override @Transactional(rollbackFor = Exception.class) public void updateCodegen(CodegenUpdateReqVO updateReqVO) { // 校验是否已经存在 if (codegenTableMapper.selectById(updateReqVO.getTable().getId()) == null) { throw exception(CODEGEN_TABLE_NOT_EXISTS); } // 校验主表字段存在 if (Objects.equals(updateReqVO.getTable().getTemplateType(), CodegenTemplateTypeEnum.SUB.getType())) { if (codegenTableMapper.selectById(updateReqVO.getTable().getMasterTableId()) == null) { throw exception(CODEGEN_MASTER_TABLE_NOT_EXISTS, updateReqVO.getTable().getMasterTableId()); } if (CollUtil.findOne(updateReqVO.getColumns(), // 关联主表的字段不存在 column -> column.getId().equals(updateReqVO.getTable().getSubJoinColumnId())) == null) { throw exception(CODEGEN_SUB_COLUMN_NOT_EXISTS, updateReqVO.getTable().getSubJoinColumnId()); } } // 更新 table 表定义 CodegenTableDO updateTableObj = BeanUtils.toBean(updateReqVO.getTable(), CodegenTableDO.class); codegenTableMapper.updateById(updateTableObj); // 更新 column 字段定义 List updateColumnObjs = BeanUtils.toBean(updateReqVO.getColumns(), CodegenColumnDO.class); updateColumnObjs.forEach(updateColumnObj -> codegenColumnMapper.updateById(updateColumnObj)); } @Override @Transactional(rollbackFor = Exception.class) public void syncCodegenFromDB(Long tableId) { // 校验是否已经存在 CodegenTableDO table = codegenTableMapper.selectById(tableId); if (table == null) { throw exception(CODEGEN_TABLE_NOT_EXISTS); } // 从数据库中,获得数据库表结构 TableInfo tableInfo = databaseTableService.getTable(table.getDataSourceConfigId(), table.getTableName()); // 执行同步 syncCodegen0(tableId, tableInfo); } private void syncCodegen0(Long tableId, TableInfo tableInfo) { // 1. 校验导入的表和字段非空 validateTableInfo(tableInfo); List tableFields = tableInfo.getFields(); // 2. 构建 CodegenColumnDO 数组,只同步新增的字段 List codegenColumns = codegenColumnMapper.selectListByTableId(tableId); Set codegenColumnNames = convertSet(codegenColumns, CodegenColumnDO::getColumnName); // 3.1 计算需要【修改】的字段,插入时重新插入,删除时将原来的删除 Map codegenColumnDOMap = convertMap(codegenColumns, CodegenColumnDO::getColumnName); BiPredicate primaryKeyPredicate = (tableField, codegenColumn) -> tableField.getMetaInfo().getJdbcType().name().equals(codegenColumn.getDataType()) && tableField.getMetaInfo().isNullable() == codegenColumn.getNullable() && tableField.isKeyFlag() == codegenColumn.getPrimaryKey() && tableField.getComment().equals(codegenColumn.getColumnComment()); Set modifyFieldNames = tableFields.stream() .filter(tableField -> codegenColumnDOMap.get(tableField.getColumnName()) != null && !primaryKeyPredicate.test(tableField, codegenColumnDOMap.get(tableField.getColumnName()))) .map(TableField::getColumnName) .collect(Collectors.toSet()); // 3.2 计算需要【删除】的字段 Set tableFieldNames = convertSet(tableFields, TableField::getName); Set deleteColumnIds = codegenColumns.stream() .filter(column -> (!tableFieldNames.contains(column.getColumnName())) || modifyFieldNames.contains(column.getColumnName())) .map(CodegenColumnDO::getId).collect(Collectors.toSet()); // 移除已经存在的字段 tableFields.removeIf(column -> codegenColumnNames.contains(column.getColumnName()) && (!modifyFieldNames.contains(column.getColumnName()))); if (CollUtil.isEmpty(tableFields) && CollUtil.isEmpty(deleteColumnIds)) { throw exception(CODEGEN_SYNC_NONE_CHANGE); } // 4.1 插入新增的字段 List columns = codegenBuilder.buildColumns(tableId, tableFields); codegenColumnMapper.insertBatch(columns); // 4.2 删除不存在的字段 if (CollUtil.isNotEmpty(deleteColumnIds)) { codegenColumnMapper.deleteBatchIds(deleteColumnIds); } } @Override @Transactional(rollbackFor = Exception.class) public void deleteCodegen(Long tableId) { // 校验是否已经存在 if (codegenTableMapper.selectById(tableId) == null) { throw exception(CODEGEN_TABLE_NOT_EXISTS); } // 删除 table 表定义 codegenTableMapper.deleteById(tableId); // 删除 column 字段定义 codegenColumnMapper.deleteListByTableId(tableId); } @Override public List getCodegenTableList(Long dataSourceConfigId) { return codegenTableMapper.selectListByDataSourceConfigId(dataSourceConfigId); } @Override public PageResult getCodegenTablePage(CodegenTablePageReqVO pageReqVO) { return codegenTableMapper.selectPage(pageReqVO); } @Override public CodegenTableDO getCodegenTable(Long id) { return codegenTableMapper.selectById(id); } @Override public List getCodegenColumnListByTableId(Long tableId) { return codegenColumnMapper.selectListByTableId(tableId); } @Override public Map generationCodes(Long tableId) { // 校验是否已经存在 CodegenTableDO table = codegenTableMapper.selectById(tableId); if (table == null) { throw exception(CODEGEN_TABLE_NOT_EXISTS); } List columns = codegenColumnMapper.selectListByTableId(tableId); if (CollUtil.isEmpty(columns)) { throw exception(CODEGEN_COLUMN_NOT_EXISTS); } // 如果是主子表,则加载对应的子表信息 List subTables = null; List> subColumnsList = null; if (CodegenTemplateTypeEnum.isMaster(table.getTemplateType())) { // 校验子表存在 subTables = codegenTableMapper.selectListByTemplateTypeAndMasterTableId( CodegenTemplateTypeEnum.SUB.getType(), tableId); if (CollUtil.isEmpty(subTables)) { throw exception(CODEGEN_MASTER_GENERATION_FAIL_NO_SUB_TABLE); } // 校验子表的关联字段存在 subColumnsList = new ArrayList<>(); for (CodegenTableDO subTable : subTables) { List subColumns = codegenColumnMapper.selectListByTableId(subTable.getId()); if (CollUtil.findOne(subColumns, column -> column.getId().equals(subTable.getSubJoinColumnId())) == null) { throw exception(CODEGEN_SUB_COLUMN_NOT_EXISTS, subTable.getId()); } subColumnsList.add(subColumns); } } // 执行生成 return codegenEngine.execute(table, columns, subTables, subColumnsList); } @Override public List getDatabaseTableList(Long dataSourceConfigId, String name, String comment) { List tables = databaseTableService.getTableList(dataSourceConfigId, name, comment); // 移除在 Codegen 中,已经存在的 Set existsTables = convertSet( codegenTableMapper.selectListByDataSourceConfigId(dataSourceConfigId), CodegenTableDO::getTableName); tables.removeIf(table -> existsTables.contains(table.getName())); return BeanUtils.toBean(tables, DatabaseTableRespVO.class); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/codegen/inner/CodegenBuilder.java ================================================ package co.yixiang.yshop.module.infra.service.codegen.inner; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.infra.convert.codegen.CodegenConvert; import co.yixiang.yshop.module.infra.dal.dataobject.codegen.CodegenColumnDO; import co.yixiang.yshop.module.infra.dal.dataobject.codegen.CodegenTableDO; import co.yixiang.yshop.module.infra.enums.codegen.CodegenColumnHtmlTypeEnum; import co.yixiang.yshop.module.infra.enums.codegen.CodegenColumnListConditionEnum; import co.yixiang.yshop.module.infra.enums.codegen.CodegenTemplateTypeEnum; import com.baomidou.mybatisplus.generator.config.po.TableField; import com.baomidou.mybatisplus.generator.config.po.TableInfo; import com.google.common.collect.Sets; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.util.*; import static cn.hutool.core.text.CharSequenceUtil.*; import static cn.hutool.core.util.RandomUtil.randomEle; import static cn.hutool.core.util.RandomUtil.randomInt; /** * 代码生成器的 Builder,负责: * 1. 将数据库的表 {@link TableInfo} 定义,构建成 {@link CodegenTableDO} * 2. 将数据库的列 {@link TableField} 构定义,建成 {@link CodegenColumnDO} */ @Component public class CodegenBuilder { /** * 字段名与 {@link CodegenColumnListConditionEnum} 的默认映射 * 注意,字段的匹配以后缀的方式 */ private static final Map COLUMN_LIST_OPERATION_CONDITION_MAPPINGS = MapUtil.builder() .put("name", CodegenColumnListConditionEnum.LIKE) .put("time", CodegenColumnListConditionEnum.BETWEEN) .put("date", CodegenColumnListConditionEnum.BETWEEN) .build(); /** * 字段名与 {@link CodegenColumnHtmlTypeEnum} 的默认映射 * 注意,字段的匹配以后缀的方式 */ private static final Map COLUMN_HTML_TYPE_MAPPINGS = MapUtil.builder() .put("status", CodegenColumnHtmlTypeEnum.RADIO) .put("sex", CodegenColumnHtmlTypeEnum.RADIO) .put("type", CodegenColumnHtmlTypeEnum.SELECT) .put("image", CodegenColumnHtmlTypeEnum.IMAGE_UPLOAD) .put("file", CodegenColumnHtmlTypeEnum.FILE_UPLOAD) .put("content", CodegenColumnHtmlTypeEnum.EDITOR) .put("description", CodegenColumnHtmlTypeEnum.EDITOR) .put("demo", CodegenColumnHtmlTypeEnum.EDITOR) .put("time", CodegenColumnHtmlTypeEnum.DATETIME) .put("date", CodegenColumnHtmlTypeEnum.DATETIME) .build(); /** * 多租户编号的字段名 */ public static final String TENANT_ID_FIELD = "tenantId"; /** * {@link co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO} 的字段 */ public static final Set BASE_DO_FIELDS = new HashSet<>(); /** * 新增操作,不需要传递的字段 */ private static final Set CREATE_OPERATION_EXCLUDE_COLUMN = Sets.newHashSet("id"); /** * 修改操作,不需要传递的字段 */ private static final Set UPDATE_OPERATION_EXCLUDE_COLUMN = Sets.newHashSet(); /** * 列表操作的条件,不需要传递的字段 */ private static final Set LIST_OPERATION_EXCLUDE_COLUMN = Sets.newHashSet("id"); /** * 列表操作的结果,不需要返回的字段 */ private static final Set LIST_OPERATION_RESULT_EXCLUDE_COLUMN = Sets.newHashSet(); static { Arrays.stream(ReflectUtil.getFields(BaseDO.class)).forEach(field -> BASE_DO_FIELDS.add(field.getName())); BASE_DO_FIELDS.add(TENANT_ID_FIELD); // 处理 OPERATION 相关的字段 CREATE_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); UPDATE_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); LIST_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); LIST_OPERATION_EXCLUDE_COLUMN.remove("createTime"); // 创建时间,还是可能需要传递的 LIST_OPERATION_RESULT_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); LIST_OPERATION_RESULT_EXCLUDE_COLUMN.remove("createTime"); // 创建时间,还是需要返回的 } public CodegenTableDO buildTable(TableInfo tableInfo) { CodegenTableDO table = CodegenConvert.INSTANCE.convert(tableInfo); initTableDefault(table); return table; } /** * 初始化 Table 表的默认字段 * * @param table 表定义 */ private void initTableDefault(CodegenTableDO table) { // 以 system_dept 举例子。moduleName 为 system、businessName 为 dept、className 为 Dept // 如果希望以 System 前缀,则可以手动在【代码生成 - 修改生成配置 - 基本信息】,将实体类名称改为 SystemDept 即可 String tableName = table.getTableName().toLowerCase(); // 第一步,_ 前缀的前面,作为 module 名字;第二步,moduleName 必须小写; table.setModuleName(subBefore(tableName, '_', false).toLowerCase()); // 第一步,第一个 _ 前缀的后面,作为 module 名字; 第二步,可能存在多个 _ 的情况,转换成驼峰; 第三步,businessName 必须小写; table.setBusinessName(toCamelCase(subAfter(tableName, '_', false)).toLowerCase()); // 驼峰 + 首字母大写;第一步,第一个 _ 前缀的后面,作为 class 名字;第二步,驼峰命名 table.setClassName(upperFirst(toCamelCase(subAfter(tableName, '_', false)))); // 去除结尾的表,作为类描述 table.setClassComment(StrUtil.removeSuffixIgnoreCase(table.getTableComment(), "表")); table.setTemplateType(CodegenTemplateTypeEnum.ONE.getType()); } public List buildColumns(Long tableId, List tableFields) { List columns = CodegenConvert.INSTANCE.convertList(tableFields); int index = 1; for (CodegenColumnDO column : columns) { column.setTableId(tableId); column.setOrdinalPosition(index++); // 特殊处理:Byte => Integer if (Byte.class.getSimpleName().equals(column.getJavaType())) { column.setJavaType(Integer.class.getSimpleName()); } // 初始化 Column 列的默认字段 processColumnOperation(column); // 处理 CRUD 相关的字段的默认值 processColumnUI(column); // 处理 UI 相关的字段的默认值 processColumnExample(column); // 处理字段的 swagger example 示例 } return columns; } private void processColumnOperation(CodegenColumnDO column) { // 处理 createOperation 字段 column.setCreateOperation(!CREATE_OPERATION_EXCLUDE_COLUMN.contains(column.getJavaField()) && !column.getPrimaryKey()); // 对于主键,创建时无需传递 // 处理 updateOperation 字段 column.setUpdateOperation(!UPDATE_OPERATION_EXCLUDE_COLUMN.contains(column.getJavaField()) || column.getPrimaryKey()); // 对于主键,更新时需要传递 // 处理 listOperation 字段 column.setListOperation(!LIST_OPERATION_EXCLUDE_COLUMN.contains(column.getJavaField()) && !column.getPrimaryKey()); // 对于主键,列表过滤不需要传递 // 处理 listOperationCondition 字段 COLUMN_LIST_OPERATION_CONDITION_MAPPINGS.entrySet().stream() .filter(entry -> StrUtil.endWithIgnoreCase(column.getJavaField(), entry.getKey())) .findFirst().ifPresent(entry -> column.setListOperationCondition(entry.getValue().getCondition())); if (column.getListOperationCondition() == null) { column.setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition()); } // 处理 listOperationResult 字段 column.setListOperationResult(!LIST_OPERATION_RESULT_EXCLUDE_COLUMN.contains(column.getJavaField())); } private void processColumnUI(CodegenColumnDO column) { // 基于后缀进行匹配 COLUMN_HTML_TYPE_MAPPINGS.entrySet().stream() .filter(entry -> StrUtil.endWithIgnoreCase(column.getJavaField(), entry.getKey())) .findFirst().ifPresent(entry -> column.setHtmlType(entry.getValue().getType())); // 如果是 Boolean 类型时,设置为 radio 类型. if (Boolean.class.getSimpleName().equals(column.getJavaType())) { column.setHtmlType(CodegenColumnHtmlTypeEnum.RADIO.getType()); } // 如果是 LocalDateTime 类型,则设置为 datetime 类型 if (LocalDateTime.class.getSimpleName().equals(column.getJavaType())) { column.setHtmlType(CodegenColumnHtmlTypeEnum.DATETIME.getType()); } // 兜底,设置默认为 input 类型 if (column.getHtmlType() == null) { column.setHtmlType(CodegenColumnHtmlTypeEnum.INPUT.getType()); } } /** * 处理字段的 swagger example 示例 * * @param column 字段 */ private void processColumnExample(CodegenColumnDO column) { // id、price、count 等可能是整数的后缀 if (StrUtil.endWithAnyIgnoreCase(column.getJavaField(), "id", "price", "count")) { column.setExample(String.valueOf(randomInt(1, Short.MAX_VALUE))); return; } // name if (StrUtil.endWithIgnoreCase(column.getJavaField(), "name")) { column.setExample(randomEle(new String[]{"张三", "李四", "王五", "赵六", "yshop"})); return; } // status if (StrUtil.endWithAnyIgnoreCase(column.getJavaField(), "status", "type")) { column.setExample(randomEle(new String[]{"1", "2"})); return; } // url if (StrUtil.endWithIgnoreCase(column.getColumnName(), "url")) { column.setExample("https://www.yixiang.co"); return; } // reason if (StrUtil.endWithIgnoreCase(column.getColumnName(), "reason")) { column.setExample(randomEle(new String[]{"不喜欢", "不对", "不好", "不香"})); return; } // description、memo、remark if (StrUtil.endWithAnyIgnoreCase(column.getColumnName(), "description", "memo", "remark")) { column.setExample(randomEle(new String[]{"你猜", "随便", "你说的对"})); return; } } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/codegen/inner/CodegenEngine.java ================================================ package co.yixiang.yshop.module.infra.service.codegen.inner; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.template.TemplateConfig; import cn.hutool.extra.template.TemplateEngine; import cn.hutool.extra.template.engine.velocity.VelocityEngine; import cn.hutool.system.SystemUtil; import co.yixiang.yshop.framework.apilog.core.annotation.ApiAccessLog; import co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum; import co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.framework.common.util.date.DateUtils; import co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.common.util.object.ObjectUtils; import co.yixiang.yshop.framework.common.util.string.StrUtils; import co.yixiang.yshop.framework.excel.core.annotations.DictFormat; import co.yixiang.yshop.framework.excel.core.convert.DictConvert; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.infra.dal.dataobject.codegen.CodegenColumnDO; import co.yixiang.yshop.module.infra.dal.dataobject.codegen.CodegenTableDO; import co.yixiang.yshop.module.infra.enums.codegen.CodegenFrontTypeEnum; import co.yixiang.yshop.module.infra.enums.codegen.CodegenSceneEnum; import co.yixiang.yshop.module.infra.enums.codegen.CodegenTemplateTypeEnum; import co.yixiang.yshop.module.infra.framework.codegen.config.CodegenProperties; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableTable; import com.google.common.collect.Maps; import com.google.common.collect.Table; import jakarta.annotation.PostConstruct; import jakarta.annotation.Resource; import lombok.Setter; import org.springframework.stereotype.Component; import java.util.*; import static cn.hutool.core.map.MapUtil.getStr; import static cn.hutool.core.text.CharSequenceUtil.*; /** * 代码生成的引擎,用于具体生成代码 * 目前基于 {@link org.apache.velocity.app.Velocity} 模板引擎实现 * * 考虑到 Java 模板引擎的框架非常多,Freemarker、Velocity、Thymeleaf 等等,所以我们采用 hutool 封装的 {@link cn.hutool.extra.template.Template} 抽象 * * @author yshop */ @Component public class CodegenEngine { /** * 后端的模板配置 * * key:模板在 resources 的地址 * value:生成的路径 */ private static final Map SERVER_TEMPLATES = MapUtil.builder(new LinkedHashMap<>()) // 有序 // Java module-biz Main .put(javaTemplatePath("controller/vo/pageReqVO"), javaModuleImplVOFilePath("PageReqVO")) .put(javaTemplatePath("controller/vo/listReqVO"), javaModuleImplVOFilePath("ListReqVO")) .put(javaTemplatePath("controller/vo/respVO"), javaModuleImplVOFilePath("RespVO")) .put(javaTemplatePath("controller/vo/saveReqVO"), javaModuleImplVOFilePath("SaveReqVO")) .put(javaTemplatePath("controller/controller"), javaModuleImplControllerFilePath()) .put(javaTemplatePath("dal/do"), javaModuleImplMainFilePath("dal/dataobject/${table.businessName}/${table.className}DO")) .put(javaTemplatePath("dal/do_sub"), // 特殊:主子表专属逻辑 javaModuleImplMainFilePath("dal/dataobject/${table.businessName}/${subTable.className}DO")) .put(javaTemplatePath("dal/mapper"), javaModuleImplMainFilePath("dal/mysql/${table.businessName}/${table.className}Mapper")) .put(javaTemplatePath("dal/mapper_sub"), // 特殊:主子表专属逻辑 javaModuleImplMainFilePath("dal/mysql/${table.businessName}/${subTable.className}Mapper")) .put(javaTemplatePath("dal/mapper.xml"), mapperXmlFilePath()) .put(javaTemplatePath("service/serviceImpl"), javaModuleImplMainFilePath("service/${table.businessName}/${table.className}ServiceImpl")) .put(javaTemplatePath("service/service"), javaModuleImplMainFilePath("service/${table.businessName}/${table.className}Service")) // Java module-biz Test .put(javaTemplatePath("test/serviceTest"), javaModuleImplTestFilePath("service/${table.businessName}/${table.className}ServiceImplTest")) // Java module-api Main .put(javaTemplatePath("enums/errorcode"), javaModuleApiMainFilePath("enums/ErrorCodeConstants_手动操作")) // SQL .put("codegen/sql/sql.vm", "sql/sql.sql") .put("codegen/sql/h2.vm", "sql/h2.sql") .build(); /** * 后端的配置模版 * * key1:UI 模版的类型 {@link CodegenFrontTypeEnum#getType()} * key2:模板在 resources 的地址 * value:生成的路径 */ private static final Table FRONT_TEMPLATES = ImmutableTable.builder() // Vue2 标准模版 .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/index.vue"), vueFilePath("views/${table.moduleName}/${table.businessName}/index.vue")) .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("api/api.js"), vueFilePath("api/${table.moduleName}/${table.businessName}/index.js")) .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/form.vue"), vueFilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue")) .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/form_sub_normal.vue"), // 特殊:主子表专属逻辑 vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue")) .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/form_sub_inner.vue"), // 特殊:主子表专属逻辑 vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue")) .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/form_sub_erp.vue"), // 特殊:主子表专属逻辑 vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue")) .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/list_sub_inner.vue"), // 特殊:主子表专属逻辑 vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue")) .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/list_sub_erp.vue"), // 特殊:主子表专属逻辑 vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue")) // Vue3 标准模版 .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/index.vue"), vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/form.vue"), vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue")) .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/components/form_sub_normal.vue"), // 特殊:主子表专属逻辑 vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue")) .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/components/form_sub_inner.vue"), // 特殊:主子表专属逻辑 vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue")) .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/components/form_sub_erp.vue"), // 特殊:主子表专属逻辑 vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue")) .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/components/list_sub_inner.vue"), // 特殊:主子表专属逻辑 vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue")) .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/components/list_sub_erp.vue"), // 特殊:主子表专属逻辑 vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue")) .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("api/api.ts"), vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) // Vue3 Schema 模版 .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/data.ts"), vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts")) .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/index.vue"), vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/form.vue"), vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue")) .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("api/api.ts"), vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) // Vue3 vben 模版 .put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/data.ts"), vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts")) .put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/index.vue"), vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/form.vue"), vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Modal.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("api/api.ts"), vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) .build(); @Resource private CodegenProperties codegenProperties; /** * 是否使用 jakarta 包,用于解决 Spring Boot 2.X 和 3.X 的兼容性问题 * * true - 使用 jakarta.validation.constraints.* * false - 使用 jakarta.validation.constraints.* */ @Setter // 允许设置的原因,是因为单测需要手动改变 private Boolean jakartaEnable; /** * 模板引擎,由 hutool 实现 */ private final TemplateEngine templateEngine; /** * 全局通用变量映射 */ private final Map globalBindingMap = new HashMap<>(); public CodegenEngine() { // 初始化 TemplateEngine 属性 TemplateConfig config = new TemplateConfig(); config.setResourceMode(TemplateConfig.ResourceMode.CLASSPATH); this.templateEngine = new VelocityEngine(config); // 设置 javaxEnable,按照是否使用 JDK17 来判断 this.jakartaEnable = SystemUtil.getJavaInfo().isJavaVersionAtLeast(1700); // 17.00 * 100 } @PostConstruct @VisibleForTesting void initGlobalBindingMap() { // 全局配置 globalBindingMap.put("basePackage", codegenProperties.getBasePackage()); globalBindingMap.put("baseFrameworkPackage", codegenProperties.getBasePackage() + '.' + "framework"); // 用于后续获取测试类的 package 地址 globalBindingMap.put("jakartaPackage", jakartaEnable ? "jakarta" : "javax"); // 全局 Java Bean globalBindingMap.put("CommonResultClassName", CommonResult.class.getName()); globalBindingMap.put("PageResultClassName", PageResult.class.getName()); // VO 类,独有字段 globalBindingMap.put("PageParamClassName", PageParam.class.getName()); globalBindingMap.put("DictFormatClassName", DictFormat.class.getName()); // DO 类,独有字段 globalBindingMap.put("BaseDOClassName", BaseDO.class.getName()); globalBindingMap.put("baseDOFields", CodegenBuilder.BASE_DO_FIELDS); globalBindingMap.put("QueryWrapperClassName", LambdaQueryWrapperX.class.getName()); globalBindingMap.put("BaseMapperClassName", BaseMapperX.class.getName()); // Util 工具类 globalBindingMap.put("ServiceExceptionUtilClassName", ServiceExceptionUtil.class.getName()); globalBindingMap.put("DateUtilsClassName", DateUtils.class.getName()); globalBindingMap.put("ExcelUtilsClassName", ExcelUtils.class.getName()); globalBindingMap.put("LocalDateTimeUtilsClassName", LocalDateTimeUtils.class.getName()); globalBindingMap.put("ObjectUtilsClassName", ObjectUtils.class.getName()); globalBindingMap.put("DictConvertClassName", DictConvert.class.getName()); globalBindingMap.put("ApiAccessLogClassName", ApiAccessLog.class.getName()); globalBindingMap.put("OperateTypeEnumClassName", OperateTypeEnum.class.getName()); globalBindingMap.put("BeanUtils", BeanUtils.class.getName()); } /** * 生成代码 * * @param table 表定义 * @param columns table 的字段定义数组 * @param subTables 子表数组,当且仅当主子表时使用 * @param subColumnsList subTables 的字段定义数组 * @return 生成的代码,key 是路径,value 是对应代码 */ public Map execute(CodegenTableDO table, List columns, List subTables, List> subColumnsList) { // 1.1 初始化 bindMap 上下文 Map bindingMap = initBindingMap(table, columns, subTables, subColumnsList); // 1.2 获得模版 Map templates = getTemplates(table.getFrontType()); // 2. 执行生成 Map result = Maps.newLinkedHashMapWithExpectedSize(templates.size()); // 有序 templates.forEach((vmPath, filePath) -> { // 2.1 特殊:主子表专属逻辑 if (isSubTemplate(vmPath)) { generateSubCode(table, subTables, result, vmPath, filePath, bindingMap); return; // 2.2 特殊:树表专属逻辑 } else if (isPageReqVOTemplate(vmPath)) { // 减少多余的类生成,例如说 PageVO.java 类 if (CodegenTemplateTypeEnum.isTree(table.getTemplateType())) { return; } } else if (isListReqVOTemplate(vmPath)) { // 减少多余的类生成,例如说 ListVO.java 类 if (!CodegenTemplateTypeEnum.isTree(table.getTemplateType())) { return; } } // 2.3 默认生成 generateCode(result, vmPath, filePath, bindingMap); }); return result; } private void generateCode(Map result, String vmPath, String filePath, Map bindingMap) { filePath = formatFilePath(filePath, bindingMap); String content = templateEngine.getTemplate(vmPath).render(bindingMap); // 格式化代码 content = prettyCode(content); result.put(filePath, content); } private void generateSubCode(CodegenTableDO table, List subTables, Map result, String vmPath, String filePath, Map bindingMap) { // 没有子表,所以不生成 if (CollUtil.isEmpty(subTables)) { return; } // 主子表的模式匹配。目的:过滤掉个性化的模版 if (vmPath.contains("_normal") && ObjectUtil.notEqual(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_NORMAL.getType())) { return; } if (vmPath.contains("_erp") && ObjectUtil.notEqual(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_ERP.getType())) { return; } if (vmPath.contains("_inner") && ObjectUtil.notEqual(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_INNER.getType())) { return; } // 逐个生成 for (int i = 0; i < subTables.size(); i++) { bindingMap.put("subIndex", i); generateCode(result, vmPath, filePath, bindingMap); } bindingMap.remove("subIndex"); } /** * 格式化生成后的代码 * * 因为尽量让 vm 模版简单,所以统一的处理都在这个方法。 * 如果不处理,Vue 的 Pretty 格式校验可能会报错 * * @param content 格式化前的代码 * @return 格式化后的代码 */ private String prettyCode(String content) { // Vue 界面:去除字段后面多余的 , 逗号,解决前端的 Pretty 代码格式检查的报错 content = content.replaceAll(",\n}", "\n}").replaceAll(",\n }", "\n }"); // Vue 界面:去除多的 dateFormatter,只有一个的情况下,说明没使用到 if (StrUtil.count(content, "dateFormatter") == 1) { content = StrUtils.removeLineContains(content, "dateFormatter"); } // Vue2 界面:修正 $refs if (StrUtil.count(content, "this.refs") >= 1) { content = content.replace("this.refs", "this.$refs"); } // Vue 界面:去除多的 dict 相关,只有一个的情况下,说明没使用到 if (StrUtil.count(content, "getIntDictOptions") == 1) { content = content.replace("getIntDictOptions, ", ""); } if (StrUtil.count(content, "getStrDictOptions") == 1) { content = content.replace("getStrDictOptions, ", ""); } if (StrUtil.count(content, "getBoolDictOptions") == 1) { content = content.replace("getBoolDictOptions, ", ""); } if (StrUtil.count(content, "DICT_TYPE.") == 0) { content = StrUtils.removeLineContains(content, "DICT_TYPE"); } return content; } private Map initBindingMap(CodegenTableDO table, List columns, List subTables, List> subColumnsList) { // 创建 bindingMap Map bindingMap = new HashMap<>(globalBindingMap); bindingMap.put("table", table); bindingMap.put("columns", columns); bindingMap.put("primaryColumn", CollectionUtils.findFirst(columns, CodegenColumnDO::getPrimaryKey)); // 主键字段 bindingMap.put("sceneEnum", CodegenSceneEnum.valueOf(table.getScene())); // className 相关 // 去掉指定前缀,将 TestDictType 转换成 DictType. 因为在 create 等方法后,不需要带上 Test 前缀 String simpleClassName = removePrefix(table.getClassName(), upperFirst(table.getModuleName())); bindingMap.put("simpleClassName", simpleClassName); bindingMap.put("simpleClassName_underlineCase", toUnderlineCase(simpleClassName)); // 将 DictType 转换成 dict_type bindingMap.put("classNameVar", lowerFirst(simpleClassName)); // 将 DictType 转换成 dictType,用于变量 // 将 DictType 转换成 dict-type String simpleClassNameStrikeCase = toSymbolCase(simpleClassName, '-'); bindingMap.put("simpleClassName_strikeCase", simpleClassNameStrikeCase); // permission 前缀 bindingMap.put("permissionPrefix", table.getModuleName() + ":" + simpleClassNameStrikeCase); // 特殊:树表专属逻辑 if (CodegenTemplateTypeEnum.isTree(table.getTemplateType())) { CodegenColumnDO treeParentColumn = CollUtil.findOne(columns, column -> Objects.equals(column.getId(), table.getTreeParentColumnId())); bindingMap.put("treeParentColumn", treeParentColumn); bindingMap.put("treeParentColumn_javaField_underlineCase", toUnderlineCase(treeParentColumn.getJavaField())); CodegenColumnDO treeNameColumn = CollUtil.findOne(columns, column -> Objects.equals(column.getId(), table.getTreeNameColumnId())); bindingMap.put("treeNameColumn", treeNameColumn); bindingMap.put("treeNameColumn_javaField_underlineCase", toUnderlineCase(treeNameColumn.getJavaField())); } // 特殊:主子表专属逻辑 if (CollUtil.isNotEmpty(subTables)) { // 创建 bindingMap bindingMap.put("subTables", subTables); bindingMap.put("subColumnsList", subColumnsList); List subPrimaryColumns = new ArrayList<>(); List subJoinColumns = new ArrayList<>(); List subJoinColumnStrikeCases = new ArrayList<>(); List subSimpleClassNames = new ArrayList<>(); List subClassNameVars = new ArrayList<>(); List simpleClassNameUnderlineCases = new ArrayList<>(); List subSimpleClassNameStrikeCases = new ArrayList<>(); for (int i = 0; i < subTables.size(); i++) { CodegenTableDO subTable = subTables.get(i); List subColumns = subColumnsList.get(i); subPrimaryColumns.add(CollectionUtils.findFirst(subColumns, CodegenColumnDO::getPrimaryKey)); // CodegenColumnDO subColumn = CollectionUtils.findFirst(subColumns, // 关联的字段 column -> Objects.equals(column.getId(), subTable.getSubJoinColumnId())); subJoinColumns.add(subColumn); subJoinColumnStrikeCases.add(toSymbolCase(subColumn.getJavaField(), '-')); // 将 DictType 转换成 dict-type // className 相关 String subSimpleClassName = removePrefix(subTable.getClassName(), upperFirst(subTable.getModuleName())); subSimpleClassNames.add(subSimpleClassName); simpleClassNameUnderlineCases.add(toUnderlineCase(subSimpleClassName)); // 将 DictType 转换成 dict_type subClassNameVars.add(lowerFirst(subSimpleClassName)); // 将 DictType 转换成 dictType,用于变量 subSimpleClassNameStrikeCases.add(toSymbolCase(subSimpleClassName, '-')); // 将 DictType 转换成 dict-type } bindingMap.put("subPrimaryColumns", subPrimaryColumns); bindingMap.put("subJoinColumns", subJoinColumns); bindingMap.put("subJoinColumn_strikeCases", subJoinColumnStrikeCases); bindingMap.put("subSimpleClassNames", subSimpleClassNames); bindingMap.put("simpleClassNameUnderlineCases", simpleClassNameUnderlineCases); bindingMap.put("subClassNameVars", subClassNameVars); bindingMap.put("subSimpleClassName_strikeCases", subSimpleClassNameStrikeCases); } return bindingMap; } private Map getTemplates(Integer frontType) { Map templates = new LinkedHashMap<>(); templates.putAll(SERVER_TEMPLATES); templates.putAll(FRONT_TEMPLATES.row(frontType)); return templates; } @SuppressWarnings("unchecked") private String formatFilePath(String filePath, Map bindingMap) { filePath = StrUtil.replace(filePath, "${basePackage}", getStr(bindingMap, "basePackage").replaceAll("\\.", "/")); filePath = StrUtil.replace(filePath, "${classNameVar}", getStr(bindingMap, "classNameVar")); filePath = StrUtil.replace(filePath, "${simpleClassName}", getStr(bindingMap, "simpleClassName")); // sceneEnum 包含的字段 CodegenSceneEnum sceneEnum = (CodegenSceneEnum) bindingMap.get("sceneEnum"); filePath = StrUtil.replace(filePath, "${sceneEnum.prefixClass}", sceneEnum.getPrefixClass()); filePath = StrUtil.replace(filePath, "${sceneEnum.basePackage}", sceneEnum.getBasePackage()); // table 包含的字段 CodegenTableDO table = (CodegenTableDO) bindingMap.get("table"); filePath = StrUtil.replace(filePath, "${table.moduleName}", table.getModuleName()); filePath = StrUtil.replace(filePath, "${table.businessName}", table.getBusinessName()); filePath = StrUtil.replace(filePath, "${table.className}", table.getClassName()); // 特殊:主子表专属逻辑 Integer subIndex = (Integer) bindingMap.get("subIndex"); if (subIndex != null) { CodegenTableDO subTable = ((List) bindingMap.get("subTables")).get(subIndex); filePath = StrUtil.replace(filePath, "${subTable.moduleName}", subTable.getModuleName()); filePath = StrUtil.replace(filePath, "${subTable.businessName}", subTable.getBusinessName()); filePath = StrUtil.replace(filePath, "${subTable.className}", subTable.getClassName()); filePath = StrUtil.replace(filePath, "${subSimpleClassName}", ((List) bindingMap.get("subSimpleClassNames")).get(subIndex)); } return filePath; } private static String javaTemplatePath(String path) { return "codegen/java/" + path + ".vm"; } private static String javaModuleImplVOFilePath(String path) { return javaModuleFilePath("controller/${sceneEnum.basePackage}/${table.businessName}/" + "vo/${sceneEnum.prefixClass}${table.className}" + path, "biz", "main"); } private static String javaModuleImplControllerFilePath() { return javaModuleFilePath("controller/${sceneEnum.basePackage}/${table.businessName}/" + "${sceneEnum.prefixClass}${table.className}Controller", "biz", "main"); } private static String javaModuleImplMainFilePath(String path) { return javaModuleFilePath(path, "biz", "main"); } private static String javaModuleApiMainFilePath(String path) { return javaModuleFilePath(path, "api", "main"); } private static String javaModuleImplTestFilePath(String path) { return javaModuleFilePath(path, "biz", "test"); } private static String javaModuleFilePath(String path, String module, String src) { return "yshop-module-${table.moduleName}/" + // 顶级模块 "yshop-module-${table.moduleName}-" + module + "/" + // 子模块 "src/" + src + "/java/${basePackage}/module/${table.moduleName}/" + path + ".java"; } private static String mapperXmlFilePath() { return "yshop-module-${table.moduleName}/" + // 顶级模块 "yshop-module-${table.moduleName}-biz/" + // 子模块 "src/main/resources/mapper/${table.businessName}/${table.className}Mapper.xml"; } private static String vueTemplatePath(String path) { return "codegen/vue/" + path + ".vm"; } private static String vueFilePath(String path) { return "yshop-ui-${sceneEnum.basePackage}-vue2/" + // 顶级目录 "src/" + path; } private static String vue3TemplatePath(String path) { return "codegen/vue3/" + path + ".vm"; } private static String vue3FilePath(String path) { return "yshop-ui-${sceneEnum.basePackage}-vue3/" + // 顶级目录 "src/" + path; } private static String vue3SchemaTemplatePath(String path) { return "codegen/vue3_schema/" + path + ".vm"; } private static String vue3VbenTemplatePath(String path) { return "codegen/vue3_vben/" + path + ".vm"; } private static boolean isSubTemplate(String path) { return path.contains("_sub"); } private static boolean isPageReqVOTemplate(String path) { return path.contains("pageReqVO"); } private static boolean isListReqVOTemplate(String path) { return path.contains("listReqVO"); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/config/ConfigService.java ================================================ package co.yixiang.yshop.module.infra.service.config; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.infra.controller.admin.config.vo.ConfigPageReqVO; import co.yixiang.yshop.module.infra.controller.admin.config.vo.ConfigSaveReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.config.ConfigDO; import jakarta.validation.Valid; /** * 参数配置 Service 接口 * * @author yshop */ public interface ConfigService { /** * 创建参数配置 * * @param createReqVO 创建信息 * @return 配置编号 */ Long createConfig(@Valid ConfigSaveReqVO createReqVO); /** * 更新参数配置 * * @param updateReqVO 更新信息 */ void updateConfig(@Valid ConfigSaveReqVO updateReqVO); /** * 删除参数配置 * * @param id 配置编号 */ void deleteConfig(Long id); /** * 获得参数配置 * * @param id 配置编号 * @return 参数配置 */ ConfigDO getConfig(Long id); /** * 根据参数键,获得参数配置 * * @param key 配置键 * @return 参数配置 */ ConfigDO getConfigByKey(String key); /** * 获得参数配置分页列表 * * @param reqVO 分页条件 * @return 分页列表 */ PageResult getConfigPage(@Valid ConfigPageReqVO reqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/config/ConfigServiceImpl.java ================================================ package co.yixiang.yshop.module.infra.service.config; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.infra.controller.admin.config.vo.ConfigPageReqVO; import co.yixiang.yshop.module.infra.controller.admin.config.vo.ConfigSaveReqVO; import co.yixiang.yshop.module.infra.convert.config.ConfigConvert; import co.yixiang.yshop.module.infra.dal.dataobject.config.ConfigDO; import co.yixiang.yshop.module.infra.dal.mysql.config.ConfigMapper; import co.yixiang.yshop.module.infra.enums.config.ConfigTypeEnum; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.infra.enums.ErrorCodeConstants.*; /** * 参数配置 Service 实现类 */ @Service @Slf4j @Validated public class ConfigServiceImpl implements ConfigService { @Resource private ConfigMapper configMapper; @Override public Long createConfig(ConfigSaveReqVO createReqVO) { // 校验参数配置 key 的唯一性 validateConfigKeyUnique(null, createReqVO.getKey()); // 插入参数配置 ConfigDO config = ConfigConvert.INSTANCE.convert(createReqVO); config.setType(ConfigTypeEnum.CUSTOM.getType()); configMapper.insert(config); return config.getId(); } @Override public void updateConfig(ConfigSaveReqVO updateReqVO) { // 校验自己存在 validateConfigExists(updateReqVO.getId()); // 校验参数配置 key 的唯一性 validateConfigKeyUnique(updateReqVO.getId(), updateReqVO.getKey()); // 更新参数配置 ConfigDO updateObj = ConfigConvert.INSTANCE.convert(updateReqVO); configMapper.updateById(updateObj); } @Override public void deleteConfig(Long id) { // 校验配置存在 ConfigDO config = validateConfigExists(id); // 内置配置,不允许删除 if (ConfigTypeEnum.SYSTEM.getType().equals(config.getType())) { throw exception(CONFIG_CAN_NOT_DELETE_SYSTEM_TYPE); } // 删除 configMapper.deleteById(id); } @Override public ConfigDO getConfig(Long id) { return configMapper.selectById(id); } @Override public ConfigDO getConfigByKey(String key) { return configMapper.selectByKey(key); } @Override public PageResult getConfigPage(ConfigPageReqVO pageReqVO) { return configMapper.selectPage(pageReqVO); } @VisibleForTesting public ConfigDO validateConfigExists(Long id) { if (id == null) { return null; } ConfigDO config = configMapper.selectById(id); if (config == null) { throw exception(CONFIG_NOT_EXISTS); } return config; } @VisibleForTesting public void validateConfigKeyUnique(Long id, String key) { ConfigDO config = configMapper.selectByKey(key); if (config == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的参数配置 if (id == null) { throw exception(CONFIG_KEY_DUPLICATE); } if (!config.getId().equals(id)) { throw exception(CONFIG_KEY_DUPLICATE); } } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/db/DataSourceConfigService.java ================================================ package co.yixiang.yshop.module.infra.service.db; import co.yixiang.yshop.module.infra.controller.admin.db.vo.DataSourceConfigSaveReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.db.DataSourceConfigDO; import jakarta.validation.Valid; import java.util.List; /** * 数据源配置 Service 接口 * * @author yshop */ public interface DataSourceConfigService { /** * 创建数据源配置 * * @param createReqVO 创建信息 * @return 编号 */ Long createDataSourceConfig(@Valid DataSourceConfigSaveReqVO createReqVO); /** * 更新数据源配置 * * @param updateReqVO 更新信息 */ void updateDataSourceConfig(@Valid DataSourceConfigSaveReqVO updateReqVO); /** * 删除数据源配置 * * @param id 编号 */ void deleteDataSourceConfig(Long id); /** * 获得数据源配置 * * @param id 编号 * @return 数据源配置 */ DataSourceConfigDO getDataSourceConfig(Long id); /** * 获得数据源配置列表 * * @return 数据源配置列表 */ List getDataSourceConfigList(); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/db/DataSourceConfigServiceImpl.java ================================================ package co.yixiang.yshop.module.infra.service.db; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.mybatis.core.util.JdbcUtils; import co.yixiang.yshop.module.infra.controller.admin.db.vo.DataSourceConfigSaveReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.db.DataSourceConfigDO; import co.yixiang.yshop.module.infra.dal.mysql.db.DataSourceConfigMapper; import com.baomidou.dynamic.datasource.creator.DataSourceProperty; import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.List; import java.util.Objects; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.infra.enums.ErrorCodeConstants.DATA_SOURCE_CONFIG_NOT_EXISTS; import static co.yixiang.yshop.module.infra.enums.ErrorCodeConstants.DATA_SOURCE_CONFIG_NOT_OK; /** * 数据源配置 Service 实现类 * * @author yshop */ @Service @Validated public class DataSourceConfigServiceImpl implements DataSourceConfigService { @Resource private DataSourceConfigMapper dataSourceConfigMapper; @Resource private DynamicDataSourceProperties dynamicDataSourceProperties; @Override public Long createDataSourceConfig(DataSourceConfigSaveReqVO createReqVO) { DataSourceConfigDO config = BeanUtils.toBean(createReqVO, DataSourceConfigDO.class); validateConnectionOK(config); // 插入 dataSourceConfigMapper.insert(config); // 返回 return config.getId(); } @Override public void updateDataSourceConfig(DataSourceConfigSaveReqVO updateReqVO) { // 校验存在 validateDataSourceConfigExists(updateReqVO.getId()); DataSourceConfigDO updateObj = BeanUtils.toBean(updateReqVO, DataSourceConfigDO.class); validateConnectionOK(updateObj); // 更新 dataSourceConfigMapper.updateById(updateObj); } @Override public void deleteDataSourceConfig(Long id) { // 校验存在 validateDataSourceConfigExists(id); // 删除 dataSourceConfigMapper.deleteById(id); } private void validateDataSourceConfigExists(Long id) { if (dataSourceConfigMapper.selectById(id) == null) { throw exception(DATA_SOURCE_CONFIG_NOT_EXISTS); } } @Override public DataSourceConfigDO getDataSourceConfig(Long id) { // 如果 id 为 0,默认为 master 的数据源 if (Objects.equals(id, DataSourceConfigDO.ID_MASTER)) { return buildMasterDataSourceConfig(); } // 从 DB 中读取 return dataSourceConfigMapper.selectById(id); } @Override public List getDataSourceConfigList() { List result = dataSourceConfigMapper.selectList(); // 补充 master 数据源 result.add(0, buildMasterDataSourceConfig()); return result; } private void validateConnectionOK(DataSourceConfigDO config) { boolean success = JdbcUtils.isConnectionOK(config.getUrl(), config.getUsername(), config.getPassword()); if (!success) { throw exception(DATA_SOURCE_CONFIG_NOT_OK); } } private DataSourceConfigDO buildMasterDataSourceConfig() { String primary = dynamicDataSourceProperties.getPrimary(); DataSourceProperty dataSourceProperty = dynamicDataSourceProperties.getDatasource().get(primary); return new DataSourceConfigDO().setId(DataSourceConfigDO.ID_MASTER).setName(primary) .setUrl(dataSourceProperty.getUrl()) .setUsername(dataSourceProperty.getUsername()) .setPassword(dataSourceProperty.getPassword()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/db/DatabaseTableService.java ================================================ package co.yixiang.yshop.module.infra.service.db; import com.baomidou.mybatisplus.generator.config.po.TableInfo; import java.util.List; /** * 数据库表 Service * * @author yshop */ public interface DatabaseTableService { /** * 获得表列表,基于表名称 + 表描述进行模糊匹配 * * @param dataSourceConfigId 数据源配置的编号 * @param nameLike 表名称,模糊匹配 * @param commentLike 表描述,模糊匹配 * @return 表列表 */ List getTableList(Long dataSourceConfigId, String nameLike, String commentLike); /** * 获得指定表名 * * @param dataSourceConfigId 数据源配置的编号 * @param tableName 表名称 * @return 表 */ TableInfo getTable(Long dataSourceConfigId, String tableName); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/db/DatabaseTableServiceImpl.java ================================================ package co.yixiang.yshop.module.infra.service.db; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.mybatis.core.util.JdbcUtils; import co.yixiang.yshop.module.infra.dal.dataobject.db.DataSourceConfigDO; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.generator.config.DataSourceConfig; import com.baomidou.mybatisplus.generator.config.GlobalConfig; import com.baomidou.mybatisplus.generator.config.StrategyConfig; import com.baomidou.mybatisplus.generator.config.builder.ConfigBuilder; import com.baomidou.mybatisplus.generator.config.po.TableInfo; import com.baomidou.mybatisplus.generator.config.rules.DateType; import com.baomidou.mybatisplus.generator.query.SQLQuery; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; /** * 数据库表 Service 实现类 * * @author yshop */ @Service public class DatabaseTableServiceImpl implements DatabaseTableService { @Resource private DataSourceConfigService dataSourceConfigService; @Override public List getTableList(Long dataSourceConfigId, String nameLike, String commentLike) { List tables = getTableList0(dataSourceConfigId, null); return tables.stream().filter(tableInfo -> (StrUtil.isEmpty(nameLike) || tableInfo.getName().contains(nameLike)) && (StrUtil.isEmpty(commentLike) || tableInfo.getComment().contains(commentLike))) .collect(Collectors.toList()); } @Override public TableInfo getTable(Long dataSourceConfigId, String name) { return CollUtil.getFirst(getTableList0(dataSourceConfigId, name)); } private List getTableList0(Long dataSourceConfigId, String name) { // 获得数据源配置 DataSourceConfigDO config = dataSourceConfigService.getDataSourceConfig(dataSourceConfigId); Assert.notNull(config, "数据源({}) 不存在!", dataSourceConfigId); DbType dbType = JdbcUtils.getDbType(config.getUrl()); // 使用 MyBatis Plus Generator 解析表结构 DataSourceConfig.Builder dataSourceConfigBuilder = new DataSourceConfig.Builder(config.getUrl(), config.getUsername(), config.getPassword()); if (Objects.equals(dbType, DbType.SQL_SERVER)) { // 特殊:SQLServer jdbc 非标准,参见 https://github.com/baomidou/mybatis-plus/issues/5419 dataSourceConfigBuilder.databaseQueryClass(SQLQuery.class); } StrategyConfig.Builder strategyConfig = new StrategyConfig.Builder().enableSkipView(); // 忽略视图,业务上一般用不到 if (StrUtil.isNotEmpty(name)) { strategyConfig.addInclude(name); } else { // 移除工作流和定时任务前缀的表名 strategyConfig.addExclude("ACT_[\\S\\s]+|QRTZ_[\\S\\s]+|FLW_[\\S\\s]+"); // 移除 ORACLE 相关的系统表 strategyConfig.addExclude("IMPDP_[\\S\\s]+|ALL_[\\S\\s]+|HS_[\\S\\\\s]+"); strategyConfig.addExclude("[\\S\\s]+\\$[\\S\\s]+|[\\S\\s]+\\$"); // 表里不能有 $,一般有都是系统的表 } GlobalConfig globalConfig = new GlobalConfig.Builder().dateType(DateType.TIME_PACK).build(); // 只使用 LocalDateTime 类型,不使用 LocalDate ConfigBuilder builder = new ConfigBuilder(null, dataSourceConfigBuilder.build(), strategyConfig.build(), null, globalConfig, null); // 按照名字排序 List tables = builder.getTableInfoList(); tables.sort(Comparator.comparing(TableInfo::getName)); return tables; } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/demo/demo01/Demo01ContactService.java ================================================ package co.yixiang.yshop.module.infra.service.demo.demo01; import jakarta.validation.*; import co.yixiang.yshop.module.infra.controller.admin.demo.demo01.vo.Demo01ContactPageReqVO; import co.yixiang.yshop.module.infra.controller.admin.demo.demo01.vo.Demo01ContactSaveReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo01.Demo01ContactDO; import co.yixiang.yshop.framework.common.pojo.PageResult; /** * 示例联系人 Service 接口 * * @author yshop */ public interface Demo01ContactService { /** * 创建示例联系人 * * @param createReqVO 创建信息 * @return 编号 */ Long createDemo01Contact(@Valid Demo01ContactSaveReqVO createReqVO); /** * 更新示例联系人 * * @param updateReqVO 更新信息 */ void updateDemo01Contact(@Valid Demo01ContactSaveReqVO updateReqVO); /** * 删除示例联系人 * * @param id 编号 */ void deleteDemo01Contact(Long id); /** * 获得示例联系人 * * @param id 编号 * @return 示例联系人 */ Demo01ContactDO getDemo01Contact(Long id); /** * 获得示例联系人分页 * * @param pageReqVO 分页查询 * @return 示例联系人分页 */ PageResult getDemo01ContactPage(Demo01ContactPageReqVO pageReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/demo/demo01/Demo01ContactServiceImpl.java ================================================ package co.yixiang.yshop.module.infra.service.demo.demo01; import co.yixiang.yshop.module.infra.controller.admin.demo.demo01.vo.Demo01ContactPageReqVO; import co.yixiang.yshop.module.infra.controller.admin.demo.demo01.vo.Demo01ContactSaveReqVO; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo01.Demo01ContactDO; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.infra.dal.mysql.demo.demo01.Demo01ContactMapper; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.infra.enums.ErrorCodeConstants.*; /** * 示例联系人 Service 实现类 * * @author yshop */ @Service @Validated public class Demo01ContactServiceImpl implements Demo01ContactService { @Resource private Demo01ContactMapper demo01ContactMapper; @Override public Long createDemo01Contact(Demo01ContactSaveReqVO createReqVO) { // 插入 Demo01ContactDO demo01Contact = BeanUtils.toBean(createReqVO, Demo01ContactDO.class); demo01ContactMapper.insert(demo01Contact); // 返回 return demo01Contact.getId(); } @Override public void updateDemo01Contact(Demo01ContactSaveReqVO updateReqVO) { // 校验存在 validateDemo01ContactExists(updateReqVO.getId()); // 更新 Demo01ContactDO updateObj = BeanUtils.toBean(updateReqVO, Demo01ContactDO.class); demo01ContactMapper.updateById(updateObj); } @Override public void deleteDemo01Contact(Long id) { // 校验存在 validateDemo01ContactExists(id); // 删除 demo01ContactMapper.deleteById(id); } private void validateDemo01ContactExists(Long id) { if (demo01ContactMapper.selectById(id) == null) { throw exception(DEMO01_CONTACT_NOT_EXISTS); } } @Override public Demo01ContactDO getDemo01Contact(Long id) { return demo01ContactMapper.selectById(id); } @Override public PageResult getDemo01ContactPage(Demo01ContactPageReqVO pageReqVO) { return demo01ContactMapper.selectPage(pageReqVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/demo/demo02/Demo02CategoryService.java ================================================ package co.yixiang.yshop.module.infra.service.demo.demo02; import java.util.*; import jakarta.validation.*; import co.yixiang.yshop.module.infra.controller.admin.demo.demo02.vo.Demo02CategoryListReqVO; import co.yixiang.yshop.module.infra.controller.admin.demo.demo02.vo.Demo02CategorySaveReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo02.Demo02CategoryDO; /** * 示例分类 Service 接口 * * @author yshop */ public interface Demo02CategoryService { /** * 创建示例分类 * * @param createReqVO 创建信息 * @return 编号 */ Long createDemo02Category(@Valid Demo02CategorySaveReqVO createReqVO); /** * 更新示例分类 * * @param updateReqVO 更新信息 */ void updateDemo02Category(@Valid Demo02CategorySaveReqVO updateReqVO); /** * 删除示例分类 * * @param id 编号 */ void deleteDemo02Category(Long id); /** * 获得示例分类 * * @param id 编号 * @return 示例分类 */ Demo02CategoryDO getDemo02Category(Long id); /** * 获得示例分类列表 * * @param listReqVO 查询条件 * @return 示例分类列表 */ List getDemo02CategoryList(Demo02CategoryListReqVO listReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/demo/demo02/Demo02CategoryServiceImpl.java ================================================ package co.yixiang.yshop.module.infra.service.demo.demo02; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.infra.controller.admin.demo.demo02.vo.Demo02CategoryListReqVO; import co.yixiang.yshop.module.infra.controller.admin.demo.demo02.vo.Demo02CategorySaveReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo02.Demo02CategoryDO; import co.yixiang.yshop.module.infra.dal.mysql.demo.demo02.Demo02CategoryMapper; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.List; import java.util.Objects; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.infra.enums.ErrorCodeConstants.*; /** * 示例分类 Service 实现类 * * @author yshop */ @Service @Validated public class Demo02CategoryServiceImpl implements Demo02CategoryService { @Resource private Demo02CategoryMapper demo02CategoryMapper; @Override public Long createDemo02Category(Demo02CategorySaveReqVO createReqVO) { // 校验父级编号的有效性 validateParentDemo02Category(null, createReqVO.getParentId()); // 校验名字的唯一性 validateDemo02CategoryNameUnique(null, createReqVO.getParentId(), createReqVO.getName()); // 插入 Demo02CategoryDO demo02Category = BeanUtils.toBean(createReqVO, Demo02CategoryDO.class); demo02CategoryMapper.insert(demo02Category); // 返回 return demo02Category.getId(); } @Override public void updateDemo02Category(Demo02CategorySaveReqVO updateReqVO) { // 校验存在 validateDemo02CategoryExists(updateReqVO.getId()); // 校验父级编号的有效性 validateParentDemo02Category(updateReqVO.getId(), updateReqVO.getParentId()); // 校验名字的唯一性 validateDemo02CategoryNameUnique(updateReqVO.getId(), updateReqVO.getParentId(), updateReqVO.getName()); // 更新 Demo02CategoryDO updateObj = BeanUtils.toBean(updateReqVO, Demo02CategoryDO.class); demo02CategoryMapper.updateById(updateObj); } @Override public void deleteDemo02Category(Long id) { // 校验存在 validateDemo02CategoryExists(id); // 校验是否有子示例分类 if (demo02CategoryMapper.selectCountByParentId(id) > 0) { throw exception(DEMO02_CATEGORY_EXITS_CHILDREN); } // 删除 demo02CategoryMapper.deleteById(id); } private void validateDemo02CategoryExists(Long id) { if (demo02CategoryMapper.selectById(id) == null) { throw exception(DEMO02_CATEGORY_NOT_EXISTS); } } private void validateParentDemo02Category(Long id, Long parentId) { if (parentId == null || Demo02CategoryDO.PARENT_ID_ROOT.equals(parentId)) { return; } // 1. 不能设置自己为父示例分类 if (Objects.equals(id, parentId)) { throw exception(DEMO02_CATEGORY_PARENT_ERROR); } // 2. 父示例分类不存在 Demo02CategoryDO parentDemo02Category = demo02CategoryMapper.selectById(parentId); if (parentDemo02Category == null) { throw exception(DEMO02_CATEGORY_PARENT_NOT_EXITS); } // 3. 递归校验父示例分类,如果父示例分类是自己的子示例分类,则报错,避免形成环路 if (id == null) { // id 为空,说明新增,不需要考虑环路 return; } for (int i = 0; i < Short.MAX_VALUE; i++) { // 3.1 校验环路 parentId = parentDemo02Category.getParentId(); if (Objects.equals(id, parentId)) { throw exception(DEMO02_CATEGORY_PARENT_IS_CHILD); } // 3.2 继续递归下一级父示例分类 if (parentId == null || Demo02CategoryDO.PARENT_ID_ROOT.equals(parentId)) { break; } parentDemo02Category = demo02CategoryMapper.selectById(parentId); if (parentDemo02Category == null) { break; } } } private void validateDemo02CategoryNameUnique(Long id, Long parentId, String name) { Demo02CategoryDO demo02Category = demo02CategoryMapper.selectByParentIdAndName(parentId, name); if (demo02Category == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的示例分类 if (id == null) { throw exception(DEMO02_CATEGORY_NAME_DUPLICATE); } if (!Objects.equals(demo02Category.getId(), id)) { throw exception(DEMO02_CATEGORY_NAME_DUPLICATE); } } @Override public Demo02CategoryDO getDemo02Category(Long id) { return demo02CategoryMapper.selectById(id); } @Override public List getDemo02CategoryList(Demo02CategoryListReqVO listReqVO) { return demo02CategoryMapper.selectList(listReqVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/demo/demo03/Demo03StudentService.java ================================================ package co.yixiang.yshop.module.infra.service.demo.demo03; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.infra.controller.admin.demo.demo03.vo.Demo03StudentPageReqVO; import co.yixiang.yshop.module.infra.controller.admin.demo.demo03.vo.Demo03StudentSaveReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO; import jakarta.validation.Valid; import java.util.List; /** * 学生 Service 接口 * * @author yshop */ public interface Demo03StudentService { /** * 创建学生 * * @param createReqVO 创建信息 * @return 编号 */ Long createDemo03Student(@Valid Demo03StudentSaveReqVO createReqVO); /** * 更新学生 * * @param updateReqVO 更新信息 */ void updateDemo03Student(@Valid Demo03StudentSaveReqVO updateReqVO); /** * 删除学生 * * @param id 编号 */ void deleteDemo03Student(Long id); /** * 获得学生 * * @param id 编号 * @return 学生 */ Demo03StudentDO getDemo03Student(Long id); /** * 获得学生分页 * * @param pageReqVO 分页查询 * @return 学生分页 */ PageResult getDemo03StudentPage(Demo03StudentPageReqVO pageReqVO); // ==================== 子表(学生课程) ==================== /** * 获得学生课程列表 * * @param studentId 学生编号 * @return 学生课程列表 */ List getDemo03CourseListByStudentId(Long studentId); /** * 获得学生课程分页 * * @param pageReqVO 分页查询 * @param studentId 学生编号 * @return 学生课程分页 */ PageResult getDemo03CoursePage(PageParam pageReqVO, Long studentId); /** * 创建学生课程 * * @param demo03Course 创建信息 * @return 编号 */ Long createDemo03Course(@Valid Demo03CourseDO demo03Course); /** * 更新学生课程 * * @param demo03Course 更新信息 */ void updateDemo03Course(@Valid Demo03CourseDO demo03Course); /** * 删除学生课程 * * @param id 编号 */ void deleteDemo03Course(Long id); /** * 获得学生课程 * * @param id 编号 * @return 学生课程 */ Demo03CourseDO getDemo03Course(Long id); // ==================== 子表(学生班级) ==================== /** * 获得学生班级 * * @param studentId 学生编号 * @return 学生班级 */ Demo03GradeDO getDemo03GradeByStudentId(Long studentId); /** * 获得学生班级分页 * * @param pageReqVO 分页查询 * @param studentId 学生编号 * @return 学生班级分页 */ PageResult getDemo03GradePage(PageParam pageReqVO, Long studentId); /** * 创建学生班级 * * @param demo03Grade 创建信息 * @return 编号 */ Long createDemo03Grade(@Valid Demo03GradeDO demo03Grade); /** * 更新学生班级 * * @param demo03Grade 更新信息 */ void updateDemo03Grade(@Valid Demo03GradeDO demo03Grade); /** * 删除学生班级 * * @param id 编号 */ void deleteDemo03Grade(Long id); /** * 获得学生班级 * * @param id 编号 * @return 学生班级 */ Demo03GradeDO getDemo03Grade(Long id); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/demo/demo03/Demo03StudentServiceImpl.java ================================================ package co.yixiang.yshop.module.infra.service.demo.demo03; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.infra.controller.admin.demo.demo03.vo.Demo03StudentPageReqVO; import co.yixiang.yshop.module.infra.controller.admin.demo.demo03.vo.Demo03StudentSaveReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; import co.yixiang.yshop.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO; import co.yixiang.yshop.module.infra.dal.mysql.demo.demo03.Demo03CourseMapper; import co.yixiang.yshop.module.infra.dal.mysql.demo.demo03.Demo03GradeMapper; import co.yixiang.yshop.module.infra.dal.mysql.demo.demo03.Demo03StudentMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.infra.enums.ErrorCodeConstants.*; /** * 学生 Service 实现类 * * @author yshop */ @Service @Validated public class Demo03StudentServiceImpl implements Demo03StudentService { @Resource private Demo03StudentMapper demo03StudentMapper; @Resource private Demo03CourseMapper demo03CourseMapper; @Resource private Demo03GradeMapper demo03GradeMapper; @Override @Transactional(rollbackFor = Exception.class) public Long createDemo03Student(Demo03StudentSaveReqVO createReqVO) { // 插入 Demo03StudentDO demo03Student = BeanUtils.toBean(createReqVO, Demo03StudentDO.class); demo03StudentMapper.insert(demo03Student); // 插入子表 createDemo03CourseList(demo03Student.getId(), createReqVO.getDemo03Courses()); createDemo03Grade(demo03Student.getId(), createReqVO.getDemo03Grade()); // 返回 return demo03Student.getId(); } @Override @Transactional(rollbackFor = Exception.class) public void updateDemo03Student(Demo03StudentSaveReqVO updateReqVO) { // 校验存在 validateDemo03StudentExists(updateReqVO.getId()); // 更新 Demo03StudentDO updateObj = BeanUtils.toBean(updateReqVO, Demo03StudentDO.class); demo03StudentMapper.updateById(updateObj); // 更新子表 updateDemo03CourseList(updateReqVO.getId(), updateReqVO.getDemo03Courses()); updateDemo03Grade(updateReqVO.getId(), updateReqVO.getDemo03Grade()); } @Override @Transactional(rollbackFor = Exception.class) public void deleteDemo03Student(Long id) { // 校验存在 validateDemo03StudentExists(id); // 删除 demo03StudentMapper.deleteById(id); // 删除子表 deleteDemo03CourseByStudentId(id); deleteDemo03GradeByStudentId(id); } private void validateDemo03StudentExists(Long id) { if (demo03StudentMapper.selectById(id) == null) { throw exception(DEMO03_STUDENT_NOT_EXISTS); } } @Override public Demo03StudentDO getDemo03Student(Long id) { return demo03StudentMapper.selectById(id); } @Override public PageResult getDemo03StudentPage(Demo03StudentPageReqVO pageReqVO) { return demo03StudentMapper.selectPage(pageReqVO); } // ==================== 子表(学生课程) ==================== @Override public List getDemo03CourseListByStudentId(Long studentId) { return demo03CourseMapper.selectListByStudentId(studentId); } private void createDemo03CourseList(Long studentId, List list) { if (list != null) { list.forEach(o -> o.setStudentId(studentId)); } demo03CourseMapper.insertBatch(list); } private void updateDemo03CourseList(Long studentId, List list) { deleteDemo03CourseByStudentId(studentId); list.forEach(o -> o.setId(null).setUpdater(null).setUpdateTime(null)); // 解决更新情况下:1)id 冲突;2)updateTime 不更新 createDemo03CourseList(studentId, list); } private void deleteDemo03CourseByStudentId(Long studentId) { demo03CourseMapper.deleteByStudentId(studentId); } @Override public PageResult getDemo03CoursePage(PageParam pageReqVO, Long studentId) { return demo03CourseMapper.selectPage(pageReqVO, studentId); } @Override public Long createDemo03Course(Demo03CourseDO demo03Course) { demo03CourseMapper.insert(demo03Course); return demo03Course.getId(); } @Override public void updateDemo03Course(Demo03CourseDO demo03Course) { demo03CourseMapper.updateById(demo03Course); } @Override public void deleteDemo03Course(Long id) { demo03CourseMapper.deleteById(id); } @Override public Demo03CourseDO getDemo03Course(Long id) { return demo03CourseMapper.selectById(id); } // ==================== 子表(学生班级) ==================== @Override public Demo03GradeDO getDemo03GradeByStudentId(Long studentId) { return demo03GradeMapper.selectByStudentId(studentId); } private void createDemo03Grade(Long studentId, Demo03GradeDO demo03Grade) { if (demo03Grade == null) { return; } demo03Grade.setStudentId(studentId); demo03GradeMapper.insert(demo03Grade); } private void updateDemo03Grade(Long studentId, Demo03GradeDO demo03Grade) { if (demo03Grade == null) { return; } demo03Grade.setStudentId(studentId); demo03Grade.setUpdater(null).setUpdateTime(null); // 解决更新情况下:updateTime 不更新 demo03GradeMapper.insertOrUpdate(demo03Grade); } private void deleteDemo03GradeByStudentId(Long studentId) { demo03GradeMapper.deleteByStudentId(studentId); } @Override public PageResult getDemo03GradePage(PageParam pageReqVO, Long studentId) { return demo03GradeMapper.selectPage(pageReqVO, studentId); } @Override public Long createDemo03Grade(Demo03GradeDO demo03Grade) { // 校验是否已经存在 if (demo03GradeMapper.selectByStudentId(demo03Grade.getStudentId()) != null) { throw exception(DEMO03_GRADE_EXISTS); } demo03GradeMapper.insert(demo03Grade); return demo03Grade.getId(); } @Override public void updateDemo03Grade(Demo03GradeDO demo03Grade) { // 校验存在 validateDemo03GradeExists(demo03Grade.getId()); // 更新 demo03GradeMapper.updateById(demo03Grade); } @Override public void deleteDemo03Grade(Long id) { // 校验存在 validateDemo03GradeExists(id); // 删除 demo03GradeMapper.deleteById(id); } @Override public Demo03GradeDO getDemo03Grade(Long id) { return demo03GradeMapper.selectById(id); } private void validateDemo03GradeExists(Long id) { if (demo03GradeMapper.selectById(id) == null) { throw exception(DEMO03_GRADE_NOT_EXISTS); } } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/file/FileConfigService.java ================================================ package co.yixiang.yshop.module.infra.service.file; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.infra.framework.file.core.client.FileClient; import co.yixiang.yshop.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; import co.yixiang.yshop.module.infra.controller.admin.file.vo.config.FileConfigSaveReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.file.FileConfigDO; import jakarta.validation.Valid; /** * 文件配置 Service 接口 * * @author yshop */ public interface FileConfigService { /** * 创建文件配置 * * @param createReqVO 创建信息 * @return 编号 */ Long createFileConfig(@Valid FileConfigSaveReqVO createReqVO); /** * 更新文件配置 * * @param updateReqVO 更新信息 */ void updateFileConfig(@Valid FileConfigSaveReqVO updateReqVO); /** * 更新文件配置为 Master * * @param id 编号 */ void updateFileConfigMaster(Long id); /** * 删除文件配置 * * @param id 编号 */ void deleteFileConfig(Long id); /** * 获得文件配置 * * @param id 编号 * @return 文件配置 */ FileConfigDO getFileConfig(Long id); /** * 获得文件配置分页 * * @param pageReqVO 分页查询 * @return 文件配置分页 */ PageResult getFileConfigPage(FileConfigPageReqVO pageReqVO); /** * 测试文件配置是否正确,通过上传文件 * * @param id 编号 * @return 文件 URL */ String testFileConfig(Long id) throws Exception; /** * 获得指定编号的文件客户端 * * @param id 配置编号 * @return 文件客户端 */ FileClient getFileClient(Long id); /** * 获得 Master 文件客户端 * * @return 文件客户端 */ FileClient getMasterFileClient(); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/file/FileConfigServiceImpl.java ================================================ package co.yixiang.yshop.module.infra.service.file; import cn.hutool.core.io.resource.ResourceUtil; import cn.hutool.core.util.IdUtil; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.framework.common.util.validation.ValidationUtils; import co.yixiang.yshop.module.infra.framework.file.core.client.FileClient; import co.yixiang.yshop.module.infra.framework.file.core.client.FileClientConfig; import co.yixiang.yshop.module.infra.framework.file.core.client.FileClientFactory; import co.yixiang.yshop.module.infra.framework.file.core.enums.FileStorageEnum; import co.yixiang.yshop.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; import co.yixiang.yshop.module.infra.controller.admin.file.vo.config.FileConfigSaveReqVO; import co.yixiang.yshop.module.infra.convert.file.FileConfigConvert; import co.yixiang.yshop.module.infra.dal.dataobject.file.FileConfigDO; import co.yixiang.yshop.module.infra.dal.mysql.file.FileConfigMapper; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import jakarta.validation.Validator; import java.time.Duration; import java.util.Map; import java.util.Objects; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache; import static co.yixiang.yshop.module.infra.enums.ErrorCodeConstants.FILE_CONFIG_DELETE_FAIL_MASTER; import static co.yixiang.yshop.module.infra.enums.ErrorCodeConstants.FILE_CONFIG_NOT_EXISTS; /** * 文件配置 Service 实现类 * * @author yshop */ @Service @Validated @Slf4j public class FileConfigServiceImpl implements FileConfigService { private static final Long CACHE_MASTER_ID = 0L; /** * {@link FileClient} 缓存,通过它异步刷新 fileClientFactory */ @Getter private final LoadingCache clientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L), new CacheLoader() { @Override public FileClient load(Long id) { FileConfigDO config = Objects.equals(CACHE_MASTER_ID, id) ? fileConfigMapper.selectByMaster() : fileConfigMapper.selectById(id); if (config != null) { fileClientFactory.createOrUpdateFileClient(config.getId(), config.getStorage(), config.getConfig()); } return fileClientFactory.getFileClient(null == config ? id : config.getId()); } }); @Resource private FileClientFactory fileClientFactory; @Resource private FileConfigMapper fileConfigMapper; @Resource private Validator validator; @Override public Long createFileConfig(FileConfigSaveReqVO createReqVO) { FileConfigDO fileConfig = FileConfigConvert.INSTANCE.convert(createReqVO) .setConfig(parseClientConfig(createReqVO.getStorage(), createReqVO.getConfig())) .setMaster(false); // 默认非 master fileConfigMapper.insert(fileConfig); return fileConfig.getId(); } @Override public void updateFileConfig(FileConfigSaveReqVO updateReqVO) { // 校验存在 FileConfigDO config = validateFileConfigExists(updateReqVO.getId()); // 更新 FileConfigDO updateObj = FileConfigConvert.INSTANCE.convert(updateReqVO) .setConfig(parseClientConfig(config.getStorage(), updateReqVO.getConfig())); fileConfigMapper.updateById(updateObj); // 清空缓存 clearCache(config.getId(), null); } @Override @Transactional(rollbackFor = Exception.class) public void updateFileConfigMaster(Long id) { // 校验存在 validateFileConfigExists(id); // 更新其它为非 master fileConfigMapper.updateBatch(new FileConfigDO().setMaster(false)); // 更新 fileConfigMapper.updateById(new FileConfigDO().setId(id).setMaster(true)); // 清空缓存 clearCache(null, true); } private FileClientConfig parseClientConfig(Integer storage, Map config) { // 获取配置类 Class configClass = FileStorageEnum.getByStorage(storage) .getConfigClass(); FileClientConfig clientConfig = JsonUtils.parseObject2(JsonUtils.toJsonString(config), configClass); // 参数校验 ValidationUtils.validate(validator, clientConfig); // 设置参数 return clientConfig; } @Override public void deleteFileConfig(Long id) { // 校验存在 FileConfigDO config = validateFileConfigExists(id); if (Boolean.TRUE.equals(config.getMaster())) { throw exception(FILE_CONFIG_DELETE_FAIL_MASTER); } // 删除 fileConfigMapper.deleteById(id); // 清空缓存 clearCache(id, null); } /** * 清空指定文件配置 * * @param id 配置编号 * @param master 是否主配置 */ private void clearCache(Long id, Boolean master) { if (id != null) { clientCache.invalidate(id); } if (Boolean.TRUE.equals(master)) { clientCache.invalidate(CACHE_MASTER_ID); } } private FileConfigDO validateFileConfigExists(Long id) { FileConfigDO config = fileConfigMapper.selectById(id); if (config == null) { throw exception(FILE_CONFIG_NOT_EXISTS); } return config; } @Override public FileConfigDO getFileConfig(Long id) { return fileConfigMapper.selectById(id); } @Override public PageResult getFileConfigPage(FileConfigPageReqVO pageReqVO) { return fileConfigMapper.selectPage(pageReqVO); } @Override public String testFileConfig(Long id) throws Exception { // 校验存在 validateFileConfigExists(id); // 上传文件 byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); return getFileClient(id).upload(content, IdUtil.fastSimpleUUID() + ".jpg", "image/jpeg"); } @Override public FileClient getFileClient(Long id) { return clientCache.getUnchecked(id); } @Override public FileClient getMasterFileClient() { return clientCache.getUnchecked(CACHE_MASTER_ID); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/file/FileService.java ================================================ package co.yixiang.yshop.module.infra.service.file; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.infra.controller.admin.file.vo.file.FileCreateReqVO; import co.yixiang.yshop.module.infra.controller.admin.file.vo.file.FilePageReqVO; import co.yixiang.yshop.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO; import co.yixiang.yshop.module.infra.dal.dataobject.file.FileDO; /** * 文件 Service 接口 * * @author yshop */ public interface FileService { /** * 获得文件分页 * * @param pageReqVO 分页查询 * @return 文件分页 */ PageResult getFilePage(FilePageReqVO pageReqVO); /** * 保存文件,并返回文件的访问路径 * * @param name 文件名称 * @param path 文件路径 * @param content 文件内容 * @return 文件路径 */ String createFile(String name, String path, byte[] content); /** * 创建文件 * * @param createReqVO 创建信息 * @return 编号 */ Long createFile(FileCreateReqVO createReqVO); /** * 删除文件 * * @param id 编号 */ void deleteFile(Long id) throws Exception; /** * 获得文件内容 * * @param configId 配置编号 * @param path 文件路径 * @return 文件内容 */ byte[] getFileContent(Long configId, String path) throws Exception; /** * 生成文件预签名地址信息 * * @param path 文件路径 * @return 预签名地址信息 */ FilePresignedUrlRespVO getFilePresignedUrl(String path) throws Exception; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/file/FileServiceImpl.java ================================================ package co.yixiang.yshop.module.infra.service.file; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.io.FileUtils; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.infra.framework.file.core.client.FileClient; import co.yixiang.yshop.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO; import co.yixiang.yshop.module.infra.framework.file.core.utils.FileTypeUtils; import co.yixiang.yshop.module.infra.controller.admin.file.vo.file.FileCreateReqVO; import co.yixiang.yshop.module.infra.controller.admin.file.vo.file.FilePageReqVO; import co.yixiang.yshop.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO; import co.yixiang.yshop.module.infra.dal.dataobject.file.FileDO; import co.yixiang.yshop.module.infra.dal.mysql.file.FileMapper; import jakarta.annotation.Resource; import lombok.SneakyThrows; import org.springframework.stereotype.Service; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS; /** * 文件 Service 实现类 * * @author yshop */ @Service public class FileServiceImpl implements FileService { @Resource private FileConfigService fileConfigService; @Resource private FileMapper fileMapper; @Override public PageResult getFilePage(FilePageReqVO pageReqVO) { return fileMapper.selectPage(pageReqVO); } @Override @SneakyThrows public String createFile(String name, String path, byte[] content) { // 计算默认的 path 名 String type = FileTypeUtils.getMineType(content, name); if (StrUtil.isEmpty(path)) { path = FileUtils.generatePath(content, name); } // 如果 name 为空,则使用 path 填充 if (StrUtil.isEmpty(name)) { name = path; } // 上传到文件存储器 FileClient client = fileConfigService.getMasterFileClient(); Assert.notNull(client, "客户端(master) 不能为空"); String url = client.upload(content, path, type); // 保存到数据库 FileDO file = new FileDO(); file.setConfigId(client.getId()); file.setName(name); file.setPath(path); file.setUrl(url); file.setType(type); file.setSize(content.length); fileMapper.insert(file); return url; } @Override public Long createFile(FileCreateReqVO createReqVO) { FileDO file = BeanUtils.toBean(createReqVO, FileDO.class); fileMapper.insert(file); return file.getId(); } @Override public void deleteFile(Long id) throws Exception { // 校验存在 FileDO file = validateFileExists(id); // 从文件存储器中删除 FileClient client = fileConfigService.getFileClient(file.getConfigId()); Assert.notNull(client, "客户端({}) 不能为空", file.getConfigId()); client.delete(file.getPath()); // 删除记录 fileMapper.deleteById(id); } private FileDO validateFileExists(Long id) { FileDO fileDO = fileMapper.selectById(id); if (fileDO == null) { throw exception(FILE_NOT_EXISTS); } return fileDO; } @Override public byte[] getFileContent(Long configId, String path) throws Exception { FileClient client = fileConfigService.getFileClient(configId); Assert.notNull(client, "客户端({}) 不能为空", configId); return client.getContent(path); } @Override public FilePresignedUrlRespVO getFilePresignedUrl(String path) throws Exception { FileClient fileClient = fileConfigService.getMasterFileClient(); FilePresignedUrlRespDTO presignedObjectUrl = fileClient.getPresignedObjectUrl(path); return BeanUtils.toBean(presignedObjectUrl, FilePresignedUrlRespVO.class, object -> object.setConfigId(fileClient.getId())); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/job/JobLogService.java ================================================ package co.yixiang.yshop.module.infra.service.job; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.quartz.core.service.JobLogFrameworkService; import co.yixiang.yshop.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.job.JobLogDO; /** * Job 日志 Service 接口 * * @author yshop */ public interface JobLogService extends JobLogFrameworkService { /** * 获得定时任务 * * @param id 编号 * @return 定时任务 */ JobLogDO getJobLog(Long id); /** * 获得定时任务分页 * * @param pageReqVO 分页查询 * @return 定时任务分页 */ PageResult getJobLogPage(JobLogPageReqVO pageReqVO); /** * 清理 exceedDay 天前的任务日志 * * @param exceedDay 超过多少天就进行清理 * @param deleteLimit 清理的间隔条数 */ Integer cleanJobLog(Integer exceedDay, Integer deleteLimit); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/job/JobLogServiceImpl.java ================================================ package co.yixiang.yshop.module.infra.service.job; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.job.JobLogDO; import co.yixiang.yshop.module.infra.dal.mysql.job.JobLogMapper; import co.yixiang.yshop.module.infra.enums.job.JobLogStatusEnum; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.time.LocalDateTime; /** * Job 日志 Service 实现类 * * @author yshop */ @Service @Validated @Slf4j public class JobLogServiceImpl implements JobLogService { @Resource private JobLogMapper jobLogMapper; @Override public Long createJobLog(Long jobId, LocalDateTime beginTime, String jobHandlerName, String jobHandlerParam, Integer executeIndex) { JobLogDO log = JobLogDO.builder().jobId(jobId).handlerName(jobHandlerName) .handlerParam(jobHandlerParam).executeIndex(executeIndex) .beginTime(beginTime).status(JobLogStatusEnum.RUNNING.getStatus()).build(); jobLogMapper.insert(log); return log.getId(); } @Override @Async public void updateJobLogResultAsync(Long logId, LocalDateTime endTime, Integer duration, boolean success, String result) { try { JobLogDO updateObj = JobLogDO.builder().id(logId).endTime(endTime).duration(duration) .status(success ? JobLogStatusEnum.SUCCESS.getStatus() : JobLogStatusEnum.FAILURE.getStatus()) .result(result).build(); jobLogMapper.updateById(updateObj); } catch (Exception ex) { log.error("[updateJobLogResultAsync][logId({}) endTime({}) duration({}) success({}) result({})]", logId, endTime, duration, success, result); } } @Override @SuppressWarnings("DuplicatedCode") public Integer cleanJobLog(Integer exceedDay, Integer deleteLimit) { int count = 0; LocalDateTime expireDate = LocalDateTime.now().minusDays(exceedDay); // 循环删除,直到没有满足条件的数据 for (int i = 0; i < Short.MAX_VALUE; i++) { int deleteCount = jobLogMapper.deleteByCreateTimeLt(expireDate, deleteLimit); count += deleteCount; // 达到删除预期条数,说明到底了 if (deleteCount < deleteLimit) { break; } } return count; } @Override public JobLogDO getJobLog(Long id) { return jobLogMapper.selectById(id); } @Override public PageResult getJobLogPage(JobLogPageReqVO pageReqVO) { return jobLogMapper.selectPage(pageReqVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/job/JobService.java ================================================ package co.yixiang.yshop.module.infra.service.job; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.infra.controller.admin.job.vo.job.JobPageReqVO; import co.yixiang.yshop.module.infra.controller.admin.job.vo.job.JobSaveReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.job.JobDO; import org.quartz.SchedulerException; import jakarta.validation.Valid; /** * 定时任务 Service 接口 * * @author yshop */ public interface JobService { /** * 创建定时任务 * * @param createReqVO 创建信息 * @return 编号 */ Long createJob(@Valid JobSaveReqVO createReqVO) throws SchedulerException; /** * 更新定时任务 * * @param updateReqVO 更新信息 */ void updateJob(@Valid JobSaveReqVO updateReqVO) throws SchedulerException; /** * 更新定时任务的状态 * * @param id 任务编号 * @param status 状态 */ void updateJobStatus(Long id, Integer status) throws SchedulerException; /** * 触发定时任务 * * @param id 任务编号 */ void triggerJob(Long id) throws SchedulerException; /** * 同步定时任务 * * 目的:自己存储的 Job 信息,强制同步到 Quartz 中 */ void syncJob() throws SchedulerException; /** * 删除定时任务 * * @param id 编号 */ void deleteJob(Long id) throws SchedulerException; /** * 获得定时任务 * * @param id 编号 * @return 定时任务 */ JobDO getJob(Long id); /** * 获得定时任务分页 * * @param pageReqVO 分页查询 * @return 定时任务分页 */ PageResult getJobPage(JobPageReqVO pageReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/job/JobServiceImpl.java ================================================ package co.yixiang.yshop.module.infra.service.job; import cn.hutool.extra.spring.SpringUtil; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.quartz.core.handler.JobHandler; import co.yixiang.yshop.framework.quartz.core.scheduler.SchedulerManager; import co.yixiang.yshop.framework.quartz.core.util.CronUtils; import co.yixiang.yshop.module.infra.controller.admin.job.vo.job.JobPageReqVO; import co.yixiang.yshop.module.infra.controller.admin.job.vo.job.JobSaveReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.job.JobDO; import co.yixiang.yshop.module.infra.dal.mysql.job.JobMapper; import co.yixiang.yshop.module.infra.enums.job.JobStatusEnum; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.quartz.SchedulerException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.util.List; import java.util.Objects; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.containsAny; import static co.yixiang.yshop.module.infra.enums.ErrorCodeConstants.*; /** * 定时任务 Service 实现类 * * @author yshop */ @Service @Validated @Slf4j public class JobServiceImpl implements JobService { @Resource private JobMapper jobMapper; @Resource private SchedulerManager schedulerManager; @Override @Transactional(rollbackFor = Exception.class) public Long createJob(JobSaveReqVO createReqVO) throws SchedulerException { validateCronExpression(createReqVO.getCronExpression()); // 1.1 校验唯一性 if (jobMapper.selectByHandlerName(createReqVO.getHandlerName()) != null) { throw exception(JOB_HANDLER_EXISTS); } // 1.2 校验 JobHandler 是否存在 validateJobHandlerExists(createReqVO.getHandlerName()); // 2. 插入 JobDO JobDO job = BeanUtils.toBean(createReqVO, JobDO.class); job.setStatus(JobStatusEnum.INIT.getStatus()); fillJobMonitorTimeoutEmpty(job); jobMapper.insert(job); // 3.1 添加 Job 到 Quartz 中 schedulerManager.addJob(job.getId(), job.getHandlerName(), job.getHandlerParam(), job.getCronExpression(), createReqVO.getRetryCount(), createReqVO.getRetryInterval()); // 3.2 更新 JobDO JobDO updateObj = JobDO.builder().id(job.getId()).status(JobStatusEnum.NORMAL.getStatus()).build(); jobMapper.updateById(updateObj); return job.getId(); } @Override @Transactional(rollbackFor = Exception.class) public void updateJob(JobSaveReqVO updateReqVO) throws SchedulerException { validateCronExpression(updateReqVO.getCronExpression()); // 1.1 校验存在 JobDO job = validateJobExists(updateReqVO.getId()); // 1.2 只有开启状态,才可以修改.原因是,如果出暂停状态,修改 Quartz Job 时,会导致任务又开始执行 if (!job.getStatus().equals(JobStatusEnum.NORMAL.getStatus())) { throw exception(JOB_UPDATE_ONLY_NORMAL_STATUS); } // 1.3 校验 JobHandler 是否存在 validateJobHandlerExists(updateReqVO.getHandlerName()); // 2. 更新 JobDO JobDO updateObj = BeanUtils.toBean(updateReqVO, JobDO.class); fillJobMonitorTimeoutEmpty(updateObj); jobMapper.updateById(updateObj); // 3. 更新 Job 到 Quartz 中 schedulerManager.updateJob(job.getHandlerName(), updateReqVO.getHandlerParam(), updateReqVO.getCronExpression(), updateReqVO.getRetryCount(), updateReqVO.getRetryInterval()); } private void validateJobHandlerExists(String handlerName) { Object handler = SpringUtil.getBean(handlerName); if (handler == null) { throw exception(JOB_HANDLER_BEAN_NOT_EXISTS); } if (!(handler instanceof JobHandler)) { throw exception(JOB_HANDLER_BEAN_TYPE_ERROR); } } @Override @Transactional(rollbackFor = Exception.class) public void updateJobStatus(Long id, Integer status) throws SchedulerException { // 校验 status if (!containsAny(status, JobStatusEnum.NORMAL.getStatus(), JobStatusEnum.STOP.getStatus())) { throw exception(JOB_CHANGE_STATUS_INVALID); } // 校验存在 JobDO job = validateJobExists(id); // 校验是否已经为当前状态 if (job.getStatus().equals(status)) { throw exception(JOB_CHANGE_STATUS_EQUALS); } // 更新 Job 状态 JobDO updateObj = JobDO.builder().id(id).status(status).build(); jobMapper.updateById(updateObj); // 更新状态 Job 到 Quartz 中 if (JobStatusEnum.NORMAL.getStatus().equals(status)) { // 开启 schedulerManager.resumeJob(job.getHandlerName()); } else { // 暂停 schedulerManager.pauseJob(job.getHandlerName()); } } @Override public void triggerJob(Long id) throws SchedulerException { // 校验存在 JobDO job = validateJobExists(id); // 触发 Quartz 中的 Job schedulerManager.triggerJob(job.getId(), job.getHandlerName(), job.getHandlerParam()); } @Override @Transactional(rollbackFor = Exception.class) public void syncJob() throws SchedulerException { // 1. 查询 Job 配置 List jobList = jobMapper.selectList(); // 2. 遍历处理 for (JobDO job : jobList) { // 2.1 先删除,再创建 schedulerManager.deleteJob(job.getHandlerName()); schedulerManager.addJob(job.getId(), job.getHandlerName(), job.getHandlerParam(), job.getCronExpression(), job.getRetryCount(), job.getRetryInterval()); // 2.2 如果 status 为暂停,则需要暂停 if (Objects.equals(job.getStatus(), JobStatusEnum.STOP.getStatus())) { schedulerManager.pauseJob(job.getHandlerName()); } log.info("[syncJob][id({}) handlerName({}) 同步完成]", job.getId(), job.getHandlerName()); } } @Override @Transactional(rollbackFor = Exception.class) public void deleteJob(Long id) throws SchedulerException { // 校验存在 JobDO job = validateJobExists(id); // 更新 jobMapper.deleteById(id); // 删除 Job 到 Quartz 中 schedulerManager.deleteJob(job.getHandlerName()); } private JobDO validateJobExists(Long id) { JobDO job = jobMapper.selectById(id); if (job == null) { throw exception(JOB_NOT_EXISTS); } return job; } private void validateCronExpression(String cronExpression) { if (!CronUtils.isValid(cronExpression)) { throw exception(JOB_CRON_EXPRESSION_VALID); } } @Override public JobDO getJob(Long id) { return jobMapper.selectById(id); } @Override public PageResult getJobPage(JobPageReqVO pageReqVO) { return jobMapper.selectPage(pageReqVO); } private static void fillJobMonitorTimeoutEmpty(JobDO job) { if (job.getMonitorTimeout() == null) { job.setMonitorTimeout(0); } } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/logger/ApiAccessLogService.java ================================================ package co.yixiang.yshop.module.infra.service.logger; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; import co.yixiang.yshop.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.logger.ApiAccessLogDO; /** * API 访问日志 Service 接口 * * @author yshop */ public interface ApiAccessLogService { /** * 创建 API 访问日志 * * @param createReqDTO API 访问日志 */ void createApiAccessLog(ApiAccessLogCreateReqDTO createReqDTO); /** * 获得 API 访问日志分页 * * @param pageReqVO 分页查询 * @return API 访问日志分页 */ PageResult getApiAccessLogPage(ApiAccessLogPageReqVO pageReqVO); /** * 清理 exceedDay 天前的访问日志 * * @param exceedDay 超过多少天就进行清理 * @param deleteLimit 清理的间隔条数 */ Integer cleanAccessLog(Integer exceedDay, Integer deleteLimit); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/logger/ApiAccessLogServiceImpl.java ================================================ package co.yixiang.yshop.module.infra.service.logger; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; import co.yixiang.yshop.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.logger.ApiAccessLogDO; import co.yixiang.yshop.module.infra.dal.mysql.logger.ApiAccessLogMapper; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.time.LocalDateTime; import static co.yixiang.yshop.module.infra.dal.dataobject.logger.ApiAccessLogDO.REQUEST_PARAMS_MAX_LENGTH; import static co.yixiang.yshop.module.infra.dal.dataobject.logger.ApiAccessLogDO.RESULT_MSG_MAX_LENGTH; /** * API 访问日志 Service 实现类 * * @author yshop */ @Slf4j @Service @Validated public class ApiAccessLogServiceImpl implements ApiAccessLogService { @Resource private ApiAccessLogMapper apiAccessLogMapper; @Override public void createApiAccessLog(ApiAccessLogCreateReqDTO createDTO) { ApiAccessLogDO apiAccessLog = BeanUtils.toBean(createDTO, ApiAccessLogDO.class); apiAccessLog.setRequestParams(StrUtil.maxLength(apiAccessLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH)); apiAccessLog.setResultMsg(StrUtil.maxLength(apiAccessLog.getResultMsg(), RESULT_MSG_MAX_LENGTH)); apiAccessLogMapper.insert(apiAccessLog); } @Override public PageResult getApiAccessLogPage(ApiAccessLogPageReqVO pageReqVO) { return apiAccessLogMapper.selectPage(pageReqVO); } @Override @SuppressWarnings("DuplicatedCode") public Integer cleanAccessLog(Integer exceedDay, Integer deleteLimit) { int count = 0; LocalDateTime expireDate = LocalDateTime.now().minusDays(exceedDay); // 循环删除,直到没有满足条件的数据 for (int i = 0; i < Short.MAX_VALUE; i++) { int deleteCount = apiAccessLogMapper.deleteByCreateTimeLt(expireDate, deleteLimit); count += deleteCount; // 达到删除预期条数,说明到底了 if (deleteCount < deleteLimit) { break; } } return count; } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/logger/ApiErrorLogService.java ================================================ package co.yixiang.yshop.module.infra.service.logger; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; import co.yixiang.yshop.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.logger.ApiErrorLogDO; /** * API 错误日志 Service 接口 * * @author yshop */ public interface ApiErrorLogService { /** * 创建 API 错误日志 * * @param createReqDTO API 错误日志 */ void createApiErrorLog(ApiErrorLogCreateReqDTO createReqDTO); /** * 获得 API 错误日志分页 * * @param pageReqVO 分页查询 * @return API 错误日志分页 */ PageResult getApiErrorLogPage(ApiErrorLogPageReqVO pageReqVO); /** * 更新 API 错误日志已处理 * * @param id API 日志编号 * @param processStatus 处理结果 * @param processUserId 处理人 */ void updateApiErrorLogProcess(Long id, Integer processStatus, Long processUserId); /** * 清理 exceedDay 天前的错误日志 * * @param exceedDay 超过多少天就进行清理 * @param deleteLimit 清理的间隔条数 */ Integer cleanErrorLog(Integer exceedDay, Integer deleteLimit); } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/service/logger/ApiErrorLogServiceImpl.java ================================================ package co.yixiang.yshop.module.infra.service.logger; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; import co.yixiang.yshop.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; import co.yixiang.yshop.module.infra.dal.dataobject.logger.ApiErrorLogDO; import co.yixiang.yshop.module.infra.dal.mysql.logger.ApiErrorLogMapper; import co.yixiang.yshop.module.infra.enums.logger.ApiErrorLogProcessStatusEnum; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.infra.dal.dataobject.logger.ApiErrorLogDO.REQUEST_PARAMS_MAX_LENGTH; import static co.yixiang.yshop.module.infra.enums.ErrorCodeConstants.API_ERROR_LOG_NOT_FOUND; import static co.yixiang.yshop.module.infra.enums.ErrorCodeConstants.API_ERROR_LOG_PROCESSED; /** * API 错误日志 Service 实现类 * * @author yshop */ @Slf4j @Service @Validated public class ApiErrorLogServiceImpl implements ApiErrorLogService { @Resource private ApiErrorLogMapper apiErrorLogMapper; @Override public void createApiErrorLog(ApiErrorLogCreateReqDTO createDTO) { ApiErrorLogDO apiErrorLog = BeanUtils.toBean(createDTO, ApiErrorLogDO.class) .setProcessStatus(ApiErrorLogProcessStatusEnum.INIT.getStatus()); apiErrorLog.setRequestParams(StrUtil.maxLength(apiErrorLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH)); apiErrorLogMapper.insert(apiErrorLog); } @Override public PageResult getApiErrorLogPage(ApiErrorLogPageReqVO pageReqVO) { return apiErrorLogMapper.selectPage(pageReqVO); } @Override public void updateApiErrorLogProcess(Long id, Integer processStatus, Long processUserId) { ApiErrorLogDO errorLog = apiErrorLogMapper.selectById(id); if (errorLog == null) { throw exception(API_ERROR_LOG_NOT_FOUND); } if (!ApiErrorLogProcessStatusEnum.INIT.getStatus().equals(errorLog.getProcessStatus())) { throw exception(API_ERROR_LOG_PROCESSED); } // 标记处理 apiErrorLogMapper.updateById(ApiErrorLogDO.builder().id(id).processStatus(processStatus) .processUserId(processUserId).processTime(LocalDateTime.now()).build()); } @Override @SuppressWarnings("DuplicatedCode") public Integer cleanErrorLog(Integer exceedDay, Integer deleteLimit) { int count = 0; LocalDateTime expireDate = LocalDateTime.now().minusDays(exceedDay); // 循环删除,直到没有满足条件的数据 for (int i = 0; i < Short.MAX_VALUE; i++) { int deleteCount = apiErrorLogMapper.deleteByCreateTimeLt(expireDate, deleteLimit); count += deleteCount; // 达到删除预期条数,说明到底了 if (deleteCount < deleteLimit) { break; } } return count; } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/websocket/DemoWebSocketMessageListener.java ================================================ package co.yixiang.yshop.module.infra.websocket; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.websocket.core.listener.WebSocketMessageListener; import co.yixiang.yshop.framework.websocket.core.sender.WebSocketMessageSender; import co.yixiang.yshop.framework.websocket.core.util.WebSocketFrameworkUtils; import co.yixiang.yshop.module.infra.websocket.message.DemoReceiveMessage; import co.yixiang.yshop.module.infra.websocket.message.DemoSendMessage; import org.springframework.stereotype.Component; import org.springframework.web.socket.WebSocketSession; import jakarta.annotation.Resource; /** * WebSocket 示例:单发消息 * * @author yshop */ @Component public class DemoWebSocketMessageListener implements WebSocketMessageListener { @Resource private WebSocketMessageSender webSocketMessageSender; @Override public void onMessage(WebSocketSession session, DemoSendMessage message) { Long fromUserId = WebSocketFrameworkUtils.getLoginUserId(session); // 情况一:单发 if (message.getToUserId() != null) { DemoReceiveMessage toMessage = new DemoReceiveMessage().setFromUserId(fromUserId) .setText(message.getText()).setSingle(true); webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getToUserId(), // 给指定用户 "demo-message-receive", toMessage); return; } // 情况二:群发 DemoReceiveMessage toMessage = new DemoReceiveMessage().setFromUserId(fromUserId) .setText(message.getText()).setSingle(false); webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), // 给所有用户 "demo-message-receive", toMessage); } @Override public String getType() { return "demo-message-send"; } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/websocket/message/DemoReceiveMessage.java ================================================ package co.yixiang.yshop.module.infra.websocket.message; import lombok.Data; /** * 示例:server -> client 同步消息 * * @author yshop */ @Data public class DemoReceiveMessage { /** * 接收人的编号 */ private Long fromUserId; /** * 内容 */ private String text; /** * 是否单聊 */ private Boolean single; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/java/co/yixiang/yshop/module/infra/websocket/message/DemoSendMessage.java ================================================ package co.yixiang.yshop.module.infra.websocket.message; import lombok.Data; /** * 示例:client -> server 发送消息 * * @author yshop */ @Data public class DemoSendMessage { /** * 发送给谁 * * 如果为空,说明发送给所有人 */ private Long toUserId; /** * 内容 */ private String text; } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/java/controller/controller.vm ================================================ package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}; import org.springframework.web.bind.annotation.*; import ${jakartaPackage}.annotation.Resource; import org.springframework.validation.annotation.Validated; #if ($sceneEnum.scene == 1)import org.springframework.security.access.prepost.PreAuthorize;#end import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Operation; import ${jakartaPackage}.validation.constraints.*; import ${jakartaPackage}.validation.*; import ${jakartaPackage}.servlet.http.*; import java.util.*; import java.io.IOException; import ${PageParamClassName}; import ${PageResultClassName}; import ${CommonResultClassName}; import ${BeanUtils}; import static ${CommonResultClassName}.success; import ${ExcelUtilsClassName}; import ${ApiAccessLogClassName}; import static ${OperateTypeEnumClassName}.*; import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; ## 特殊:主子表专属逻辑 #foreach ($subTable in $subTables) import ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}.${subTable.className}DO; #end import ${basePackage}.module.${table.moduleName}.service.${table.businessName}.${table.className}Service; @Tag(name = "${sceneEnum.name} - ${table.classComment}") @RestController ##二级的 businessName 暂时不算在 HTTP 路径上,可以根据需要写 @RequestMapping("/${table.moduleName}/${simpleClassName_strikeCase}") @Validated public class ${sceneEnum.prefixClass}${table.className}Controller { @Resource private ${table.className}Service ${classNameVar}Service; @PostMapping("/create") @Operation(summary = "创建${table.classComment}") #if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:create')") #end public CommonResult<${primaryColumn.javaType}> create${simpleClassName}(@Valid @RequestBody ${sceneEnum.prefixClass}${table.className}SaveReqVO createReqVO) { return success(${classNameVar}Service.create${simpleClassName}(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新${table.classComment}") #if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:update')") #end public CommonResult update${simpleClassName}(@Valid @RequestBody ${sceneEnum.prefixClass}${table.className}SaveReqVO updateReqVO) { ${classNameVar}Service.update${simpleClassName}(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除${table.classComment}") @Parameter(name = "id", description = "编号", required = true) #if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:delete')") #end public CommonResult delete${simpleClassName}(@RequestParam("id") ${primaryColumn.javaType} id) { ${classNameVar}Service.delete${simpleClassName}(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得${table.classComment}") @Parameter(name = "id", description = "编号", required = true, example = "1024") #if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") #end public CommonResult<${sceneEnum.prefixClass}${table.className}RespVO> get${simpleClassName}(@RequestParam("id") ${primaryColumn.javaType} id) { ${table.className}DO ${classNameVar} = ${classNameVar}Service.get${simpleClassName}(id); return success(BeanUtils.toBean(${classNameVar}, ${sceneEnum.prefixClass}${table.className}RespVO.class)); } #if ( $table.templateType != 2 ) @GetMapping("/page") @Operation(summary = "获得${table.classComment}分页") #if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") #end public CommonResult> get${simpleClassName}Page(@Valid ${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO) { PageResult<${table.className}DO> pageResult = ${classNameVar}Service.get${simpleClassName}Page(pageReqVO); return success(BeanUtils.toBean(pageResult, ${sceneEnum.prefixClass}${table.className}RespVO.class)); } ## 特殊:树表专属逻辑(树不需要分页接口) #else @GetMapping("/list") @Operation(summary = "获得${table.classComment}列表") #if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") #end public CommonResult> get${simpleClassName}List(@Valid ${sceneEnum.prefixClass}${table.className}ListReqVO listReqVO) { List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}List(listReqVO); return success(BeanUtils.toBean(list, ${sceneEnum.prefixClass}${table.className}RespVO.class)); } #end @GetMapping("/export-excel") @Operation(summary = "导出${table.classComment} Excel") #if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:export')") #end @ApiAccessLog(operateType = EXPORT) #if ( $table.templateType != 2 ) public void export${simpleClassName}Excel(@Valid ${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO, HttpServletResponse response) throws IOException { pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}Page(pageReqVO).getList(); // 导出 Excel ExcelUtils.write(response, "${table.classComment}.xls", "数据", ${table.className}RespVO.class, BeanUtils.toBean(list, ${table.className}RespVO.class)); } ## 特殊:树表专属逻辑(树不需要分页接口) #else public void export${simpleClassName}Excel(@Valid ${sceneEnum.prefixClass}${table.className}ListReqVO listReqVO, HttpServletResponse response) throws IOException { List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}List(listReqVO); // 导出 Excel ExcelUtils.write(response, "${table.classComment}.xls", "数据", ${table.className}RespVO.class, BeanUtils.toBean(list, ${table.className}RespVO.class)); } #end ## 特殊:主子表专属逻辑 #foreach ($subTable in $subTables) #set ($index = $foreach.count - 1) #set ($subSimpleClassName = $subSimpleClassNames.get($index)) #set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段 #set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 #set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index)) #set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index)) #set ($subClassNameVar = $subClassNameVars.get($index)) // ==================== 子表($subTable.classComment) ==================== ## 情况一:MASTER_ERP 时,需要分查询页子表 #if ( $table.templateType == 11 ) @GetMapping("/${subSimpleClassName_strikeCase}/page") @Operation(summary = "获得${subTable.classComment}分页") @Parameter(name = "${subJoinColumn.javaField}", description = "${subJoinColumn.columnComment}") #if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") #end public CommonResult> get${subSimpleClassName}Page(PageParam pageReqVO, @RequestParam("${subJoinColumn.javaField}") ${subJoinColumn.javaType} ${subJoinColumn.javaField}) { return success(${classNameVar}Service.get${subSimpleClassName}Page(pageReqVO, ${subJoinColumn.javaField})); } ## 情况二:非 MASTER_ERP 时,需要列表查询子表 #else #if ( $subTable.subJoinMany ) @GetMapping("/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}") @Operation(summary = "获得${subTable.classComment}列表") @Parameter(name = "${subJoinColumn.javaField}", description = "${subJoinColumn.columnComment}") #if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") #end public CommonResult> get${subSimpleClassName}ListBy${SubJoinColumnName}(@RequestParam("${subJoinColumn.javaField}") ${subJoinColumn.javaType} ${subJoinColumn.javaField}) { return success(${classNameVar}Service.get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField})); } #else @GetMapping("/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}") @Operation(summary = "获得${subTable.classComment}") @Parameter(name = "${subJoinColumn.javaField}", description = "${subJoinColumn.columnComment}") #if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") #end public CommonResult<${subTable.className}DO> get${subSimpleClassName}By${SubJoinColumnName}(@RequestParam("${subJoinColumn.javaField}") ${subJoinColumn.javaType} ${subJoinColumn.javaField}) { return success(${classNameVar}Service.get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField})); } #end #end ## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作 #if ( $table.templateType == 11 ) @PostMapping("/${subSimpleClassName_strikeCase}/create") @Operation(summary = "创建${subTable.classComment}") #if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:create')") #end public CommonResult<${subPrimaryColumn.javaType}> create${subSimpleClassName}(@Valid @RequestBody ${subTable.className}DO ${subClassNameVar}) { return success(${classNameVar}Service.create${subSimpleClassName}(${subClassNameVar})); } @PutMapping("/${subSimpleClassName_strikeCase}/update") @Operation(summary = "更新${subTable.classComment}") #if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:update')") #end public CommonResult update${subSimpleClassName}(@Valid @RequestBody ${subTable.className}DO ${subClassNameVar}) { ${classNameVar}Service.update${subSimpleClassName}(${subClassNameVar}); return success(true); } @DeleteMapping("/${subSimpleClassName_strikeCase}/delete") @Parameter(name = "id", description = "编号", required = true) @Operation(summary = "删除${subTable.classComment}") #if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:delete')") #end public CommonResult delete${subSimpleClassName}(@RequestParam("id") ${subPrimaryColumn.javaType} id) { ${classNameVar}Service.delete${subSimpleClassName}(id); return success(true); } @GetMapping("/${subSimpleClassName_strikeCase}/get") @Operation(summary = "获得${subTable.classComment}") @Parameter(name = "id", description = "编号", required = true) #if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") #end public CommonResult<${subTable.className}DO> get${subSimpleClassName}(@RequestParam("id") ${subPrimaryColumn.javaType} id) { return success(${classNameVar}Service.get${subSimpleClassName}(id)); } #end #end } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/java/controller/vo/listReqVO.vm ================================================ package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import ${PageParamClassName}; #foreach ($column in $columns) #if (${column.javaType} == "BigDecimal") import java.math.BigDecimal; #break #end #end ## 处理 LocalDateTime 字段的引入 #foreach ($column in $columns) #if (${column.listOperation} && ${column.javaType} == "LocalDateTime") import java.time.LocalDateTime; import org.springframework.format.annotation.DateTimeFormat; import static ${DateUtilsClassName}.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; #break #end #end ## 字段模板 #macro(columnTpl $prefix $prefixStr) @Schema(description = "${prefixStr}${column.columnComment}"#if ("$!column.example" != ""), example = "${column.example}"#end) private ${column.javaType}#if ("$!prefix" != "") ${prefix}${JavaField}#else ${column.javaField}#end; #end @Schema(description = "${sceneEnum.name} - ${table.classComment}列表 Request VO") @Data public class ${sceneEnum.prefixClass}${table.className}ListReqVO { #foreach ($column in $columns) #if (${column.listOperation})##查询操作 #if (${column.listOperationCondition} == "BETWEEN")## 情况一,Between 的时候 @Schema(description = "${column.columnComment}"#if ("$!column.example" != ""), example = "${column.example}"#end) @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private ${column.javaType}[] ${column.javaField}; #else##情况二,非 Between 的时间 #columnTpl('', '') #end #end #end } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/java/controller/vo/pageReqVO.vm ================================================ package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import ${PageParamClassName}; #foreach ($column in $columns) #if (${column.javaType} == "BigDecimal") import java.math.BigDecimal; #break #end #end ## 处理 LocalDateTime 字段的引入 #foreach ($column in $columns) #if (${column.listOperationCondition} && ${column.javaType} == "LocalDateTime") import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static ${DateUtilsClassName}.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; #break #end #end ## 字段模板 #macro(columnTpl $prefix $prefixStr) @Schema(description = "${prefixStr}${column.columnComment}"#if ("$!column.example" != ""), example = "${column.example}"#end) private ${column.javaType}#if ("$!prefix" != "") ${prefix}${JavaField}#else ${column.javaField}#end; #end @Schema(description = "${sceneEnum.name} - ${table.classComment}分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ${sceneEnum.prefixClass}${table.className}PageReqVO extends PageParam { #foreach ($column in $columns) #if (${column.listOperation})##查询操作 #if (${column.listOperationCondition} == "BETWEEN")## 情况一,Between 的时候 @Schema(description = "${column.columnComment}"#if ("$!column.example" != ""), example = "${column.example}"#end) @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private ${column.javaType}[] ${column.javaField}; #else##情况二,非 Between 的时间 #columnTpl('', '') #end #end #end } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/java/controller/vo/respVO.vm ================================================ package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; ## 处理 BigDecimal 字段的引入 import java.util.*; #foreach ($column in $columns) #if (${column.javaType} == "BigDecimal") import java.math.BigDecimal; #break #end #end ## 处理 LocalDateTime 字段的引入 #foreach ($column in $columns) #if (${column.listOperationResult} && ${column.javaType} == "LocalDateTime") import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; #break #end #end ## 处理 Excel 导出 import com.alibaba.excel.annotation.*; #foreach ($column in $columns) #if ("$!column.dictType" != "")## 有设置数据字典 import ${DictFormatClassName}; import ${DictConvertClassName}; #break #end #end @Schema(description = "${sceneEnum.name} - ${table.classComment} Response VO") @Data @ExcelIgnoreUnannotated public class ${sceneEnum.prefixClass}${table.className}RespVO { ## 逐个处理字段 #foreach ($column in $columns) #if (${column.listOperationResult}) ## 1. 处理 Swagger 注解 @Schema(description = "${column.columnComment}"#if (!${column.nullable}), requiredMode = Schema.RequiredMode.REQUIRED#end#if ("$!column.example" != ""), example = "${column.example}"#end) ## 2. 处理 Excel 导出 #if ("$!column.dictType" != "")##处理枚举值 @ExcelProperty(value = "${column.columnComment}", converter = DictConvert.class) @DictFormat("${column.dictType}") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 #else @ExcelProperty("${column.columnComment}") #end ## 3. 处理字段定义 private ${column.javaType} ${column.javaField}; #end #end } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/java/controller/vo/saveReqVO.vm ================================================ package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import ${jakartaPackage}.validation.constraints.*; ## 处理 BigDecimal 字段的引入 #foreach ($column in $columns) #if (${column.javaType} == "BigDecimal") import java.math.BigDecimal; #break #end #end ## 处理 LocalDateTime 字段的引入 #foreach ($column in $columns) #if ((${column.createOperation} || ${column.updateOperation}) && ${column.javaType} == "LocalDateTime") import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; #break #end #end ## 特殊:主子表专属逻辑 #foreach ($subTable in $subTables) import ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}.${subTable.className}DO; #end @Schema(description = "${sceneEnum.name} - ${table.classComment}新增/修改 Request VO") @Data public class ${sceneEnum.prefixClass}${table.className}SaveReqVO { ## 逐个处理字段 #foreach ($column in $columns) #if (${column.createOperation} || ${column.updateOperation}) ## 1. 处理 Swagger 注解 @Schema(description = "${column.columnComment}"#if (!${column.nullable}), requiredMode = Schema.RequiredMode.REQUIRED#end#if ("$!column.example" != ""), example = "${column.example}"#end) ## 2. 处理 Validator 参数校验 #if (!${column.nullable} && !${column.primaryKey}) #if (${column.javaType} == 'String') @NotEmpty(message = "${column.columnComment}不能为空") #else @NotNull(message = "${column.columnComment}不能为空") #end #end ## 3. 处理字段定义 private ${column.javaType} ${column.javaField}; #end #end ## 特殊:主子表专属逻辑(非 ERP 模式) #if ( $subTables && $subTables.size() > 0 && $table.templateType != 11 ) #foreach ($subTable in $subTables) #set ($index = $foreach.count - 1) #if ( $subTable.subJoinMany) @Schema(description = "${subTable.classComment}列表") private List<${subTable.className}DO> ${subClassNameVars.get($index)}s; #else @Schema(description = "${subTable.classComment}") private ${subTable.className}DO ${subClassNameVars.get($index)}; #end #end #end } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/java/dal/do.vm ================================================ package ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}; import lombok.*; import java.util.*; #foreach ($column in $columns) #if (${column.javaType} == "BigDecimal") import java.math.BigDecimal; #end #if (${column.javaType} == "LocalDateTime") import java.time.LocalDateTime; #end #end import com.baomidou.mybatisplus.annotation.*; import ${BaseDOClassName}; /** * ${table.classComment} DO * * @author ${table.author} */ @TableName("${table.tableName.toLowerCase()}") @KeySequence("${table.tableName.toLowerCase()}_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class ${table.className}DO extends BaseDO { ## 特殊:树表专属逻辑 #if ( $table.templateType == 2 ) public static final Long ${treeParentColumn_javaField_underlineCase.toUpperCase()}_ROOT = 0L; #end #foreach ($column in $columns) #if (!${baseDOFields.contains(${column.javaField})})##排除 BaseDO 的字段 /** * ${column.columnComment} #if ("$!column.dictType" != "")##处理枚举值 * * 枚举 {@link TODO ${column.dictType} 对应的类} #end */ #if (${column.primaryKey})##处理主键 @TableId#if (${column.javaType} == 'String')(type = IdType.INPUT)#end #end private ${column.javaType} ${column.javaField}; #end #end } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/java/dal/do_sub.vm ================================================ #set ($subTable = $subTables.get($subIndex))##当前表 #set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 package ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}; import lombok.*; import java.util.*; #foreach ($column in $subColumns) #if (${column.javaType} == "BigDecimal") import java.math.BigDecimal; #end #if (${column.javaType} == "LocalDateTime") import java.time.LocalDateTime; #end #end import com.baomidou.mybatisplus.annotation.*; import ${BaseDOClassName}; /** * ${subTable.classComment} DO * * @author ${subTable.author} */ @TableName("${subTable.tableName.toLowerCase()}") @KeySequence("${subTable.tableName.toLowerCase()}_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class ${subTable.className}DO extends BaseDO { #foreach ($column in $subColumns) #if (!${baseDOFields.contains(${column.javaField})})##排除 BaseDO 的字段 /** * ${column.columnComment} #if ("$!column.dictType" != "")##处理枚举值 * * 枚举 {@link TODO ${column.dictType} 对应的类} #end */ #if (${column.primaryKey})##处理主键 @TableId#if (${column.javaType} == 'String')(type = IdType.INPUT)#end #end private ${column.javaType} ${column.javaField}; #end #end } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/java/dal/mapper.vm ================================================ package ${basePackage}.module.${table.moduleName}.dal.mysql.${table.businessName}; import java.util.*; import ${PageResultClassName}; import ${QueryWrapperClassName}; import ${BaseMapperClassName}; import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; import org.apache.ibatis.annotations.Mapper; import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; ## 字段模板 #macro(listCondition) #foreach ($column in $columns) #if (${column.listOperation}) #set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 #if (${column.listOperationCondition} == "=")##情况一,= 的时候 .eqIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) #end #if (${column.listOperationCondition} == "!=")##情况二,!= 的时候 .neIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) #end #if (${column.listOperationCondition} == ">")##情况三,> 的时候 .gtIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) #end #if (${column.listOperationCondition} == ">=")##情况四,>= 的时候 .geIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) #end #if (${column.listOperationCondition} == "<")##情况五,< 的时候 .ltIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) #end #if (${column.listOperationCondition} == "<=")##情况五,<= 的时候 .leIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) #end #if (${column.listOperationCondition} == "LIKE")##情况七,Like 的时候 .likeIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) #end #if (${column.listOperationCondition} == "BETWEEN")##情况八,Between 的时候 .betweenIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) #end #end #end #end /** * ${table.classComment} Mapper * * @author ${table.author} */ @Mapper public interface ${table.className}Mapper extends BaseMapperX<${table.className}DO> { ## 特殊:树表专属逻辑(树不需要分页接口) #if ( $table.templateType != 2 ) default PageResult<${table.className}DO> selectPage(${sceneEnum.prefixClass}${table.className}PageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX<${table.className}DO>() #listCondition() .orderByDesc(${table.className}DO::getId));## 大多数情况下,id 倒序 } #else default List<${table.className}DO> selectList(${sceneEnum.prefixClass}${table.className}ListReqVO reqVO) { return selectList(new LambdaQueryWrapperX<${table.className}DO>() #listCondition() .orderByDesc(${table.className}DO::getId));## 大多数情况下,id 倒序 } #end ## 特殊:树表专属逻辑 #if ( $table.templateType == 2 ) #set ($TreeParentJavaField = $treeParentColumn.javaField.substring(0,1).toUpperCase() + ${treeParentColumn.javaField.substring(1)})##首字母大写 #set ($TreeNameJavaField = $treeNameColumn.javaField.substring(0,1).toUpperCase() + ${treeNameColumn.javaField.substring(1)})##首字母大写 default ${table.className}DO selectBy${TreeParentJavaField}And${TreeNameJavaField}(Long ${treeParentColumn.javaField}, String ${treeNameColumn.javaField}) { return selectOne(${table.className}DO::get${TreeParentJavaField}, ${treeParentColumn.javaField}, ${table.className}DO::get${TreeNameJavaField}, ${treeNameColumn.javaField}); } default Long selectCountBy${TreeParentJavaField}(${treeParentColumn.javaType} ${treeParentColumn.javaField}) { return selectCount(${table.className}DO::get${TreeParentJavaField}, ${treeParentColumn.javaField}); } #end } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/java/dal/mapper.xml.vm ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/java/dal/mapper_sub.vm ================================================ #set ($subTable = $subTables.get($subIndex))##当前表 #set ($subColumns = $subJoinColumnsList.get($subIndex))##当前字段数组 #set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 package ${basePackage}.module.${subTable.moduleName}.dal.mysql.${subTable.businessName}; import java.util.*; import ${PageResultClassName}; import ${PageParamClassName}; import ${QueryWrapperClassName}; import ${BaseMapperClassName}; import ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}.${subTable.className}DO; import org.apache.ibatis.annotations.Mapper; /** * ${subTable.classComment} Mapper * * @author ${subTable.author} */ @Mapper public interface ${subTable.className}Mapper extends BaseMapperX<${subTable.className}DO> { ## 情况一:MASTER_ERP 时,需要分查询页子表 #if ( $table.templateType == 11 ) default PageResult<${subTable.className}DO> selectPage(PageParam reqVO, ${subJoinColumn.javaType} ${subJoinColumn.javaField}) { return selectPage(reqVO, new LambdaQueryWrapperX<${subTable.className}DO>() .eq(${subTable.className}DO::get${SubJoinColumnName}, ${subJoinColumn.javaField}) .orderByDesc(${subTable.className}DO::getId));## 大多数情况下,id 倒序 } ## 情况二:非 MASTER_ERP 时,需要列表查询子表 #else #if ( $subTable.subJoinMany) default List<${subTable.className}DO> selectListBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) { return selectList(${subTable.className}DO::get${SubJoinColumnName}, ${subJoinColumn.javaField}); } #else default ${subTable.className}DO selectBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) { return selectOne(${subTable.className}DO::get${SubJoinColumnName}, ${subJoinColumn.javaField}); } #end #end default int deleteBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) { return delete(${subTable.className}DO::get${SubJoinColumnName}, ${subJoinColumn.javaField}); } } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/java/enums/errorcode.vm ================================================ // TODO 待办:请将下面的错误码复制到 yshop-module-${table.moduleName}-api 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!! // ========== ${table.classComment} TODO 补充编号 ========== ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS = new ErrorCode(TODO 补充编号, "${table.classComment}不存在"); ## 特殊:树表专属逻辑 #if ( $table.templateType == 2 ) ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_EXITS_CHILDREN = new ErrorCode(TODO 补充编号, "存在存在子${table.classComment},无法删除"); ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_PARENT_NOT_EXITS = new ErrorCode(TODO 补充编号,"父级${table.classComment}不存在"); ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_PARENT_ERROR = new ErrorCode(TODO 补充编号, "不能设置自己为父${table.classComment}"); ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_${treeNameColumn_javaField_underlineCase.toUpperCase()}_DUPLICATE = new ErrorCode(TODO 补充编号, "已经存在该${treeNameColumn.columnComment}的${table.classComment}"); ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_PARENT_IS_CHILD = new ErrorCode(TODO 补充编号, "不能设置自己的子${table.className}为父${table.className}"); #end ## 特殊:主子表专属逻辑 #if ( $table.templateType == 11 )## 特殊:ERP 情况 #foreach ($subTable in $subTables) #set ($index = $foreach.count - 1) #set ($simpleClassNameUnderlineCase = $simpleClassNameUnderlineCases.get($index)) ErrorCode ${simpleClassNameUnderlineCase.toUpperCase()}_NOT_EXISTS = new ErrorCode(TODO 补充编号, "${subTable.classComment}不存在"); #if ( !$subTable.subJoinMany ) ErrorCode ${simpleClassNameUnderlineCase.toUpperCase()}_EXISTS = new ErrorCode(TODO 补充编号, "${subTable.classComment}已存在"); #end #end #end ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/java/service/service.vm ================================================ package ${basePackage}.module.${table.moduleName}.service.${table.businessName}; import java.util.*; import ${jakartaPackage}.validation.*; import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; ## 特殊:主子表专属逻辑 #foreach ($subTable in $subTables) import ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}.${subTable.className}DO; #end import ${PageResultClassName}; import ${PageParamClassName}; /** * ${table.classComment} Service 接口 * * @author ${table.author} */ public interface ${table.className}Service { /** * 创建${table.classComment} * * @param createReqVO 创建信息 * @return 编号 */ ${primaryColumn.javaType} create${simpleClassName}(@Valid ${sceneEnum.prefixClass}${table.className}SaveReqVO createReqVO); /** * 更新${table.classComment} * * @param updateReqVO 更新信息 */ void update${simpleClassName}(@Valid ${sceneEnum.prefixClass}${table.className}SaveReqVO updateReqVO); /** * 删除${table.classComment} * * @param id 编号 */ void delete${simpleClassName}(${primaryColumn.javaType} id); /** * 获得${table.classComment} * * @param id 编号 * @return ${table.classComment} */ ${table.className}DO get${simpleClassName}(${primaryColumn.javaType} id); ## 特殊:树表专属逻辑(树不需要分页接口) #if ( $table.templateType != 2 ) /** * 获得${table.classComment}分页 * * @param pageReqVO 分页查询 * @return ${table.classComment}分页 */ PageResult<${table.className}DO> get${simpleClassName}Page(${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO); #else /** * 获得${table.classComment}列表 * * @param listReqVO 查询条件 * @return ${table.classComment}列表 */ List<${table.className}DO> get${simpleClassName}List(${sceneEnum.prefixClass}${table.className}ListReqVO listReqVO); #end ## 特殊:主子表专属逻辑 #foreach ($subTable in $subTables) #set ($index = $foreach.count - 1) #set ($subSimpleClassName = $subSimpleClassNames.get($index)) #set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段 #set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 #set ($subClassNameVar = $subClassNameVars.get($index)) // ==================== 子表($subTable.classComment) ==================== ## 情况一:MASTER_ERP 时,需要分查询页子表 #if ( $table.templateType == 11 ) /** * 获得${subTable.classComment}分页 * * @param pageReqVO 分页查询 * @param ${subJoinColumn.javaField} ${subJoinColumn.columnComment} * @return ${subTable.classComment}分页 */ PageResult<${subTable.className}DO> get${subSimpleClassName}Page(PageParam pageReqVO, ${subJoinColumn.javaType} ${subJoinColumn.javaField}); ## 情况二:非 MASTER_ERP 时,需要列表查询子表 #else #if ( $subTable.subJoinMany ) /** * 获得${subTable.classComment}列表 * * @param ${subJoinColumn.javaField} ${subJoinColumn.columnComment} * @return ${subTable.classComment}列表 */ List<${subTable.className}DO> get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}); #else /** * 获得${subTable.classComment} * * @param ${subJoinColumn.javaField} ${subJoinColumn.columnComment} * @return ${subTable.classComment} */ ${subTable.className}DO get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}); #end #end ## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作 #if ( $table.templateType == 11 ) /** * 创建${subTable.classComment} * * @param ${subClassNameVar} 创建信息 * @return 编号 */ ${subPrimaryColumn.javaType} create${subSimpleClassName}(@Valid ${subTable.className}DO ${subClassNameVar}); /** * 更新${subTable.classComment} * * @param ${subClassNameVar} 更新信息 */ void update${subSimpleClassName}(@Valid ${subTable.className}DO ${subClassNameVar}); /** * 删除${subTable.classComment} * * @param id 编号 */ void delete${subSimpleClassName}(${subPrimaryColumn.javaType} id); /** * 获得${subTable.classComment} * * @param id 编号 * @return ${subTable.classComment} */ ${subTable.className}DO get${subSimpleClassName}(${subPrimaryColumn.javaType} id); #end #end } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm ================================================ package ${basePackage}.module.${table.moduleName}.service.${table.businessName}; import org.springframework.stereotype.Service; import ${jakartaPackage}.annotation.Resource; import org.springframework.validation.annotation.Validated; import org.springframework.transaction.annotation.Transactional; import java.util.*; import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; ## 特殊:主子表专属逻辑 #foreach ($subTable in $subTables) import ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}.${subTable.className}DO; #end import ${PageResultClassName}; import ${PageParamClassName}; import ${BeanUtils}; import ${basePackage}.module.${table.moduleName}.dal.mysql.${table.businessName}.${table.className}Mapper; ## 特殊:主子表专属逻辑 #foreach ($subTable in $subTables) #set ($index = $foreach.count - 1) import ${basePackage}.module.${subTable.moduleName}.dal.mysql.${subTable.businessName}.${subTable.className}Mapper; #end import static ${ServiceExceptionUtilClassName}.exception; import static ${basePackage}.module.${table.moduleName}.enums.ErrorCodeConstants.*; /** * ${table.classComment} Service 实现类 * * @author ${table.author} */ @Service @Validated public class ${table.className}ServiceImpl implements ${table.className}Service { @Resource private ${table.className}Mapper ${classNameVar}Mapper; ## 特殊:主子表专属逻辑 #foreach ($subTable in $subTables) #set ($index = $foreach.count - 1) @Resource private ${subTable.className}Mapper ${subClassNameVars.get($index)}Mapper; #end @Override ## 特殊:主子表专属逻辑(非 ERP 模式) #if ( $subTables && $subTables.size() > 0 && $table.templateType != 11 ) @Transactional(rollbackFor = Exception.class) #end public ${primaryColumn.javaType} create${simpleClassName}(${sceneEnum.prefixClass}${table.className}SaveReqVO createReqVO) { ## 特殊:树表专属逻辑 #if ( $table.templateType == 2 ) #set ($TreeParentJavaField = $treeParentColumn.javaField.substring(0,1).toUpperCase() + ${treeParentColumn.javaField.substring(1)})##首字母大写 #set ($TreeNameJavaField = $treeNameColumn.javaField.substring(0,1).toUpperCase() + ${treeNameColumn.javaField.substring(1)})##首字母大写 // 校验${treeParentColumn.columnComment}的有效性 validateParent${simpleClassName}(null, createReqVO.get${TreeParentJavaField}()); // 校验${treeNameColumn.columnComment}的唯一性 validate${simpleClassName}${TreeNameJavaField}Unique(null, createReqVO.get${TreeParentJavaField}(), createReqVO.get${TreeNameJavaField}()); #end // 插入 ${table.className}DO ${classNameVar} = BeanUtils.toBean(createReqVO, ${table.className}DO.class); ${classNameVar}Mapper.insert(${classNameVar}); ## 特殊:主子表专属逻辑(非 ERP 模式) #if ( $subTables && $subTables.size() > 0 && $table.templateType != 11 ) // 插入子表 #foreach ($subTable in $subTables) #set ($index = $foreach.count - 1) #set ($subSimpleClassName = $subSimpleClassNames.get($index)) #set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 #if ( $subTable.subJoinMany) create${subSimpleClassName}List(${classNameVar}.getId(), createReqVO.get${subSimpleClassNames.get($index)}s()); #else create${subSimpleClassName}(${classNameVar}.getId(), createReqVO.get${subSimpleClassNames.get($index)}()); #end #end #end // 返回 return ${classNameVar}.getId(); } @Override ## 特殊:主子表专属逻辑(非 ERP 模式) #if ( $subTables && $subTables.size() > 0 && $table.templateType != 11 ) @Transactional(rollbackFor = Exception.class) #end public void update${simpleClassName}(${sceneEnum.prefixClass}${table.className}SaveReqVO updateReqVO) { // 校验存在 validate${simpleClassName}Exists(updateReqVO.getId()); ## 特殊:树表专属逻辑 #if ( $table.templateType == 2 ) #set ($TreeParentJavaField = $treeParentColumn.javaField.substring(0,1).toUpperCase() + ${treeParentColumn.javaField.substring(1)})##首字母大写 #set ($TreeNameJavaField = $treeNameColumn.javaField.substring(0,1).toUpperCase() + ${treeNameColumn.javaField.substring(1)})##首字母大写 // 校验${treeParentColumn.columnComment}的有效性 validateParent${simpleClassName}(updateReqVO.getId(), updateReqVO.get${TreeParentJavaField}()); // 校验${treeNameColumn.columnComment}的唯一性 validate${simpleClassName}${TreeNameJavaField}Unique(updateReqVO.getId(), updateReqVO.get${TreeParentJavaField}(), updateReqVO.get${TreeNameJavaField}()); #end // 更新 ${table.className}DO updateObj = BeanUtils.toBean(updateReqVO, ${table.className}DO.class); ${classNameVar}Mapper.updateById(updateObj); ## 特殊:主子表专属逻辑(非 ERP 模式) #if ( $subTables && $subTables.size() > 0 && $table.templateType != 11) // 更新子表 #foreach ($subTable in $subTables) #set ($index = $foreach.count - 1) #set ($subSimpleClassName = $subSimpleClassNames.get($index)) #set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 #if ( $subTable.subJoinMany) update${subSimpleClassName}List(updateReqVO.getId(), updateReqVO.get${subSimpleClassNames.get($index)}s()); #else update${subSimpleClassName}(updateReqVO.getId(), updateReqVO.get${subSimpleClassNames.get($index)}()); #end #end #end } @Override ## 特殊:主子表专属逻辑 #if ( $subTables && $subTables.size() > 0) @Transactional(rollbackFor = Exception.class) #end public void delete${simpleClassName}(${primaryColumn.javaType} id) { // 校验存在 validate${simpleClassName}Exists(id); ## 特殊:树表专属逻辑 #if ( $table.templateType == 2 ) #set ($ParentJavaField = $treeParentColumn.javaField.substring(0,1).toUpperCase() + ${treeParentColumn.javaField.substring(1)})##首字母大写 // 校验是否有子${table.classComment} if (${classNameVar}Mapper.selectCountBy${ParentJavaField}(id) > 0) { throw exception(${simpleClassName_underlineCase.toUpperCase()}_EXITS_CHILDREN); } #end // 删除 ${classNameVar}Mapper.deleteById(id); ## 特殊:主子表专属逻辑 #if ( $subTables && $subTables.size() > 0) // 删除子表 #foreach ($subTable in $subTables) #set ($index = $foreach.count - 1) #set ($subSimpleClassName = $subSimpleClassNames.get($index)) #set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 delete${subSimpleClassName}By${SubJoinColumnName}(id); #end #end } private void validate${simpleClassName}Exists(${primaryColumn.javaType} id) { if (${classNameVar}Mapper.selectById(id) == null) { throw exception(${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS); } } ## 特殊:树表专属逻辑 #if ( $table.templateType == 2 ) #set ($TreeParentJavaField = $treeParentColumn.javaField.substring(0,1).toUpperCase() + ${treeParentColumn.javaField.substring(1)})##首字母大写 #set ($TreeNameJavaField = $treeNameColumn.javaField.substring(0,1).toUpperCase() + ${treeNameColumn.javaField.substring(1)})##首字母大写 private void validateParent${simpleClassName}(Long id, Long ${treeParentColumn.javaField}) { if (${treeParentColumn.javaField} == null || ${simpleClassName}DO.${treeParentColumn_javaField_underlineCase.toUpperCase()}_ROOT.equals(${treeParentColumn.javaField})) { return; } // 1. 不能设置自己为父${table.classComment} if (Objects.equals(id, ${treeParentColumn.javaField})) { throw exception(${simpleClassName_underlineCase.toUpperCase()}_PARENT_ERROR); } // 2. 父${table.classComment}不存在 ${simpleClassName}DO parent${simpleClassName} = ${classNameVar}Mapper.selectById(${treeParentColumn.javaField}); if (parent${simpleClassName} == null) { throw exception(${simpleClassName_underlineCase.toUpperCase()}_PARENT_NOT_EXITS); } // 3. 递归校验父${table.classComment},如果父${table.classComment}是自己的子${table.classComment},则报错,避免形成环路 if (id == null) { // id 为空,说明新增,不需要考虑环路 return; } for (int i = 0; i < Short.MAX_VALUE; i++) { // 3.1 校验环路 ${treeParentColumn.javaField} = parent${simpleClassName}.get${TreeParentJavaField}(); if (Objects.equals(id, ${treeParentColumn.javaField})) { throw exception(${simpleClassName_underlineCase.toUpperCase()}_PARENT_IS_CHILD); } // 3.2 继续递归下一级父${table.classComment} if (${treeParentColumn.javaField} == null || ${simpleClassName}DO.${treeParentColumn_javaField_underlineCase.toUpperCase()}_ROOT.equals(${treeParentColumn.javaField})) { break; } parent${simpleClassName} = ${classNameVar}Mapper.selectById(${treeParentColumn.javaField}); if (parent${simpleClassName} == null) { break; } } } private void validate${simpleClassName}${TreeNameJavaField}Unique(Long id, Long ${treeParentColumn.javaField}, String ${treeNameColumn.javaField}) { ${simpleClassName}DO ${classNameVar} = ${classNameVar}Mapper.selectBy${TreeParentJavaField}And${TreeNameJavaField}(${treeParentColumn.javaField}, ${treeNameColumn.javaField}); if (${classNameVar} == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的${table.classComment} if (id == null) { throw exception(${simpleClassName_underlineCase.toUpperCase()}_${treeNameColumn_javaField_underlineCase.toUpperCase()}_DUPLICATE); } if (!Objects.equals(${classNameVar}.getId(), id)) { throw exception(${simpleClassName_underlineCase.toUpperCase()}_${treeNameColumn_javaField_underlineCase.toUpperCase()}_DUPLICATE); } } #end @Override public ${table.className}DO get${simpleClassName}(${primaryColumn.javaType} id) { return ${classNameVar}Mapper.selectById(id); } ## 特殊:树表专属逻辑(树不需要分页接口) #if ( $table.templateType != 2 ) @Override public PageResult<${table.className}DO> get${simpleClassName}Page(${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO) { return ${classNameVar}Mapper.selectPage(pageReqVO); } #else @Override public List<${table.className}DO> get${simpleClassName}List(${sceneEnum.prefixClass}${table.className}ListReqVO listReqVO) { return ${classNameVar}Mapper.selectList(listReqVO); } #end ## 特殊:主子表专属逻辑 #foreach ($subTable in $subTables) #set ($index = $foreach.count - 1) #set ($subSimpleClassName = $subSimpleClassNames.get($index)) #set ($simpleClassNameUnderlineCase = $simpleClassNameUnderlineCases.get($index)) #set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段 #set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 #set ($subClassNameVar = $subClassNameVars.get($index)) // ==================== 子表($subTable.classComment) ==================== ## 情况一:MASTER_ERP 时,需要分查询页子表 #if ( $table.templateType == 11 ) @Override public PageResult<${subTable.className}DO> get${subSimpleClassName}Page(PageParam pageReqVO, ${subJoinColumn.javaType} ${subJoinColumn.javaField}) { return ${subClassNameVars.get($index)}Mapper.selectPage(pageReqVO, ${subJoinColumn.javaField}); } ## 情况二:非 MASTER_ERP 时,需要列表查询子表 #else #if ( $subTable.subJoinMany ) @Override public List<${subTable.className}DO> get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) { return ${subClassNameVars.get($index)}Mapper.selectListBy${SubJoinColumnName}(${subJoinColumn.javaField}); } #else @Override public ${subTable.className}DO get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) { return ${subClassNameVars.get($index)}Mapper.selectBy${SubJoinColumnName}(${subJoinColumn.javaField}); } #end #end ## 情况一:MASTER_ERP 时,支持单个的新增、修改、删除操作 #if ( $table.templateType == 11 ) @Override public ${subPrimaryColumn.javaType} create${subSimpleClassName}(${subTable.className}DO ${subClassNameVar}) { ## 特殊:一对一时,需要保证只有一条,不能重复插入 #if ( !$subTable.subJoinMany) // 校验是否已经存在 if (${subClassNameVars.get($index)}Mapper.selectBy${SubJoinColumnName}(${subClassNameVar}.get${SubJoinColumnName}()) != null) { throw exception(${simpleClassNameUnderlineCase.toUpperCase()}_EXISTS); } // 插入 #end ${subClassNameVars.get($index)}Mapper.insert(${subClassNameVar}); return ${subClassNameVar}.getId(); } @Override public void update${subSimpleClassName}(${subTable.className}DO ${subClassNameVar}) { // 校验存在 validate${subSimpleClassName}Exists(${subClassNameVar}.getId()); // 更新 ${subClassNameVars.get($index)}Mapper.updateById(${subClassNameVar}); } @Override public void delete${subSimpleClassName}(${subPrimaryColumn.javaType} id) { // 校验存在 validate${subSimpleClassName}Exists(id); // 删除 ${subClassNameVars.get($index)}Mapper.deleteById(id); } @Override public ${subTable.className}DO get${subSimpleClassName}(${subPrimaryColumn.javaType} id) { return ${subClassNameVars.get($index)}Mapper.selectById(id); } private void validate${subSimpleClassName}Exists(${subPrimaryColumn.javaType} id) { if (${subClassNameVar}Mapper.selectById(id) == null) { throw exception(${simpleClassNameUnderlineCase.toUpperCase()}_NOT_EXISTS); } } ## 情况二:非 MASTER_ERP 时,支持批量的新增、修改操作 #else #if ( $subTable.subJoinMany) private void create${subSimpleClassName}List(${primaryColumn.javaType} ${subJoinColumn.javaField}, List<${subTable.className}DO> list) { list.forEach(o -> o.set$SubJoinColumnName(${subJoinColumn.javaField})); ${subClassNameVars.get($index)}Mapper.insertBatch(list); } private void update${subSimpleClassName}List(${primaryColumn.javaType} ${subJoinColumn.javaField}, List<${subTable.className}DO> list) { delete${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}); list.forEach(o -> o.setId(null).setUpdater(null).setUpdateTime(null)); // 解决更新情况下:1)id 冲突;2)updateTime 不更新 create${subSimpleClassName}List(${subJoinColumn.javaField}, list); } #else private void create${subSimpleClassName}(${primaryColumn.javaType} ${subJoinColumn.javaField}, ${subTable.className}DO ${subClassNameVar}) { if (${subClassNameVar} == null) { return; } ${subClassNameVar}.set$SubJoinColumnName(${subJoinColumn.javaField}); ${subClassNameVars.get($index)}Mapper.insert(${subClassNameVar}); } private void update${subSimpleClassName}(${primaryColumn.javaType} ${subJoinColumn.javaField}, ${subTable.className}DO ${subClassNameVar}) { if (${subClassNameVar} == null) { return; } ${subClassNameVar}.set$SubJoinColumnName(${subJoinColumn.javaField}); ${subClassNameVar}.setUpdater(null).setUpdateTime(null); // 解决更新情况下:updateTime 不更新 ${subClassNameVars.get($index)}Mapper.insertOrUpdate(${subClassNameVar}); } #end #end private void delete${subSimpleClassName}By${SubJoinColumnName}(${primaryColumn.javaType} ${subJoinColumn.javaField}) { ${subClassNameVars.get($index)}Mapper.deleteBy${SubJoinColumnName}(${subJoinColumn.javaField}); } #end } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/java/test/serviceTest.vm ================================================ package ${basePackage}.module.${table.moduleName}.service.${table.businessName}; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import ${jakartaPackage}.annotation.Resource; import ${baseFrameworkPackage}.test.core.ut.BaseDbUnitTest; import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; import ${basePackage}.module.${table.moduleName}.dal.mysql.${table.businessName}.${table.className}Mapper; import ${PageResultClassName}; import ${jakartaPackage}.annotation.Resource; import org.springframework.context.annotation.Import; import java.util.*; import java.time.LocalDateTime; import static cn.hutool.core.util.RandomUtil.*; import static ${basePackage}.module.${table.moduleName}.enums.ErrorCodeConstants.*; import static ${baseFrameworkPackage}.test.core.util.AssertUtils.*; import static ${baseFrameworkPackage}.test.core.util.RandomUtils.*; import static ${LocalDateTimeUtilsClassName}.*; import static ${ObjectUtilsClassName}.*; import static ${DateUtilsClassName}.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; ## 字段模板 #macro(getPageCondition $VO) // mock 数据 ${table.className}DO db${simpleClassName} = randomPojo(${table.className}DO.class, o -> { // 等会查询到 #foreach ($column in $columns) #if (${column.listOperation}) #set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 o.set$JavaField(null); #end #end }); ${classNameVar}Mapper.insert(db${simpleClassName}); #foreach ($column in $columns) #if (${column.listOperation}) #set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 // 测试 ${column.javaField} 不匹配 ${classNameVar}Mapper.insert(cloneIgnoreId(db${simpleClassName}, o -> o.set$JavaField(null))); #end #end // 准备参数 ${sceneEnum.prefixClass}${table.className}${VO} reqVO = new ${sceneEnum.prefixClass}${table.className}${VO}(); #foreach ($column in $columns) #if (${column.listOperation}) #set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 #if (${column.listOperationCondition} == "BETWEEN")## BETWEEN 的情况 reqVO.set${JavaField}(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); #else reqVO.set$JavaField(null); #end #end #end #end /** * {@link ${table.className}ServiceImpl} 的单元测试类 * * @author ${table.author} */ @Import(${table.className}ServiceImpl.class) public class ${table.className}ServiceImplTest extends BaseDbUnitTest { @Resource private ${table.className}ServiceImpl ${classNameVar}Service; @Resource private ${table.className}Mapper ${classNameVar}Mapper; @Test public void testCreate${simpleClassName}_success() { // 准备参数 ${sceneEnum.prefixClass}${table.className}SaveReqVO createReqVO = randomPojo(${sceneEnum.prefixClass}${table.className}SaveReqVO.class).setId(null); // 调用 ${primaryColumn.javaType} ${classNameVar}Id = ${classNameVar}Service.create${simpleClassName}(createReqVO); // 断言 assertNotNull(${classNameVar}Id); // 校验记录的属性是否正确 ${table.className}DO ${classNameVar} = ${classNameVar}Mapper.selectById(${classNameVar}Id); assertPojoEquals(createReqVO, ${classNameVar}, "id"); } @Test public void testUpdate${simpleClassName}_success() { // mock 数据 ${table.className}DO db${simpleClassName} = randomPojo(${table.className}DO.class); ${classNameVar}Mapper.insert(db${simpleClassName});// @Sql: 先插入出一条存在的数据 // 准备参数 ${sceneEnum.prefixClass}${table.className}SaveReqVO updateReqVO = randomPojo(${sceneEnum.prefixClass}${table.className}SaveReqVO.class, o -> { o.setId(db${simpleClassName}.getId()); // 设置更新的 ID }); // 调用 ${classNameVar}Service.update${simpleClassName}(updateReqVO); // 校验是否更新正确 ${table.className}DO ${classNameVar} = ${classNameVar}Mapper.selectById(updateReqVO.getId()); // 获取最新的 assertPojoEquals(updateReqVO, ${classNameVar}); } @Test public void testUpdate${simpleClassName}_notExists() { // 准备参数 ${sceneEnum.prefixClass}${table.className}SaveReqVO updateReqVO = randomPojo(${sceneEnum.prefixClass}${table.className}SaveReqVO.class); // 调用, 并断言异常 assertServiceException(() -> ${classNameVar}Service.update${simpleClassName}(updateReqVO), ${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS); } @Test public void testDelete${simpleClassName}_success() { // mock 数据 ${table.className}DO db${simpleClassName} = randomPojo(${table.className}DO.class); ${classNameVar}Mapper.insert(db${simpleClassName});// @Sql: 先插入出一条存在的数据 // 准备参数 ${primaryColumn.javaType} id = db${simpleClassName}.getId(); // 调用 ${classNameVar}Service.delete${simpleClassName}(id); // 校验数据不存在了 assertNull(${classNameVar}Mapper.selectById(id)); } @Test public void testDelete${simpleClassName}_notExists() { // 准备参数 ${primaryColumn.javaType} id = random${primaryColumn.javaType}Id(); // 调用, 并断言异常 assertServiceException(() -> ${classNameVar}Service.delete${simpleClassName}(id), ${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS); } ## 特殊:树表专属逻辑(树不需要分页接口) #if ( $table.templateType != 2 ) @Test @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 public void testGet${simpleClassName}Page() { #getPageCondition("PageReqVO") // 调用 PageResult<${table.className}DO> pageResult = ${classNameVar}Service.get${simpleClassName}Page(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(db${simpleClassName}, pageResult.getList().get(0)); } #else @Test @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 public void testGet${simpleClassName}List() { #getPageCondition("ListReqVO") // 调用 List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}List(reqVO); // 断言 assertEquals(1, list.size()); assertPojoEquals(db${simpleClassName}, list.get(0)); } #end } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/sql/h2.vm ================================================ -- 将该建表 SQL 语句,添加到 yshop-module-${table.moduleName}-biz 模块的 test/resources/sql/create_tables.sql 文件里 CREATE TABLE IF NOT EXISTS "${table.tableName.toLowerCase()}" ( #foreach ($column in $columns) #if (${column.javaType} == 'Long') #set ($dataType='bigint') #elseif (${column.javaType} == 'Integer') #set ($dataType='int') #elseif (${column.javaType} == 'Boolean') #set ($dataType='bit') #elseif (${column.javaType} == 'Date') #set ($dataType='datetime') #else #set ($dataType='varchar') #end #if (${column.primaryKey})##处理主键 "${column.javaField}"#if (${column.javaType} == 'String') ${dataType} NOT NULL#else ${dataType} NOT NULL GENERATED BY DEFAULT AS IDENTITY#end, #else #if (${column.columnName} == 'create_time') "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, #elseif (${column.columnName} == 'update_time') "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, #elseif (${column.columnName} == 'creator' || ${column.columnName} == 'updater') "${column.columnName}" ${dataType} DEFAULT '', #elseif (${column.columnName} == 'deleted') "deleted" bit NOT NULL DEFAULT FALSE, #elseif (${column.columnName} == 'tenantId') "tenant_id" bigint NOT NULL DEFAULT 0, #else "${column.columnName.toLowerCase()}" ${dataType}#if (${column.nullable} == false) NOT NULL#end, #end #end #end PRIMARY KEY ("${primaryColumn.columnName.toLowerCase()}") ) COMMENT '${table.tableComment}'; -- 将该删表 SQL 语句,添加到 yshop-module-${table.moduleName}-biz 模块的 test/resources/sql/clean.sql 文件里 DELETE FROM "${table.tableName}"; ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/sql/sql.vm ================================================ -- 菜单 SQL INSERT INTO system_menu( name, permission, type, sort, parent_id, path, icon, component, status, component_name ) VALUES ( '${table.classComment}管理', '', 2, 0, ${table.parentMenuId}, '${simpleClassName_strikeCase}', '', '${table.moduleName}/${table.businessName}/index', 0, '${table.className}' ); -- 按钮父菜单ID -- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码 SELECT @parentId := LAST_INSERT_ID(); -- 按钮 SQL #set ($functionNames = ['查询', '创建', '更新', '删除', '导出']) #set ($functionOps = ['query', 'create', 'update', 'delete', 'export']) #foreach ($functionName in $functionNames) #set ($index = $foreach.count - 1) INSERT INTO system_menu( name, permission, type, sort, parent_id, path, icon, component, status ) VALUES ( '${table.classComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, @parentId, '', '', '', 0 ); #end ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue/api/api.js.vm ================================================ import request from '@/utils/request' #set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}") // 创建${table.classComment} export function create${simpleClassName}(data) { return request({ url: '${baseURL}/create', method: 'post', data: data }) } // 更新${table.classComment} export function update${simpleClassName}(data) { return request({ url: '${baseURL}/update', method: 'put', data: data }) } // 删除${table.classComment} export function delete${simpleClassName}(id) { return request({ url: '${baseURL}/delete?id=' + id, method: 'delete' }) } // 获得${table.classComment} export function get${simpleClassName}(id) { return request({ url: '${baseURL}/get?id=' + id, method: 'get' }) } #if ( $table.templateType != 2 ) // 获得${table.classComment}分页 export function get${simpleClassName}Page(params) { return request({ url: '${baseURL}/page', method: 'get', params }) } #else // 获得${table.classComment}列表 export function get${simpleClassName}List(params) { return request({ url: '${baseURL}/list', method: 'get', params }) } #end // 导出${table.classComment} Excel export function export${simpleClassName}Excel(params) { return request({ url: '${baseURL}/export-excel', method: 'get', params, responseType: 'blob' }) } ## 特殊:主子表专属逻辑 #foreach ($subTable in $subTables) #set ($index = $foreach.count - 1) #set ($subSimpleClassName = $subSimpleClassNames.get($index)) #set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段 #set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 #set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index)) #set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index)) #set ($subClassNameVar = $subClassNameVars.get($index)) // ==================== 子表($subTable.classComment) ==================== ## 情况一:MASTER_ERP 时,需要分查询页子表 #if ($table.templateType == 11) // 获得${subTable.classComment}分页 export function get${subSimpleClassName}Page(params) { return request({ url: '${baseURL}/${subSimpleClassName_strikeCase}/page', method: 'get', params }) } ## 情况二:非 MASTER_ERP 时,需要列表查询子表 #else #if ($subTable.subJoinMany) // 获得${subTable.classComment}列表 export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}) { return request({ url: '${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=' + ${subJoinColumn.javaField}, method: 'get' }) } #else // 获得${subTable.classComment} export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}) { return request({ url: '${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=' + ${subJoinColumn.javaField}, method: 'get' }) } #end #end ## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作 #if ($table.templateType == 11) // 新增${subTable.classComment} export function create${subSimpleClassName}(data) { return request({ url: '${baseURL}/${subSimpleClassName_strikeCase}/create', method: 'post', data }) } // 修改${subTable.classComment} export function update${subSimpleClassName}(data) { return request({ url: '${baseURL}/${subSimpleClassName_strikeCase}/update', method: 'post', data }) } // 删除${subTable.classComment} export function delete${subSimpleClassName}(id) { return request({ url: '${baseURL}/${subSimpleClassName_strikeCase}/delete?id=' + id, method: 'delete' }) } // 获得${subTable.classComment} export function get${subSimpleClassName}(id) { return request({ url: '${baseURL}/${subSimpleClassName_strikeCase}/get?id=' + id, method: 'get' }) } #end #end ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue/views/components/form_sub_erp.vue.vm ================================================ #set ($subTable = $subTables.get($subIndex))##当前表 #set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 #set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) #set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue/views/components/form_sub_inner.vue.vm ================================================ ## 主表的 normal 和 inner 使用相同的 form 表单 #parse("codegen/vue/views/components/form_sub_normal.vue.vm") ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue/views/components/form_sub_normal.vue.vm ================================================ #set ($subTable = $subTables.get($subIndex))##当前表 #set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 #set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 #set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) #set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue/views/components/list_sub_erp.vue.vm ================================================ #set ($subTable = $subTables.get($subIndex))##当前表 #set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 #set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 #set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) #set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue/views/components/list_sub_inner.vue.vm ================================================ ## 子表的 erp 和 inner 使用相似的 list 列表,差异主要两点: ## 1)inner 使用 list 不分页,erp 使用 page 分页 ## 2)erp 支持单个子表的新增、修改、删除,inner 不支持 #parse("codegen/vue/views/components/list_sub_erp.vue.vm") ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue/views/form.vue.vm ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue/views/index.vue.vm ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue3/api/api.ts.vm ================================================ import request from '@/config/axios' #set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}") // ${table.classComment} VO export interface ${simpleClassName}VO { #foreach ($column in $columns) #if ($column.createOperation || $column.updateOperation) #if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal") ${column.javaField}: number // ${column.columnComment} #elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime") ${column.javaField}: Date // ${column.columnComment} #else ${column.javaField}: ${column.javaType.toLowerCase()} // ${column.columnComment} #end #end #end } // ${table.classComment} API export const ${simpleClassName}Api = { #if ( $table.templateType != 2 ) // 查询${table.classComment}分页 get${simpleClassName}Page: async (params: any) => { return await request.get({ url: `${baseURL}/page`, params }) }, #else // 查询${table.classComment}列表 get${simpleClassName}List: async (params) => { return await request.get({ url: `${baseURL}/list`, params }) }, #end // 查询${table.classComment}详情 get${simpleClassName}: async (id: number) => { return await request.get({ url: `${baseURL}/get?id=` + id }) }, // 新增${table.classComment} create${simpleClassName}: async (data: ${simpleClassName}VO) => { return await request.post({ url: `${baseURL}/create`, data }) }, // 修改${table.classComment} update${simpleClassName}: async (data: ${simpleClassName}VO) => { return await request.put({ url: `${baseURL}/update`, data }) }, // 删除${table.classComment} delete${simpleClassName}: async (id: number) => { return await request.delete({ url: `${baseURL}/delete?id=` + id }) }, // 导出${table.classComment} Excel export${simpleClassName}: async (params) => { return await request.download({ url: `${baseURL}/export-excel`, params }) }, ## 特殊:主子表专属逻辑 #foreach ($subTable in $subTables) #set ($index = $foreach.count - 1) #set ($subSimpleClassName = $subSimpleClassNames.get($index)) #set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段 #set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 #set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index)) #set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index)) #set ($subClassNameVar = $subClassNameVars.get($index)) // ==================== 子表($subTable.classComment) ==================== ## 情况一:MASTER_ERP 时,需要分查询页子表 #if ( $table.templateType == 11 ) // 获得${subTable.classComment}分页 get${subSimpleClassName}Page: async (params) => { return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/page`, params }) }, ## 情况二:非 MASTER_ERP 时,需要列表查询子表 #else #if ( $subTable.subJoinMany ) // 获得${subTable.classComment}列表 get${subSimpleClassName}ListBy${SubJoinColumnName}: async (${subJoinColumn.javaField}) => { return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=` + ${subJoinColumn.javaField} }) }, #else // 获得${subTable.classComment} get${subSimpleClassName}By${SubJoinColumnName}: async (${subJoinColumn.javaField}) => { return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=` + ${subJoinColumn.javaField} }) }, #end #end ## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作 #if ( $table.templateType == 11 ) // 新增${subTable.classComment} create${subSimpleClassName}: async (data) => { return await request.post({ url: `${baseURL}/${subSimpleClassName_strikeCase}/create`, data }) }, // 修改${subTable.classComment} update${subSimpleClassName}: async (data) => { return await request.put({ url: `${baseURL}/${subSimpleClassName_strikeCase}/update`, data }) }, // 删除${subTable.classComment} delete${subSimpleClassName}: async (id: number) => { return await request.delete({ url: `${baseURL}/${subSimpleClassName_strikeCase}/delete?id=` + id }) }, // 获得${subTable.classComment} get${subSimpleClassName}: async (id: number) => { return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/get?id=` + id }) }, #end #end } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm ================================================ #set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 #set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) #set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_inner.vue.vm ================================================ ## 主表的 normal 和 inner 使用相同的 form 表单 #parse("codegen/vue3/views/components/form_sub_normal.vue.vm") ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm ================================================ #set ($subTable = $subTables.get($subIndex))##当前表 #set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 #set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 #set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) #set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm ================================================ #set ($subTable = $subTables.get($subIndex))##当前表 #set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 #set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 #set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) #set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue3/views/components/list_sub_inner.vue.vm ================================================ ## 子表的 erp 和 inner 使用相似的 list 列表,差异主要两点: ## 1)inner 使用 list 不分页,erp 使用 page 分页 ## 2)erp 支持单个子表的新增、修改、删除,inner 不支持 #parse("codegen/vue3/views/components/list_sub_erp.vue.vm") ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue3_schema/api/api.ts.vm ================================================ import request from '@/config/axios' #set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}") export interface ${simpleClassName}VO { #foreach ($column in $columns) #if ($column.createOperation || $column.updateOperation) #if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal") ${column.javaField}: number #elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdatetime") ${column.javaField}: Date #else ${column.javaField}: ${column.javaType.toLowerCase()} #end #end #end } // 查询${table.classComment}列表 export const get${simpleClassName}Page = async (params) => { return await request.get({ url: '${baseURL}/page', params }) } // 查询${table.classComment}详情 export const get${simpleClassName} = async (id: number) => { return await request.get({ url: '${baseURL}/get?id=' + id }) } // 新增${table.classComment} export const create${simpleClassName} = async (data: ${simpleClassName}VO) => { return await request.post({ url: '${baseURL}/create', data }) } // 修改${table.classComment} export const update${simpleClassName} = async (data: ${simpleClassName}VO) => { return await request.put({ url: '${baseURL}/update', data }) } // 删除${table.classComment} export const delete${simpleClassName} = async (id: number) => { return await request.delete({ url: '${baseURL}/delete?id=' + id }) } // 导出${table.classComment} Excel export const export${simpleClassName}Api = async (params) => { return await request.download({ url: '${baseURL}/export-excel', params }) } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue3_schema/views/data.ts.vm ================================================ import type { CrudSchema } from '@/hooks/web/useCrudSchemas' import { dateFormatter } from '@/utils/formatTime' // 表单校验 export const rules = reactive({ #foreach ($column in $columns) #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键 #set($comment=$column.columnComment) $column.javaField: [required], #end #end }) // CrudSchema https://www.yixiang.co/vue3/crud-schema/ const crudSchemas = reactive([ #foreach($column in $columns) #if ($column.listOperation || $column.listOperationResult || $column.createOperation || $column.updateOperation) #set ($dictType = $column.dictType) #set ($javaField = $column.javaField) #set ($javaType = $column.javaType) { label: '${column.columnComment}', field: '${column.javaField}', ## ========= 字典部分 ========= #if ("" != $dictType)## 有数据字典 dictType: DICT_TYPE.$dictType.toUpperCase(), #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short") dictClass: 'number', #elseif ($javaType == "String") dictClass: 'string', #elseif ($javaType == "Boolean") dictClass: 'boolean', #end #end ## ========= Table 表格部分 ========= #if (!$column.listOperationResult) isTable: false, #else #if ($column.htmlType == "datetime") formatter: dateFormatter, #end #end ## ========= Search 表格部分 ========= #if ($column.listOperation) isSearch: true, #if ($column.htmlType == "datetime") search: { component: 'DatePicker', componentProps: { valueFormat: 'YYYY-MM-DD HH:mm:ss', type: 'daterange', defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')] } }, #end #end ## ========= Form 表单部分 ========= #if ((!$column.createOperation && !$column.updateOperation) || $column.primaryKey) isForm: false, #else #if($column.htmlType == "imageUpload")## 图片上传 form: { component: 'UploadImg' }, #elseif($column.htmlType == "fileUpload")## 文件上传 form: { component: 'UploadFile' }, #elseif($column.htmlType == "editor")## 文本编辑器 form: { component: 'Editor', componentProps: { valueHtml: '', height: 200 } }, #elseif($column.htmlType == "select")## 下拉框 form: { component: 'SelectV2' }, #elseif($column.htmlType == "checkbox")## 多选框 form: { component: 'Checkbox' }, #elseif($column.htmlType == "radio")## 单选框 form: { component: 'Radio' }, #elseif($column.htmlType == "datetime")## 时间框 form: { component: 'DatePicker', componentProps: { type: 'datetime', valueFormat: 'x' } }, #elseif($column.htmlType == "textarea")## 文本框 form: { component: 'Input', componentProps: { type: 'textarea', rows: 4 }, colProps: { span: 24 } }, #elseif(${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer")## 文本框 form: { component: 'InputNumber', value: 0 }, #end #end }, #end #end { label: '操作', field: 'action', isForm: false } ]) export const { allSchemas } = useCrudSchemas(crudSchemas) ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue3_schema/views/form.vue.vm ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue3_schema/views/index.vue.vm ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue3_vben/api/api.ts.vm ================================================ import { defHttp } from '@/utils/http/axios' #set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}") // 查询${table.classComment}列表 export function get${simpleClassName}Page(params) { return defHttp.get({ url: '${baseURL}/page', params }) } // 查询${table.classComment}详情 export function get${simpleClassName}(id: number) { return defHttp.get({ url: `${baseURL}/get?id=${id}` }) } // 新增${table.classComment} export function create${simpleClassName}(data) { return defHttp.post({ url: '${baseURL}/create', data }) } // 修改${table.classComment} export function update${simpleClassName}(data) { return defHttp.put({ url: '${baseURL}/update', data }) } // 删除${table.classComment} export function delete${simpleClassName}(id: number) { return defHttp.delete({ url: `${baseURL}/delete?id=${id}` }) } // 导出${table.classComment} Excel export function export${simpleClassName}(params) { return defHttp.download({ url: '${baseURL}/export-excel', params }, '${table.classComment}.xls') } ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue3_vben/views/data.ts.vm ================================================ import type {BasicColumn, FormSchema} from '@/components/Table' import {useRender} from '@/components/Table' import {DICT_TYPE, getDictOptions} from '@/utils/dict' export const columns: BasicColumn[] = [ #foreach($column in $columns) #if ($column.listOperationResult) #set ($dictType=$column.dictType) #set ($javaField = $column.javaField) #set ($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) #set ($comment=$column.columnComment) #if ($column.javaType == "LocalDateTime")## 时间类型 { title: '${comment}', dataIndex: '${javaField}', width: 180, customRender: ({ text }) => { return useRender.renderDate(text) }, }, #elseif("" != $column.dictType)## 数据字典 { title: '${comment}', dataIndex: '${javaField}', width: 180, customRender: ({ text }) => { return useRender.renderDict(text, DICT_TYPE.$dictType.toUpperCase()) }, }, #else { title: '${comment}', dataIndex: '${javaField}', width: 160, }, #end #end #end ] export const searchFormSchema: FormSchema[] = [ #foreach($column in $columns) #if ($column.listOperation) #set ($dictType=$column.dictType) #set ($javaField = $column.javaField) #set ($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) #set ($comment=$column.columnComment) { label: '${comment}', field: '${javaField}', #if ($column.htmlType == "input") component: 'Input', #elseif ($column.htmlType == "select") component: 'Select', componentProps: { #if ("" != $dictType)## 设置了 dictType 数据字典的情况 options: getDictOptions(DICT_TYPE.$dictType.toUpperCase()), #else## 未设置 dictType 数据字典的情况 options: [], #end }, #elseif ($column.htmlType == "radio") component: 'Radio', componentProps: { #if ("" != $dictType)## 设置了 dictType 数据字典的情况 options: getDictOptions(DICT_TYPE.$dictType.toUpperCase()), #else## 未设置 dictType 数据字典的情况 options: [], #end }, #elseif($column.htmlType == "datetime") component: 'RangePicker', #end colProps: { span: 8 }, }, #end #end ] export const createFormSchema: FormSchema[] = [ { label: '编号', field: 'id', show: false, component: 'Input', }, #foreach($column in $columns) #if ($column.createOperation) #set ($dictType = $column.dictType) #set ($javaField = $column.javaField) #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) #set ($comment = $column.columnComment) #if (!$column.primaryKey)## 忽略主键,不用在表单里 { label: '${comment}', field: '${javaField}', #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键 required: true, #end #if ($column.htmlType == "input") component: 'Input', #elseif($column.htmlType == "imageUpload")## 图片上传 component: 'FileUpload', componentProps: { fileType: 'image', maxCount: 1, }, #elseif($column.htmlType == "fileUpload")## 文件上传 component: 'FileUpload', componentProps: { fileType: 'file', maxCount: 1, }, #elseif($column.htmlType == "editor")## 文本编辑器 component: 'Editor', #elseif($column.htmlType == "select")## 下拉框 component: 'Select', componentProps: { #if ("" != $dictType)## 有数据字典 options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'), #else##没数据字典 options:[], #end }, #elseif($column.htmlType == "checkbox")## 多选框 component: 'Checkbox', componentProps: { #if ("" != $dictType)## 有数据字典 options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'), #else##没数据字典 options:[], #end }, #elseif($column.htmlType == "radio")## 单选框 component: 'RadioButtonGroup', componentProps: { #if ("" != $dictType)## 有数据字典 options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'), #else##没数据字典 options:[], #end }, #elseif($column.htmlType == "datetime")## 时间框 component: 'DatePicker', componentProps: { showTime: true, format: 'YYYY-MM-DD HH:mm:ss', valueFormat: 'x', }, #elseif($column.htmlType == "textarea")## 文本域 component: 'InputTextArea', #end }, #end #end #end ] export const updateFormSchema: FormSchema[] = [ { label: '编号', field: 'id', show: false, component: 'Input', }, #foreach($column in $columns) #if ($column.updateOperation) #set ($dictType = $column.dictType) #set ($javaField = $column.javaField) #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) #set ($comment = $column.columnComment) #if (!$column.primaryKey)## 忽略主键,不用在表单里 { label: '${comment}', field: '${javaField}', #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键 required: true, #end #if ($column.htmlType == "input") component: 'Input', #elseif($column.htmlType == "imageUpload")## 图片上传 component: 'FileUpload', componentProps: { fileType: 'image', maxCount: 1, }, #elseif($column.htmlType == "fileUpload")## 文件上传 component: 'FileUpload', componentProps: { fileType: 'file', maxCount: 1, }, #elseif($column.htmlType == "editor")## 文本编辑器 component: 'Editor', #elseif($column.htmlType == "select")## 下拉框 component: 'Select', componentProps: { #if ("" != $dictType)## 有数据字典 options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'), #else##没数据字典 options:[], #end }, #elseif($column.htmlType == "checkbox")## 多选框 component: 'Checkbox', componentProps: { #if ("" != $dictType)## 有数据字典 options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'), #else##没数据字典 options:[], #end }, #elseif($column.htmlType == "radio")## 单选框 component: 'RadioButtonGroup', componentProps: { #if ("" != $dictType)## 有数据字典 options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'), #else##没数据字典 options:[], #end }, #elseif($column.htmlType == "datetime")## 时间框 component: 'DatePicker', componentProps: { showTime: true, format: 'YYYY-MM-DD HH:mm:ss', valueFormat: 'x', }, #elseif($column.htmlType == "textarea")## 文本域 component: 'InputTextArea', #end }, #end #end #end ] ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue3_vben/views/form.vue.vm ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-infra/yshop-module-infra-biz/src/main/resources/codegen/vue3_vben/views/index.vue.vm ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-mall/pom.xml ================================================ yshop co.yixiang.boot ${revision} 4.0.0 yshop-module-mall pom ${project.artifactId} 商城大模块,由 product 商品、promotion 营销、trade 交易等组成 yshop-module-product-api yshop-module-product-biz yshop-module-shop-api yshop-module-shop-biz yshop-module-order-api yshop-module-order-biz yshop-module-store-api yshop-module-store-biz ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-api/pom.xml ================================================ 4.0.0 co.yixiang.boot yshop-module-mall ${revision} yshop-module-order-api jar ${project.artifactId} order 模块 API,暴露给其它模块调用 co.yixiang.boot yshop-common org.springframework.boot spring-boot-starter-validation true ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-api/src/main/java/co/yixiang/yshop/module/order/enums/AdminAfterOrderStatusEnum.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.order.enums; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.stream.Stream; /** * @author hupeng * 后台订单相关枚举 */ @Getter @AllArgsConstructor public enum AdminAfterOrderStatusEnum { STATUS_1(1,"售后中"), STATUS_2(2,"已完成"); private Integer value; private String desc; public static AdminAfterOrderStatusEnum toType(int value) { return Stream.of(AdminAfterOrderStatusEnum.values()) .filter(p -> p.value == value) .findAny() .orElse(null); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-api/src/main/java/co/yixiang/yshop/module/order/enums/AdminOrderStatusEnum.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.order.enums; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.stream.Stream; /** * @author hupeng * 后台订单相关枚举 */ @Getter @AllArgsConstructor public enum AdminOrderStatusEnum { STATUS_0(0,"未支付"), STATUS_1(1,"未发货"), STATUS_2(2,"待收货"), STATUS_3(3,"待评价"), STATUS_4(4,"交易完成"), STATUS_5(5,"已退款"), STATUS_6(6,"已删除"); private Integer value; private String desc; public static AdminOrderStatusEnum toType(int value) { return Stream.of(AdminOrderStatusEnum.values()) .filter(p -> p.value == value) .findAny() .orElse(null); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-api/src/main/java/co/yixiang/yshop/module/order/enums/AfterChangeTypeEnum.java ================================================ package co.yixiang.yshop.module.order.enums; import lombok.AllArgsConstructor; import lombok.Getter; /** * @author hupeng * 售后状态枚举 */ @Getter @AllArgsConstructor public enum AfterChangeTypeEnum { STATE_0(0,"售后订单生成"), STATE_1(1,"后台审核成功"), STATE_2(2,"用户发货"), STATE_3(3,"打款"), STATE_4(4,"审核失败"), STATE_5(5,"用户撤销"); private Integer value; private String desc; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-api/src/main/java/co/yixiang/yshop/module/order/enums/AfterSalesStatusEnum.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.order.enums; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.stream.Stream; /** * 售后状态枚举 * * @author hupeng * @date 2023/6/23 */ @Getter @AllArgsConstructor public enum AfterSalesStatusEnum { STATUS_0(0,"已提交等待平台审核"), STATUS_1(1,"平台已审核,等待用户发货/退款"), STATUS_2(2,"用户已发货"), STATUS_3(3,"已完成"); private Integer value; private String desc; public static AfterSalesStatusEnum toType(int value) { return Stream.of(AfterSalesStatusEnum.values()) .filter(p -> p.value == value) .findAny() .orElse(null); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-api/src/main/java/co/yixiang/yshop/module/order/enums/AfterStatusEnum.java ================================================ package co.yixiang.yshop.module.order.enums; import lombok.AllArgsConstructor; import lombok.Getter; /** * @author hupeng * 售后状态枚举 */ @Getter @AllArgsConstructor public enum AfterStatusEnum { STATE_0(0,"正常"), STATE_1(1,"用户取消"), STATE_2(2,"商家拒绝"); private Integer value; private String desc; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-api/src/main/java/co/yixiang/yshop/module/order/enums/AfterTypeEnum.java ================================================ package co.yixiang.yshop.module.order.enums; import lombok.AllArgsConstructor; import lombok.Getter; /** * @author hupeng * 售后类型枚举 */ @Getter @AllArgsConstructor public enum AfterTypeEnum { TYPE_1(1,"同意"), TYPE_2(2,"拒绝"), SERVICE_TYPE_0(0,"仅退款"), SERVICE_TYPE_1(1,"退货退款"); private Integer value; private String desc; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-api/src/main/java/co/yixiang/yshop/module/order/enums/AppFromEnum.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.order.enums; import lombok.AllArgsConstructor; import lombok.Getter; /** * @author hupeng * 应用来源相关枚举 */ @Getter @AllArgsConstructor public enum AppFromEnum { WEIXIN_H5("weixinh5","weixinh5"), H5("h5","H5"), WECHAT("wechat","公众号"), APP("app","APP"), PC("pc","PC"), ROUNTINE("routine","小程序"), UNIAPPH5("uniappH5","uniappH5"); private String value; private String desc; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-api/src/main/java/co/yixiang/yshop/module/order/enums/ErrorCodeConstants.java ================================================ package co.yixiang.yshop.module.order.enums; import co.yixiang.yshop.framework.common.exception.ErrorCode; public interface ErrorCodeConstants { // ========== 订单1008007000========== ErrorCode STORE_ORDER_NOT_EXISTS = new ErrorCode(1008007000, "订单不存在"); ErrorCode STORE_ORDER_CART_INFO_NOT_EXISTS = new ErrorCode(1008007001, "订单购物详情不存在"); ErrorCode STORE_ORDER_STATUS_NOT_EXISTS = new ErrorCode(1008007002, "订单操作记录不存在"); ErrorCode STORE_AFTER_SALES_NOT_EXISTS = new ErrorCode(1008007003, "售后记录不存在"); ErrorCode STORE_AFTER_SALES_ITEM_NOT_EXISTS = new ErrorCode(1008007004, "售后子不存在"); ErrorCode STORE_AFTER_SALES_STATUS_NOT_EXISTS = new ErrorCode(1008007005, "售后订单操作详情不存在"); ErrorCode INVALID_PRODUCT = new ErrorCode(1008007005, "有失效的商品请重新提交"); ErrorCode VALID_PRODUCT_EMPTY = new ErrorCode(1008007005, "请提交购买的商品"); ErrorCode PARAM_ERROR = new ErrorCode(1008007006, "参数错误"); ErrorCode ORDER_EXPIRED = new ErrorCode(1008007007, "订单已过期,请刷新当前页面"); ErrorCode SELECT_ADDRESS = new ErrorCode(1008007008, "请选择收货地址"); ErrorCode ORDER_GEN_FAIL = new ErrorCode(1008007009, "订单生成失败"); ErrorCode ORDER_PAY_FINISH = new ErrorCode(1008007010, "该订单已支付"); ErrorCode PAY_YUE_NOT = new ErrorCode(1008007011, "余额不足"); ErrorCode ORDER_STATUS_ERROR = new ErrorCode(1008007012, "订单状态错误"); ErrorCode ORDER_STATUS_FINISH = new ErrorCode(1008007023, "订单已经完成,不要重复操作"); ErrorCode COMMENT_PRODUCT_NOT_EXISTS = new ErrorCode(1008007013, "评价产品不存在"); ErrorCode COMMENT_PRODUCT_IN_EXISTS = new ErrorCode(1008007014, "该产品已评价"); ErrorCode ORDER_NOT_DELETE = new ErrorCode(1008007018, "该订单无法删除"); ErrorCode ORDER_STATUS_NOT_EXPRESS_ = new ErrorCode(1008007015, "当前状态不能添加物流信息"); ErrorCode ORDER_REFUND_NOT = new ErrorCode(1008007017, "订单状态不能售后"); ErrorCode ORDER_NOT_REVOKE = new ErrorCode(1008007016, "订单不能撤销"); ErrorCode ORDER_NOT_CANCEL = new ErrorCode(1008007018, "订单不能取消"); ErrorCode ORDER_ADDRESS_REQUERED = new ErrorCode(1008007019, "请输入商家收货人信息"); ErrorCode ORDER_REFUNDING = new ErrorCode(1008007020, "正在申请退款中"); ErrorCode ORDER_REFUNDED = new ErrorCode(1008007021, "订单已退款"); ErrorCode ORDER_PRICE_ERROR = new ErrorCode(1008007022, "退款金额不正确"); // ========== 订单电子面单记录 ========== ErrorCode STORE_ORDER_ELECTRONICS_NOT_EXISTS = new ErrorCode(1008010000, "订单电子面单记录不存在"); ErrorCode STORE_ORDER_DESK_NOT = new ErrorCode(1008007023, "当前桌号不存在"); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-api/src/main/java/co/yixiang/yshop/module/order/enums/OrderLogEnum.java ================================================ package co.yixiang.yshop.module.order.enums; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.stream.Stream; /** * @author hupeng * 订单操作相关枚举 */ @Getter @AllArgsConstructor public enum OrderLogEnum { ORDER_TAKE_DESK("desk","扫码点餐"), ORDER_TAKE_OUT("takeout","外卖"), ORDER_TAKE_IN("takein","自取"), PINK_ORDER_FAIL_1("ORDER_EXIST","订单生成失败,你已经参加该团了,请先支付订单"), PINK_ORDER_FAIL_2("ORDER_EXIST","订单生成失败,你已经在该团内不能再参加了"), REFUND_ORDER_SUCCESS("refund_price_success","退款成功"), ORDER_EDIT("order_edit","订单改价"), REMOVE_ORDER("remove_order","删除订单"), EVAL_ORDER("order_eval","用户评价"), REFUND_ORDER_APPLY("apply_refund","用户申请退款"), TAKE_ORDER_DELIVERY("user_take_delivery","用户已收货"), PAY_ORDER_FAIL("PAY_DEFICIENCY","余额不足"), PAY_ORDER_SUCCESS("pay_success","用户付款成功"), CREATE_ORDER_SUCCESS("SUCCESS","订单创建成功"), CREATE_ORDER("yshop_create_order","订单生成"), NONE_ORDER("NONE","订单OK"), DELIVERY_GOODS("delivery_goods", "订单发货"), OFFLINE_PAY("offline_pay", "线下支付"), EXTEND_ORDER("EXTEND_ORDER","订单已生成"); private String value; private String desc; public static OrderLogEnum toType(String value) { return Stream.of(OrderLogEnum.values()) .filter(p -> p.value.equals(value)) .findAny() .orElse(null); } public static String getDesc(String value) { return toType(value) == null ? null : toType(value).desc; } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-api/src/main/java/co/yixiang/yshop/module/order/enums/OrderStatusEnum.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.order.enums; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.stream.Stream; /** * @author hupeng * 订单相关枚举 */ @Getter @AllArgsConstructor public enum OrderStatusEnum { STATUS__1(-1,"全部订单"), STATUS_0(0,"未支付"), STATUS_1(1,"待发货"), STATUS_2(2,"待收货"), STATUS_3(3,"待评价"), STATUS_4(4,"已完成"), STATUS_MINUS_1(-1,"退款中"), STATUS_MINUS_2(-2,"已退款"), STATUS_MINUS_3(-3,"退款"); private Integer value; private String desc; public static OrderStatusEnum toType(int value) { return Stream.of(OrderStatusEnum.values()) .filter(p -> p.value == value) .findAny() .orElse(null); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-api/src/main/java/co/yixiang/yshop/module/order/enums/PayTypeEnum.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.order.enums; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.stream.Stream; /** * @author hupeng * 支付相关枚举 */ @Getter @AllArgsConstructor public enum PayTypeEnum { CASH("cash","现金支付"), ALI("alipay","支付宝支付"), WEIXIN("weixin","微信支付"), YUE("yue","余额支付"), INTEGRAL("integral","积分兑换"); private String value; private String desc; public static PayTypeEnum toType(String value) { return Stream.of(PayTypeEnum.values()) .filter(p -> p.value.equals(value)) .findAny() .orElse(null); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-api/src/main/java/co/yixiang/yshop/module/order/enums/ShippingTempEnum.java ================================================ package co.yixiang.yshop.module.order.enums; import lombok.AllArgsConstructor; import lombok.Getter; /** * @author hupeng * 运费模板类型枚举 */ @Getter @AllArgsConstructor public enum ShippingTempEnum { TYPE_1(1,"按件数"), TYPE_2(2,"按重量"), TYPE_3(3,"按体积"); private Integer value; private String desc; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-api/src/main/java/co/yixiang/yshop/module/order/enums/UpdateOrderEnum.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.order.enums; import lombok.AllArgsConstructor; import lombok.Getter; /** * @author hupeng * 应用来源相关枚举 */ @Getter @AllArgsConstructor public enum UpdateOrderEnum { UPDATE_ORDER("updateOrder","修改订单"), ORDER_SEND("orderSend","订单发货"), RMARK("remark","备注"), SEND_INFO("sendInfo","配送信息"); private String value; private String desc; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/pom.xml ================================================ co.yixiang.boot yshop-module-mall ${revision} 4.0.0 yshop-module-order-biz jar ${project.artifactId} order 模块,主要实现商品购物车相关功能 co.yixiang.boot yshop-module-order-api ${revision} co.yixiang.boot yshop-module-pay-biz ${revision} co.yixiang.boot yshop-module-message-biz ${revision} co.yixiang.boot yshop-module-product-biz ${revision} co.yixiang.boot yshop-spring-boot-starter-web co.yixiang.boot yshop-spring-boot-starter-security co.yixiang.boot yshop-spring-boot-starter-mybatis co.yixiang.boot yshop-spring-boot-starter-test co.yixiang.boot yshop-spring-boot-starter-excel ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/admin/storeorder/StoreOrderController.java ================================================ package co.yixiang.yshop.module.order.controller.admin.storeorder; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.order.controller.admin.storeorder.vo.*; import co.yixiang.yshop.module.order.convert.storeorder.StoreOrderConvert; import co.yixiang.yshop.module.order.dal.dataobject.storeorder.StoreOrderDO; import co.yixiang.yshop.module.order.dal.dataobject.storeorderstatus.StoreOrderStatusDO; import co.yixiang.yshop.module.order.dal.redis.order.AsyncCountRedisDAO; import co.yixiang.yshop.module.order.service.storeorder.AsyncStoreOrderService; import co.yixiang.yshop.module.order.service.storeorder.StoreOrderService; import co.yixiang.yshop.module.order.service.storeorder.dto.OrderTimeDataDto; import co.yixiang.yshop.module.order.service.storeorderstatus.StoreOrderStatusService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.io.IOException; import java.util.Collection; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 订单") @RestController @RequestMapping("/order/store-order") @Validated public class StoreOrderController { @Resource private StoreOrderService storeOrderService; @Resource private StoreOrderStatusService storeOrderStatusService; @Resource private AsyncCountRedisDAO asyncCountRedisDAO; @Resource private AsyncStoreOrderService asyncStoreOrderService; @PostMapping("/create") @Operation(summary = "创建订单") @PreAuthorize("@ss.hasPermission('order:store-order:create')") public CommonResult createStoreOrder(@Valid @RequestBody StoreOrderCreateReqVO createReqVO) { return success(storeOrderService.createStoreOrder(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新订单") @PreAuthorize("@ss.hasPermission('order:store-order:update')") public CommonResult updateStoreOrder(@Valid @RequestBody StoreOrderUpdateReqVO updateReqVO) { storeOrderService.updateStoreOrder(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除订单") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('order:store-order:delete')") public CommonResult deleteStoreOrder(@RequestParam("id") Long id) { storeOrderService.deleteStoreOrder(id); return success(true); } @GetMapping("/pay") @Operation(summary = "订单线下支付") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('order:store-order:delete')") public CommonResult payStoreOrder(@RequestParam("id") Long id) { storeOrderService.payStoreOrder(id); return success(true); } @GetMapping("/take") @Operation(summary = "确认收货") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('order:store-order:delete')") public CommonResult takeStoreOrder(@RequestParam("id") Long id) { storeOrderService.takeStoreOrder(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得订单") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('order:store-order:query')") public CommonResult getStoreOrder(@RequestParam("id") Long id) { return success(storeOrderService.getStoreOrder(id)); } @GetMapping("/list") @Operation(summary = "获得订单列表") @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") @PreAuthorize("@ss.hasPermission('order:store-order:query')") public CommonResult> getStoreOrderList(@RequestParam("ids") Collection ids) { List list = storeOrderService.getStoreOrderList(ids); return success(StoreOrderConvert.INSTANCE.convertList(list)); } @GetMapping("/page") @Operation(summary = "获得订单分页") @PreAuthorize("@ss.hasPermission('order:store-order:query')") public CommonResult> getStoreOrderPage(@Valid StoreOrderPageReqVO pageVO) { return success(storeOrderService.getStoreOrderPage(pageVO)); } @GetMapping("/record-list") @Operation(summary = "获得订单记录列表") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('order:store-order:query')") public CommonResult> getStoreOrderRecordList(@RequestParam("id") Long id) { List list = storeOrderStatusService.list(new LambdaQueryWrapper() .eq(StoreOrderStatusDO::getOid,id)); return success(list); } @GetMapping("/export-excel") @Operation(summary = "导出订单 Excel") @PreAuthorize("@ss.hasPermission('order:store-order:export')") public void exportStoreOrderExcel(@Valid StoreOrderExportReqVO exportReqVO, HttpServletResponse response) throws IOException { List list = storeOrderService.getStoreOrderList(exportReqVO); // 导出 Excel List datas = StoreOrderConvert.INSTANCE.convertList02(list); ExcelUtils.write(response, "订单.xls", "数据", StoreOrderExcelVO.class, datas); } @GetMapping("/count") @Operation(summary = "获得订单统计") public CommonResult getStoreOrderCount() { asyncStoreOrderService.getOrderTimeData(); return success(asyncCountRedisDAO.get()); } @Operation(summary = "退款") @PostMapping(value = "/refund") @PreAuthorize("@ss.hasPermission('order:store-order:update')") public CommonResult refund(@Validated @RequestBody StoreOrderRefundVO updateReqVO) { storeOrderService.orderRefund(updateReqVO.getId(),updateReqVO.getPayPrice(), 0, null); return success(true); } @Operation(summary = "订单通知") @GetMapping(value = "/notice") public CommonResult refund() { return success(storeOrderService.orderNotice()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/admin/storeorder/vo/ShoperOrderTimeDataVo.java ================================================ package co.yixiang.yshop.module.order.controller.admin.storeorder.vo; import lombok.Data; import java.io.Serializable; /** * @ClassName OrderTimeDataDTO * @Author hupeng <610796224@qq.com> * @Date 2023/7/26 **/ @Data public class ShoperOrderTimeDataVo implements Serializable { /**今日成交额*/ private Double todayPrice; /**今日订单数*/ private Long todayCount; /**昨日成交额*/ private Double proPrice; /**昨日订单数*/ private Long proCount; /**本月成交额*/ private Double monthPrice; /**本月订单数*/ private Long monthCount; /**上周订单数*/ private Long lastWeekCount; /**上周成交额*/ private Double lastWeekPrice; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/admin/storeorder/vo/StoreOrderBaseVO.java ================================================ package co.yixiang.yshop.module.order.controller.admin.storeorder.vo; import co.yixiang.yshop.framework.desensitize.core.slider.annotation.MobileDesensitize; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.springframework.format.annotation.DateTimeFormat; import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; /** * 订单 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class StoreOrderBaseVO { @Schema(description = "订单号", required = true, example = "20527") @NotNull(message = "订单号不能为空") private String orderId; @Schema(description = "额外订单号", example = "12452") private String extendOrderId; @Schema(description = "用户id", required = true, example = "8323") @NotNull(message = "用户id不能为空") private Long uid; @Schema(description = "用户姓名", required = true, example = "张三") //@NotNull(message = "用户姓名不能为空") private String realName; //@MobileDesensitize @Schema(description = "用户电话", required = true) //@NotNull(message = "用户电话不能为空") private String userPhone; @Schema(description = "详细地址", required = true) // @NotNull(message = "详细地址不能为空") private String userAddress; @Schema(description = "购物车id", required = true, example = "23301") //@NotNull(message = "购物车id不能为空") private String cartId; @Schema(description = "运费金额", required = true, example = "637") //@NotNull(message = "运费金额不能为空") private BigDecimal freightPrice; @Schema(description = "订单商品总数", required = true) //@NotNull(message = "订单商品总数不能为空") private Integer totalNum; @Schema(description = "订单总价", required = true, example = "31659") //@NotNull(message = "订单总价不能为空") private BigDecimal totalPrice; @Schema(description = "邮费", required = true) // @NotNull(message = "邮费不能为空") private BigDecimal totalPostage; @Schema(description = "实际支付金额", required = true, example = "19682") // @NotNull(message = "实际支付金额不能为空") private BigDecimal payPrice; @Schema(description = "支付邮费", required = true) //@NotNull(message = "支付邮费不能为空") private BigDecimal payPostage; @Schema(description = "抵扣金额", required = true, example = "16463") //@NotNull(message = "抵扣金额不能为空") private BigDecimal deductionPrice; @Schema(description = "优惠券id", required = true, example = "3299") //@NotNull(message = "优惠券id不能为空") private Integer couponId; @Schema(description = "优惠券金额", required = true, example = "22157") //@NotNull(message = "优惠券金额不能为空") private BigDecimal couponPrice; @Schema(description = "支付状态", required = true, example = "11728") //@NotNull(message = "支付状态不能为空") private Integer paid; @Schema(description = "支付时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime payTime; @Schema(description = "支付方式", required = true, example = "2") //@NotNull(message = "支付方式不能为空") private String payType; @Schema(description = "订单状态(-1 : 申请退款 -2 : 退货成功 0:待发货;1:待收货;2:已收货;3:已完成;-1:已退款)", required = true, example = "1") //@NotNull(message = "订单状态(-1 : 申请退款 -2 : 退货成功 0:待发货;1:待收货;2:已收货;3:已完成;-1:已退款)不能为空") private Integer status; @Schema(description = "0 未退款 1 申请中 2 已退款", required = true, example = "2") //@NotNull(message = "0 未退款 1 申请中 2 已退款不能为空") private Integer refundStatus; @Schema(description = "退款图片") private String refundReasonWapImg; @Schema(description = "退款用户说明") private String refundReasonWapExplain; @Schema(description = "退款时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime refundReasonTime; @Schema(description = "前台退款原因") private String refundReasonWap; @Schema(description = "不退款的理由", example = "不喜欢") private String refundReason; @Schema(description = "退款金额", required = true, example = "7547") //@NotNull(message = "退款金额不能为空") private BigDecimal refundPrice; @Schema(description = "快递公司编号") private String deliverySn; @Schema(description = "快递名称/送货人姓名", example = "张三") private String deliveryName; @Schema(description = "发货类型", example = "1") private String deliveryType; @Schema(description = "快递单号/手机号", example = "24798") private String deliveryId; @Schema(description = "消费赚取积分", required = true) //@NotNull(message = "消费赚取积分不能为空") private BigDecimal gainIntegral; @Schema(description = "使用积分", required = true) //@NotNull(message = "使用积分不能为空") private BigDecimal useIntegral; @Schema(description = "实际支付积分", required = true) //@NotNull(message = "实际支付积分不能为空") private BigDecimal payIntegral; @Schema(description = "给用户退了多少积分") private BigDecimal backIntegral; @Schema(description = "备注", required = true) //@NotNull(message = "备注不能为空") private String mark; @Schema(description = "唯一id(md5加密)类似id", required = true) //@NotNull(message = "唯一id(md5加密)类似id不能为空") private String unique; @Schema(description = "管理员备注", example = "随便") private String remark; @Schema(description = "商户ID", required = true, example = "8499") //@NotNull(message = "商户ID不能为空") private Integer merId; @Schema(description = "拼团产品id0一般产品", example = "3865") private Long combinationId; @Schema(description = "拼团id 0没有拼团", required = true, example = "8463") //@NotNull(message = "拼团id 0没有拼团不能为空") private Long pinkId; @Schema(description = "成本价", required = true) //@NotNull(message = "成本价不能为空") private BigDecimal cost; @Schema(description = "秒杀产品ID", required = true, example = "21525") //@NotNull(message = "秒杀产品ID不能为空") private Long seckillId; @Schema(description = "砍价id", example = "5132") private Integer bargainId; @Schema(description = "核销码", required = true) //@NotNull(message = "核销码不能为空") private String verifyCode; @Schema(description = "门店id", required = true, example = "12064") //@NotNull(message = "门店id不能为空") private Integer storeId; @Schema(description = "配送方式 1=快递 ,2=门店自提", required = true, example = "2") //@NotNull(message = "配送方式 1=快递 ,2=门店自提不能为空") private Integer shippingType; @Schema(description = "支付渠道(0微信公众号1微信小程序)") private Integer isChannel; @Schema(description = "系统删除") private Integer isSystemDel; @Schema(description = "订单类型") private String orderType; /** * 取餐👌 */ private Long numberId; /** * 门店ID */ private Long shopId; /** * 门店名称 */ private String shopName; private LocalDateTime getTime; /** * 桌面ID */ private Long deskId; /** * 桌号 */ private String deskNumber; /** * 就餐人数 */ private Integer deskPeople; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/admin/storeorder/vo/StoreOrderCreateReqVO.java ================================================ package co.yixiang.yshop.module.order.controller.admin.storeorder.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 订单创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StoreOrderCreateReqVO extends StoreOrderBaseVO { } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/admin/storeorder/vo/StoreOrderExcelVO.java ================================================ package co.yixiang.yshop.module.order.controller.admin.storeorder.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.LocalDateTime; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.alibaba.excel.annotation.ExcelProperty; /** * 订单 Excel VO * * @author yshop */ @Data public class StoreOrderExcelVO { @ExcelProperty("订单ID") private Long id; @ExcelProperty("订单号") private String orderId; @ExcelProperty("额外订单号") private String extendOrderId; @ExcelProperty("用户id") private Long uid; @ExcelProperty("用户姓名") private String realName; @ExcelProperty("用户电话") private String userPhone; @ExcelProperty("详细地址") private String userAddress; @ExcelProperty("购物车id") private String cartId; @ExcelProperty("运费金额") private BigDecimal freightPrice; @ExcelProperty("订单商品总数") private Integer totalNum; @ExcelProperty("订单总价") private BigDecimal totalPrice; @ExcelProperty("邮费") private BigDecimal totalPostage; @ExcelProperty("实际支付金额") private BigDecimal payPrice; @ExcelProperty("支付邮费") private BigDecimal payPostage; @ExcelProperty("抵扣金额") private BigDecimal deductionPrice; @ExcelProperty("优惠券id") private Integer couponId; @ExcelProperty("优惠券金额") private BigDecimal couponPrice; @ExcelProperty("支付状态") private Integer paid; @ExcelProperty("支付时间") private LocalDateTime payTime; @ExcelProperty("支付方式") private String payType; @ExcelProperty("订单状态(-1 : 申请退款 -2 : 退货成功 0:待发货;1:待收货;2:已收货;3:已完成;-1:已退款)") private Integer status; @ExcelProperty("0 未退款 1 申请中 2 已退款") private Integer refundStatus; @ExcelProperty("退款图片") private String refundReasonWapImg; @ExcelProperty("退款用户说明") private String refundReasonWapExplain; @ExcelProperty("退款时间") private LocalDateTime refundReasonTime; @ExcelProperty("前台退款原因") private String refundReasonWap; @ExcelProperty("不退款的理由") private String refundReason; @ExcelProperty("退款金额") private BigDecimal refundPrice; @ExcelProperty("快递公司编号") private String deliverySn; @ExcelProperty("快递名称/送货人姓名") private String deliveryName; @ExcelProperty("发货类型") private String deliveryType; @ExcelProperty("快递单号/手机号") private String deliveryId; @ExcelProperty("消费赚取积分") private BigDecimal gainIntegral; @ExcelProperty("使用积分") private BigDecimal useIntegral; @ExcelProperty("实际支付积分") private BigDecimal payIntegral; @ExcelProperty("给用户退了多少积分") private BigDecimal backIntegral; @ExcelProperty("备注") private String mark; @ExcelProperty("唯一id(md5加密)类似id") private String unique; @ExcelProperty("管理员备注") private String remark; @ExcelProperty("商户ID") private Integer merId; @ExcelProperty("拼团产品id0一般产品") private Long combinationId; @ExcelProperty("拼团id 0没有拼团") private Long pinkId; @ExcelProperty("成本价") private BigDecimal cost; @ExcelProperty("秒杀产品ID") private Long seckillId; @ExcelProperty("砍价id") private Integer bargainId; @ExcelProperty("核销码") private String verifyCode; @ExcelProperty("门店id") private Integer storeId; @ExcelProperty("配送方式 1=快递 ,2=门店自提") private Integer shippingType; @ExcelProperty("支付渠道(0微信公众号1微信小程序)") private Integer isChannel; @ExcelProperty("系统删除") private Integer isSystemDel; @ExcelProperty("添加时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/admin/storeorder/vo/StoreOrderExportReqVO.java ================================================ package co.yixiang.yshop.module.order.controller.admin.storeorder.vo; import lombok.*; import java.math.BigDecimal; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import java.time.LocalDateTime; import org.springframework.format.annotation.DateTimeFormat; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 订单 Excel 导出 Request VO,参数和 StoreOrderPageReqVO 是一致的") @Data public class StoreOrderExportReqVO { @Schema(description = "订单号", example = "20527") private String orderId; @Schema(description = "额外订单号", example = "12452") private String extendOrderId; @Schema(description = "用户id", example = "8323") private Long uid; @Schema(description = "用户姓名", example = "张三") private String realName; @Schema(description = "用户电话") private String userPhone; @Schema(description = "详细地址") private String userAddress; @Schema(description = "购物车id", example = "23301") private String cartId; @Schema(description = "运费金额", example = "637") private BigDecimal freightPrice; @Schema(description = "订单商品总数") private Integer totalNum; @Schema(description = "订单总价", example = "31659") private BigDecimal totalPrice; @Schema(description = "邮费") private BigDecimal totalPostage; @Schema(description = "实际支付金额", example = "19682") private BigDecimal payPrice; @Schema(description = "支付邮费") private BigDecimal payPostage; @Schema(description = "抵扣金额", example = "16463") private BigDecimal deductionPrice; @Schema(description = "优惠券id", example = "3299") private Integer couponId; @Schema(description = "优惠券金额", example = "22157") private BigDecimal couponPrice; @Schema(description = "支付状态", example = "11728") private Integer paid; @Schema(description = "支付时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] payTime; @Schema(description = "支付方式", example = "2") private String payType; @Schema(description = "订单状态(-1 : 申请退款 -2 : 退货成功 0:待发货;1:待收货;2:已收货;3:已完成;-1:已退款)", example = "1") private Integer status; @Schema(description = "0 未退款 1 申请中 2 已退款", example = "2") private Integer refundStatus; @Schema(description = "退款图片") private String refundReasonWapImg; @Schema(description = "退款用户说明") private String refundReasonWapExplain; @Schema(description = "退款时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] refundReasonTime; @Schema(description = "前台退款原因") private String refundReasonWap; @Schema(description = "不退款的理由", example = "不喜欢") private String refundReason; @Schema(description = "退款金额", example = "7547") private BigDecimal refundPrice; @Schema(description = "快递公司编号") private String deliverySn; @Schema(description = "快递名称/送货人姓名", example = "张三") private String deliveryName; @Schema(description = "发货类型", example = "1") private String deliveryType; @Schema(description = "快递单号/手机号", example = "24798") private String deliveryId; @Schema(description = "消费赚取积分") private BigDecimal gainIntegral; @Schema(description = "使用积分") private BigDecimal useIntegral; @Schema(description = "实际支付积分") private BigDecimal payIntegral; @Schema(description = "给用户退了多少积分") private BigDecimal backIntegral; @Schema(description = "备注") private String mark; @Schema(description = "唯一id(md5加密)类似id") private String unique; @Schema(description = "管理员备注", example = "随便") private String remark; @Schema(description = "商户ID", example = "8499") private Integer merId; @Schema(description = "拼团产品id0一般产品", example = "3865") private Long combinationId; @Schema(description = "拼团id 0没有拼团", example = "8463") private Long pinkId; @Schema(description = "成本价") private BigDecimal cost; @Schema(description = "秒杀产品ID", example = "21525") private Long seckillId; @Schema(description = "砍价id", example = "5132") private Integer bargainId; @Schema(description = "核销码") private String verifyCode; @Schema(description = "门店id", example = "12064") private Integer storeId; @Schema(description = "配送方式 1=快递 ,2=门店自提", example = "2") private Integer shippingType; @Schema(description = "支付渠道(0微信公众号1微信小程序)") private Integer isChannel; @Schema(description = "系统删除") private Integer isSystemDel; @Schema(description = "添加时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/admin/storeorder/vo/StoreOrderPageReqVO.java ================================================ package co.yixiang.yshop.module.order.controller.admin.storeorder.vo; import lombok.*; import java.math.BigDecimal; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 订单分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StoreOrderPageReqVO extends PageParam { @Schema(description = "取餐号", example = "20527") private String numberId; @Schema(description = "订单号", example = "20527") private String orderId; @Schema(description = "用户姓名", example = "张三") private String realName; @Schema(description = "用户电话") private String userPhone; @Schema(description = "添加时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; @Schema(description = "订单状态搜索值") private Integer orderStatus; @Schema(description = "支付状态搜索值") private String payStatus; @Schema(description = "工作台订单还是普通订单") private String type; @Schema(description = "店铺ID", example = "20527") private String ShopId; @Schema(description = "订单类型", example = "desk") private String orderType; @Schema(description = "桌面id") private Integer deskId; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/admin/storeorder/vo/StoreOrderRefundVO.java ================================================ package co.yixiang.yshop.module.order.controller.admin.storeorder.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; @Schema(description = "管理后台 - 订单退款 Request VO") @Data @ToString(callSuper = true) public class StoreOrderRefundVO { @Schema(description = "订单ID", required = true, example = "31716") @NotNull(message = "订单ID不能为空") private Long id; @Schema(description = "退款金额", required = true, example = "31716") @NotNull(message = "退款金额不能为空") private BigDecimal payPrice; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/admin/storeorder/vo/StoreOrderRespVO.java ================================================ package co.yixiang.yshop.module.order.controller.admin.storeorder.vo; import co.yixiang.yshop.module.member.controller.admin.user.vo.UserRespVO; import co.yixiang.yshop.module.order.dal.dataobject.storeordercartinfo.StoreOrderCartInfoDO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; import java.util.List; @Schema(description = "管理后台 - 订单 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StoreOrderRespVO extends StoreOrderBaseVO { @Schema(description = "订单ID", required = true, example = "31716") private Long id; @Schema(description = "添加时间", required = true) private LocalDateTime createTime; @Schema(description = "用户信息", required = true) private UserRespVO userRespVO; @Schema(description = "商品信息", required = true) private List storeOrderCartInfoDOList; @Schema(description = "订单状态", required = true) private String StatusStr; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/admin/storeorder/vo/StoreOrderUpdateReqVO.java ================================================ package co.yixiang.yshop.module.order.controller.admin.storeorder.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 订单更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StoreOrderUpdateReqVO extends StoreOrderBaseVO { @Schema(description = "订单ID", required = true, example = "31716") @NotNull(message = "订单ID不能为空") private Long id; /** * updateOrder: '修改订单', * orderSend: '订单发货', * remark: '备注', * sendInfo: '配送信息', */ @Schema(description = "更新类型", required = true, example = "31716") private String updateType; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/app/order/AppOrderController.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co * 注意: * 本软件为www.yixiang.co开发研制,未经购买不得使用 * 购买后可获得全部源代码(禁止转卖、分享、上传到码云、github等开源平台) * 一经发现盗用、分享等行为,将追究法律责任,后果自负 */ package co.yixiang.yshop.module.order.controller.app.order; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.enums.OrderInfoEnum; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.security.core.annotations.PreAuthenticated; import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserOrderCountVo; import co.yixiang.yshop.module.order.controller.app.order.param.*; import co.yixiang.yshop.module.order.controller.app.order.vo.AppStoreOrderQueryVo; import co.yixiang.yshop.module.order.dal.dataobject.storeorder.StoreOrderDO; import co.yixiang.yshop.module.order.dal.redis.order.AsyncOrderRedisDAO; import co.yixiang.yshop.module.order.service.storeorder.AppStoreOrderService; import co.yixiang.yshop.module.pay.http.HttpRequestNoticeNewParams; import co.yixiang.yshop.module.store.controller.app.storeshop.vo.AppStoreShopVO; import co.yixiang.yshop.module.store.convert.storeshop.StoreShopConvert; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; import co.yixiang.yshop.module.store.service.storeshop.AppStoreShopService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.egzosn.pay.spring.boot.core.PayServiceManager; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; 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.time.LocalDateTime; import java.util.List; import java.util.Map; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; import static co.yixiang.yshop.module.order.enums.ErrorCodeConstants.PARAM_ERROR; import static co.yixiang.yshop.module.order.enums.ErrorCodeConstants.STORE_ORDER_NOT_EXISTS; /** *

* 订单控制器 *

* * @author hupeng * @since 2023-6-23 */ @Slf4j @RestController @RequiredArgsConstructor(onConstructor = @__(@Autowired)) @Tag(name = "用户 APP - 订单模块") @RequestMapping("/order") public class AppOrderController { private final AppStoreOrderService appStoreOrderService; private final AsyncOrderRedisDAO asyncOrderRedisDAO; private final PayServiceManager manager; private final AppStoreShopService appStoreShopService; ; /** * 订单创建 */ @PreAuthenticated @PostMapping("/create") @Operation(summary = "订单创建") public CommonResult> create(@RequestBody @Valid AppOrderParam param) { Long uid = getLoginUserId(); return success(appStoreOrderService.createOrder(uid, param)); } /** * 订单支付 */ @PreAuthenticated @PostMapping(value = "/pay") @Operation(summary = "订单支付") public CommonResult> pay(@RequestBody @Valid AppPayParam param) { Long uid = getLoginUserId(); return success(appStoreOrderService.pay(uid,param)); } /** * 支付回调地址 * * @param request 请求 * @param detailsId 列表id * @return 支付是否成功 */ @RequestMapping(value = "/notify/payBack{detailsId}.json") public String payBack(HttpServletRequest request, @PathVariable String detailsId) { return manager.payBack(detailsId, new HttpRequestNoticeNewParams(request)); } /** * 订单列表 */ @PreAuthenticated @GetMapping("/list") @Operation(summary = "订单列表") @Parameters({ @Parameter(name = "type", description = "商品状态,-1全部 默认为0未支付 1待发货 2待收货 3待评价 4已完成 5退款中 6已退款 7退款", required = true, example = "1"), @Parameter(name = "page", description = "页码,默认为1", required = true, example = "1"), @Parameter(name = "limit", description = "页大小,默认为10", required = true, example = "10 ") }) public CommonResult> orderList(@RequestParam(value = "type", defaultValue = "0") int type, @RequestParam(value = "page", defaultValue = "1") int page, @RequestParam(value = "limit", defaultValue = "10") int limit) { Long uid = getLoginUserId(); return success(appStoreOrderService.orderList(uid, type, page, limit)); } /** * 订单详情 */ @PreAuthenticated @GetMapping("/detail/{key}") @Operation(summary = "订单详情") @Parameter(name = "key", description = "唯一的uni值或者订单号", required = true, example = "10 ") public CommonResult detail(@PathVariable String key) { Long uid = getLoginUserId(); if (StrUtil.isEmpty(key)) { throw exception(PARAM_ERROR); } AppStoreOrderQueryVo storeOrder = appStoreOrderService.getOrderInfo(key, uid); if (ObjectUtil.isNull(storeOrder)) { throw exception(STORE_ORDER_NOT_EXISTS); } return success(appStoreOrderService.handleOrder(storeOrder)); } /** * 订单收货 */ @PreAuthenticated @PostMapping("/take") @Operation(summary = "订单收货") public CommonResult orderTake(@RequestBody @Validated AppDoOrderParam param) { Long uid = getLoginUserId(); appStoreOrderService.takeOrder(param.getUni(), uid); return success(true); } /** * 订单退款审核 */ @PostMapping("/refund") @Operation(summary = "订单退款审核") public CommonResult refundVerify(@RequestBody AppRefundParam param) { Long uid = getLoginUserId(); appStoreOrderService.orderApplyRefund(param.getRefundReasonWapExplain(), param.getRefundReasonWapImg(), param.getText(), param.getUni(), uid); return success(true); } /** * 订单删除 */ @PreAuthenticated @PostMapping("/del") @Operation(summary = "订单删除") public CommonResult orderDel(@Validated @RequestBody AppDoOrderParam param) { Long uid = getLoginUserId(); appStoreOrderService.removeOrder(param.getUni(), uid); return success(true); } /** * 订单取消 未支付的订单回退积分,回退优惠券,回退库存 */ @PreAuthenticated @PostMapping("/cancel") @Operation(summary = "订单取消") public CommonResult cancelOrder(@Validated @RequestBody AppHandleOrderParam param) { Long uid = getLoginUserId(); appStoreOrderService.cancelOrder(param.getId(), uid); return success(true); } /** * 个人中心订单统计 */ @PreAuthenticated @PostMapping("/user_count") @Operation(summary = "个人中心订单统计") public CommonResult countOrder() { Long uid = getLoginUserId(); AppUserOrderCountVo appUserOrderCountVo = asyncOrderRedisDAO.get(uid); return success(appUserOrderCountVo); } @PreAuthenticated @GetMapping("/getShop") @Operation(summary = "获取店铺") public CommonResult getShop(@RequestParam("shopId") Integer shopId) { StoreShopDO storeShopDO = appStoreShopService.getById(shopId); AppStoreShopVO appStoreShopVO = StoreShopConvert.INSTANCE.convert02(storeShopDO); appStoreShopVO.setIsEmpty(true); return success(appStoreShopVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/app/order/param/AppComputeOrderParam.java ================================================ package co.yixiang.yshop.module.order.controller.app.order.param; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; /** * @ClassName ComputeOrderParam * @Author hupeng <610796224@qq.com> * @Date 2023/6/19 **/ @Getter @Setter @ToString @Builder @NoArgsConstructor @AllArgsConstructor public class AppComputeOrderParam { @Schema(description = "地址ID", required = true) private String addressId; @Schema(description = "优惠券ID", required = true) private String couponId; @Schema(description = "支付方式", required = true) private String payType; @Schema(description = "使用积分 1-表示使用", required = true) private String useIntegral; @Schema(description = "配送方式 1=快递 ,2=门店自提", required = true) private String shippingType; @Schema(description = "拼团ID", required = true) private String pinkId; @Schema(description = "拼团产品ID", required = true) private String combinationId; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/app/order/param/AppConfirmOrderParam.java ================================================ package co.yixiang.yshop.module.order.controller.app.order.param; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; import jakarta.validation.constraints.NotBlank; /** * @ClassName 确认订单ConfirmOrderDTO * @Author hupeng <610796224@qq.com> * @Date 2023/6/18 **/ @Getter @Setter public class AppConfirmOrderParam { @NotBlank(message = "请提交购买的商品") @Schema(description = "购物车ID", required = true) private String cartId; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/app/order/param/AppDoOrderParam.java ================================================ package co.yixiang.yshop.module.order.controller.app.order.param; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; import jakarta.validation.constraints.NotBlank; /** * @ClassName HandleOrderParam * @Author hupeng <610796224@qq.com> * @Date 2023/6/21 **/ @Getter @Setter public class AppDoOrderParam { @NotBlank(message = "参数有误") @Schema(description = "订单ID", required = true) private String uni; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/app/order/param/AppExpressParam.java ================================================ package co.yixiang.yshop.module.order.controller.app.order.param; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; /** * @ClassName ExpressParam * @Author hupeng <610796224@qq.com> * @Date 2023/7/26 **/ @Data public class AppExpressParam implements Serializable { @Schema(description = "订单编号", required = true) private String orderCode; @Schema(description = "快递公司编码", required = true) private String shipperCode; @Schema(description = "物流单号", required = true) private String logisticCode; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/app/order/param/AppHandleOrderParam.java ================================================ package co.yixiang.yshop.module.order.controller.app.order.param; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; import jakarta.validation.constraints.NotBlank; /** * @ClassName HandleOrderParam * @Author hupeng <610796224@qq.com> * @Date 2023/6/23 **/ @Getter @Setter public class AppHandleOrderParam { @NotBlank(message = "参数有误") @Schema(description = "订单ID", required = true) private String id; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/app/order/param/AppOrderParam.java ================================================ package co.yixiang.yshop.module.order.controller.app.order.param; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.Size; import java.io.Serializable; import java.util.List; /** * @ClassName OrderParam * @Author hupeng <610796224@qq.com> * @Date 2023/8/22 **/ @Data public class AppOrderParam implements Serializable { @Schema(description = "地址ID", required = true) private String addressId; @Schema(description = "门店ID", required = true) private String shopId; @Schema(description = "手机号", required = true) private String mobile; @Schema(description = "优惠券ID", required = true) private String couponId; @Schema(description = "购买类型:takein=自取,takeout=外卖,desk=扫码点餐", required = true) private String orderType; @Size(max = 200,message = "长度超过了限制") @Schema(description = "备注", required = true) private String remark; @Schema(description = "取餐时间", required = true) private Integer gettime; @Schema(description = "秒杀产品ID", required = true) private List productId; @Schema(description = "规格", required = true) private List spec; @Schema(description = "数量", required = true) private List number; @Schema(description = "支付类型", required = true) private String payType; @Schema(description = "桌面ID", required = true) private Long deskId; @Schema(description = "桌号", required = true) private String deskNumber; @Schema(description = "就餐人数", required = true) private Integer deskPeople; @Schema(description = "订单号", required = true) private String orderId; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/app/order/param/AppPayParam.java ================================================ package co.yixiang.yshop.module.order.controller.app.order.param; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotBlank; import java.io.Serializable; /** * @ClassName PayDto * @Author hupeng <610796224@qq.com> * @Date 2013/6/20 **/ @Data public class AppPayParam implements Serializable { @Schema(description = "来源", required = true) private String from; @NotBlank(message = "选择支付类型 PayTypeEnum类型(alipay weixin yue)") @Schema(description = "支付类型", required = true) private String paytype; @NotBlank(message = "参数错误") @Schema(description = "订单ID", required = true) private String uni; // @Schema(description = "服务商id 当不是余额支付必填1-阿里支付 3-微信支付 这里当编号与数据库id对应", required = true) // private String detailsId; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/app/order/param/AppProductReplyParam.java ================================================ package co.yixiang.yshop.module.order.controller.app.order.param; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; /** * @ClassName ProductReplyParam * @Author hupeng <610796224@qq.com> * @Date 2023/6/20 **/ @Getter @Setter public class AppProductReplyParam { @NotBlank(message = "评论不能为空") @Size(min = 1, max = 200,message = "长度超过了限制") @Schema(description = "商品评论内容", required = true) private String comment; @Schema(description = "商品评论图片地址", required = true) private String pics; @NotBlank(message = "请为商品评分") @Schema(description = "商品评分", required = true) private String productScore; @NotBlank(message = "请为商品评分") @Schema(description = "服务评分", required = true) private String serviceScore; @NotBlank(message = "参数有误") @Schema(description = "参数有误", required = true) private String unique; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/app/order/param/AppRefundParam.java ================================================ package co.yixiang.yshop.module.order.controller.app.order.param; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotBlank; import java.io.Serializable; /** * @ClassName RefundParam * @Author hupeng <610796224@qq.com> * @Date 2023/6/23 **/ @Data public class AppRefundParam implements Serializable { @Schema(description = "退款备注", required = true) private String refundReasonWapExplain; @Schema(description = "退款图片", required = true) private String refundReasonWapImg; @NotBlank(message = "请填写退款原因") @Schema(description = "退款原因", required = true) private String text; @NotBlank(message = "参数错误") @Schema(description = "订单号", required = true) private String uni; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/app/order/vo/AppComputeVo.java ================================================ package co.yixiang.yshop.module.order.controller.app.order.vo; import co.yixiang.yshop.framework.common.serializer.BigDecimalSerializer; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.math.BigDecimal; /** * @ClassName ComputeVo * @Author hupeng <610796224@qq.com> * @Date 2019/10/27 **/ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class AppComputeVo implements Serializable { @JsonSerialize(using = BigDecimalSerializer.class) private BigDecimal couponPrice; @JsonSerialize(using = BigDecimalSerializer.class) private BigDecimal deductionPrice; @JsonSerialize(using = BigDecimalSerializer.class) private BigDecimal payPostage; @JsonSerialize(using = BigDecimalSerializer.class) private BigDecimal payPrice; @JsonSerialize(using = BigDecimalSerializer.class) private BigDecimal totalPrice; private Double usedIntegral; //使用了多少积分 @JsonSerialize(using = BigDecimalSerializer.class) private BigDecimal payIntegral; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/app/order/vo/AppConfirmOrderVo.java ================================================ package co.yixiang.yshop.module.order.controller.app.order.vo; import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserQueryVo; import co.yixiang.yshop.module.member.dal.dataobject.useraddress.UserAddressDO; import co.yixiang.yshop.module.order.service.storeorder.dto.PriceGroupDto; import co.yixiang.yshop.module.product.controller.app.cart.vo.AppStoreCartQueryVo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.List; /** * @ClassName ConfirmOrderVo * @Author hupeng <610796224@qq.com> * @Date 2023/6/18 **/ @Data @NoArgsConstructor @AllArgsConstructor @Builder @Schema(description = "用户 APP - 订单确认参数参数") public class AppConfirmOrderVo implements Serializable { //地址信息 private UserAddressDO addressInfo; //砍价id private Integer bargainId; private Integer combinationId; //优惠券减 private Boolean deduction; private Boolean enableIntegral; private Double enableIntegralNum; //积分抵扣 private Integer integralRatio; private String orderKey; private PriceGroupDto priceGroup; private Integer seckillId; //店铺自提 private Integer storeSelfMention; // private StoreCouponUserVo usableCoupon; private AppUserQueryVo userInfo; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/controller/app/order/vo/AppStoreOrderQueryVo.java ================================================ package co.yixiang.yshop.module.order.controller.app.order.vo; import co.yixiang.yshop.module.order.dal.dataobject.storeordercartinfo.StoreOrderCartInfoDO; import co.yixiang.yshop.module.order.service.storeorder.dto.StatusDto; import co.yixiang.yshop.module.product.controller.app.cart.vo.AppStoreCartQueryVo; import co.yixiang.yshop.module.store.controller.app.storeshop.vo.AppStoreShopVO; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Date; import java.util.List; /** *

* 订单表 查询结果对象 *

* * @author hupeng * @date 2023-6-19 */ @Data @Schema(description = "用户 APP - 订单表查询参数") public class AppStoreOrderQueryVo implements Serializable { private static final long serialVersionUID = 1L; @Schema(description = "订单ID", required = true) private Long id; @Schema(description = "订单号", required = true) private String orderId; private String extendOrderId; @Schema(description = "用户id", required = true) private Long uid; @Schema(description = "用户姓名", required = true) private String realName; @Schema(description = "用户电话", required = true) private String userPhone; @Schema(description = "详细地址", required = true) private String userAddress; @Schema(description = "购物车id", required = true) private String cartId; @Schema(description = "购物车信息", required = true) private List cartInfo; @Schema(description = "订单信息合集", required = true) private StatusDto statusDto; @Schema(description = "运费金额", required = true) private BigDecimal freightPrice; @Schema(description = "订单商品总数", required = true) private Integer totalNum; @Schema(description = "订单总价", required = true) private BigDecimal totalPrice; @Schema(description = "邮费", required = true) private BigDecimal totalPostage; @Schema(description = "实际支付金额", required = true) private BigDecimal payPrice; @Schema(description = "实际支付积分", required = true) private BigDecimal payIntegral; @Schema(description = "支付邮费", required = true) private BigDecimal payPostage; @Schema(description = "抵扣金额", required = true) private BigDecimal deductionPrice; @Schema(description = "优惠券id", required = true) private Integer couponId; @Schema(description = "优惠券金额", required = true) private BigDecimal couponPrice; @Schema(description = "支付状态", required = true) private Integer paid; @Schema(description = "支付时间", required = true) private LocalDateTime payTime; @Schema(description = "支付方式", required = true) private String payType; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8") @Schema(description = "创建时间", required = true) private LocalDateTime createTime; @Schema(description = "订单状态(-1 : 申请退款 -2 : 退货成功 0:待发货;1:待收货;2:已收货;3:待评价;-1:已退款", required = true) private Integer status; @Schema(description = "0 未退款 1 申请中 2 已退款", required = true) private Integer refundStatus; @Schema(description = "退款图片", required = true) private String refundReasonWapImg; @Schema(description = "退款用户说明", required = true) private String refundReasonWapExplain; @Schema(description = "退款时间", required = true) private LocalDateTime refundReasonTime; @Schema(description = "前台退款原因", required = true) private String refundReasonWap; @Schema(description = "不退款的理由", required = true) private String refundReason; @Schema(description = "退款金额", required = true) private BigDecimal refundPrice; @Schema(description = "快递名称/送货人姓名", required = true) private String deliveryName; @Schema(description = "快递公司编号", required = true) private String deliverySn; @Schema(description = "发货类型", required = true) private String deliveryType; @Schema(description = "快递单号/手机号", required = true) private String deliveryId; @Schema(description = "发货时间", required = true) private LocalDateTime deliveryTime; @Schema(description = "消费赚取积分", required = true) private BigDecimal gainIntegral; @Schema(description = "使用积分", required = true) private BigDecimal useIntegral; @Schema(description = "给用户退了多少积分", required = true) private BigDecimal backIntegral; @Schema(description = "备注", required = true) private String mark; @Schema(description = "确认订单返回的key", required = true) private String unique; @Schema(description = "管理员备注", required = true) private String remark; @Schema(description = "拼团产品id0一般产品", required = true) private Long combinationId; @Schema(description = "拼团id 0没有拼团", required = true) private Long pinkId; @Schema(description = "成本价", required = true) private BigDecimal cost; @Schema(description = "秒杀产品ID", required = true) private Long seckillId; @Schema(description = "配送方式 1=快递 ,2=门店自提\"", required = true) private Integer shippingType; @Schema(description = "取餐时间", required = true) private LocalDateTime getTime; @Schema(description = "取餐号", required = true) private Integer numberId; @Schema(description = "购买类型:takein=自取,takeout=外卖", required = true) private String orderType; @Schema(description = "门店", required = true) private AppStoreShopVO shop; @Schema(description = "门店id", required = true) private Long shopId; @Schema(description = "前面等等的制作数量", required = true) private Long preNum; @Schema(description = "店铺名称", required = true) private String shopName; @Schema(description = "桌面ID", required = true) private Long deskId; @Schema(description = "桌号", required = true) private String deskNumber; @Schema(description = "就餐人数", required = true) private Integer deskPeople; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/convert/storeorder/StoreOrderConvert.java ================================================ package co.yixiang.yshop.module.order.convert.storeorder; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.order.controller.app.order.vo.AppStoreOrderQueryVo; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import co.yixiang.yshop.module.order.controller.admin.storeorder.vo.*; import co.yixiang.yshop.module.order.dal.dataobject.storeorder.StoreOrderDO; /** * 订单 Convert * * @author yshop */ @Mapper public interface StoreOrderConvert { StoreOrderConvert INSTANCE = Mappers.getMapper(StoreOrderConvert.class); StoreOrderDO convert(StoreOrderCreateReqVO bean); StoreOrderDO convert(StoreOrderUpdateReqVO bean); StoreOrderRespVO convert(StoreOrderDO bean); AppStoreOrderQueryVo convert1(StoreOrderDO bean); List convertList(List list); List convertList01(List list); PageResult convertPage(PageResult page); List convertList02(List list); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/convert/storeordercartinfo/StoreOrderCartInfoConvert.java ================================================ package co.yixiang.yshop.module.order.convert.storeordercartinfo; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; /** * 订单购物详情 Convert * * @author yshop */ @Mapper public interface StoreOrderCartInfoConvert { StoreOrderCartInfoConvert INSTANCE = Mappers.getMapper(StoreOrderCartInfoConvert.class); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/convert/storeorderstatus/StoreOrderStatusConvert.java ================================================ package co.yixiang.yshop.module.order.convert.storeorderstatus; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; /** * 订单操作记录 Convert * * @author yshop */ @Mapper public interface StoreOrderStatusConvert { StoreOrderStatusConvert INSTANCE = Mappers.getMapper(StoreOrderStatusConvert.class); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/dal/dataobject/ordernumber/OrderNumberDO.java ================================================ package co.yixiang.yshop.module.order.dal.dataobject.ordernumber; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; import java.math.BigDecimal; import java.time.LocalDateTime; /** * 订单 DO * * @author yshop */ @TableName("yshop_order_number") @KeySequence("yshop_order_number_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class OrderNumberDO { /** * ID */ private Long id; /** * 订单号 */ private String orderId; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/dal/dataobject/storeorder/StoreOrderDO.java ================================================ package co.yixiang.yshop.module.order.dal.dataobject.storeorder; import lombok.*; import java.util.*; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.LocalDateTime; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; /** * 订单 DO * * @author yshop */ @TableName("yshop_store_order") @KeySequence("yshop_store_order_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class StoreOrderDO extends BaseDO { /** * 订单ID */ @TableId private Long id; /** * 订单号 */ private String orderId; /** * 额外订单号 */ private String extendOrderId; /** * 用户id */ private Long uid; /** * 用户姓名 */ private String realName; /** * 用户电话 */ private String userPhone; /** * 详细地址 */ private String userAddress; /** * 购物车id */ private String cartId; /** * 运费金额 */ private BigDecimal freightPrice; /** * 订单商品总数 */ private Integer totalNum; /** * 订单总价 */ private BigDecimal totalPrice; /** * 邮费 */ private BigDecimal totalPostage; /** * 实际支付金额 */ private BigDecimal payPrice; /** * 支付邮费 */ private BigDecimal payPostage; /** * 抵扣金额 */ private BigDecimal deductionPrice; /** * 优惠券id */ private Integer couponId; /** * 优惠券金额 */ private BigDecimal couponPrice; /** * 支付状态 */ private Integer paid; /** * 支付时间 */ private LocalDateTime payTime; /** * 支付方式 */ private String payType; /** * 订单类型 购买类型:takein=自取,takeout=外卖 */ private String orderType; /** * 订单状态(-1 : 申请退款 -2 : 退货成功 0:待发货;1:待收货;2:已收货;3:已完成;-1:已退款) */ private Integer status; /** * 0 未退款 1 申请中 2 已退款 */ private Integer refundStatus; /** * 退款图片 */ private String refundReasonWapImg; /** * 退款用户说明 */ private String refundReasonWapExplain; /** * 退款时间 */ private LocalDateTime refundReasonTime; /** * 前台退款原因 */ private String refundReasonWap; /** * 不退款的理由 */ private String refundReason; /** * 退款金额 */ private BigDecimal refundPrice; /** * 快递公司编号 */ private String deliverySn; /** * 快递名称/送货人姓名 */ private String deliveryName; /** * 发货类型 */ private String deliveryType; /** * 快递单号/手机号 */ private String deliveryId; /** * 发货时间 */ private LocalDateTime deliveryTime; /** * 消费赚取积分 */ private BigDecimal gainIntegral; /** * 使用积分 */ private BigDecimal useIntegral; /** * 实际支付积分 */ private BigDecimal payIntegral; /** * 给用户退了多少积分 */ private BigDecimal backIntegral; /** * 备注 */ private String mark; /** * 唯一id(md5加密)类似id */ @TableField(value = "`unique`") private String unique; /** * 管理员备注 */ private String remark; /** * 商户ID */ private Integer merId; /** * 拼团产品id0一般产品 */ private Long combinationId; /** * 拼团id 0没有拼团 */ private Long pinkId; /** * 成本价 */ private BigDecimal cost; /** * 秒杀产品ID */ private Long seckillId; /** * 砍价id */ private Integer bargainId; /** * 核销码 */ private String verifyCode; /** * 门店id */ private Integer storeId; /** * 配送方式 1=快递 ,2=门店自提 */ private Integer shippingType; /** * 支付渠道(0微信公众号1微信小程序) */ private Integer isChannel; /** * 系统删除 */ private Integer isSystemDel; /** * 门店ID */ private Long shopId; /** * 门店名称 */ private String shopName; /** * 取餐时间 */ private LocalDateTime getTime; /** * 取餐👌 */ private Long numberId; private String outTradeNo; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/dal/dataobject/storeordercartinfo/StoreOrderCartInfoDO.java ================================================ package co.yixiang.yshop.module.order.dal.dataobject.storeordercartinfo; import lombok.*; import java.math.BigDecimal; import java.util.*; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; /** * 订单购物详情 DO * * @author yshop */ @TableName("yshop_store_order_cart_info") @KeySequence("yshop_store_order_cart_info_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class StoreOrderCartInfoDO { /** * id */ @TableId private Long id; /** * 订单id */ private Long oid; /** * 订单号 */ private String orderId; /** * 购物车id */ private Long cartId; /** * 商品ID */ private Long productId; /** * 购买东西的详细信息 */ private String cartInfo; /** * 唯一id */ @TableField(value = "`unique`") private String unique; /** * 是否能售后0不能1能 */ private Integer isAfterSales; /** * 商品名称 */ private String title; /** * 商品图片 */ private String image; /** * 数量 */ private Integer number; /** * 规格 */ private String spec; /** * 价格 */ private BigDecimal price; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/dal/dataobject/storeorderstatus/StoreOrderStatusDO.java ================================================ package co.yixiang.yshop.module.order.dal.dataobject.storeorderstatus; import lombok.*; import java.util.*; import java.time.LocalDateTime; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; /** * 订单操作记录 DO * * @author yshop */ @TableName("yshop_store_order_status") @KeySequence("yshop_store_order_status_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class StoreOrderStatusDO { /** * id */ @TableId private Long id; /** * 订单id */ private Long oid; /** * 操作类型 */ private String changeType; /** * 操作备注 */ private String changeMessage; /** * 操作时间 */ private LocalDateTime changeTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/dal/mysql/ordernumber/OrderNumberMapper.java ================================================ package co.yixiang.yshop.module.order.dal.mysql.ordernumber; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.order.dal.dataobject.ordernumber.OrderNumberDO; import org.apache.ibatis.annotations.Mapper; /** * 订单 Mapper * * @author yshop */ @Mapper public interface OrderNumberMapper extends BaseMapperX { } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/dal/mysql/storeorder/StoreOrderMapper.java ================================================ package co.yixiang.yshop.module.order.dal.mysql.storeorder; import java.util.*; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.enums.OrderInfoEnum; import co.yixiang.yshop.framework.common.enums.ShopCommonEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import co.yixiang.yshop.module.order.dal.dataobject.storeorder.StoreOrderDO; import co.yixiang.yshop.module.order.enums.AdminOrderStatusEnum; import co.yixiang.yshop.module.order.enums.OrderLogEnum; import co.yixiang.yshop.framework.common.enums.OrderTypeEnum; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.toolkit.Constants; import org.apache.ibatis.annotations.Mapper; import co.yixiang.yshop.module.order.controller.admin.storeorder.vo.*; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; /** * 订单 Mapper * * @author yshop */ @Mapper public interface StoreOrderMapper extends BaseMapperX { default PageResult selectPage(StoreOrderPageReqVO reqVO) { LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX(); Long shopId = SecurityFrameworkUtils.getLoginUser().getShopId(); if(shopId > 0) { wrapper.eq(StoreOrderDO::getShopId,shopId); } wrapper.eqIfPresent(StoreOrderDO::getOrderId, reqVO.getOrderId()) .eqIfPresent(StoreOrderDO::getNumberId, reqVO.getNumberId()) .eqIfPresent(StoreOrderDO::getShopId, reqVO.getShopId()) .likeIfPresent(StoreOrderDO::getRealName, reqVO.getRealName()) .eqIfPresent(StoreOrderDO::getUserPhone, reqVO.getUserPhone()) .eqIfPresent(StoreOrderDO::getOrderType,reqVO.getOrderType()) .betweenIfPresent(StoreOrderDO::getCreateTime, reqVO.getCreateTime()); //.orderByDesc(StoreOrderDO::getId); if( OrderTypeEnum.TYPE_WORK.getValue().equals(reqVO.getType())){ wrapper.ne(StoreOrderDO::getIsSystemDel, ShopCommonEnum.DELETE_1.getValue()).orderByAsc(StoreOrderDO::getCreateTime); }else{ wrapper.orderByDesc(StoreOrderDO::getCreateTime); } if (reqVO.getOrderStatus() != null) { switch (AdminOrderStatusEnum.toType(reqVO.getOrderStatus())) { //未支付 case STATUS_0: wrapper.ne(StoreOrderDO::getIsSystemDel, ShopCommonEnum.DELETE_1.getValue()) .eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_0.getValue()) .eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()) .eq(StoreOrderDO::getStatus, OrderInfoEnum.STATUS_0.getValue()); break; //待发货 case STATUS_1: wrapper.ne(StoreOrderDO::getIsSystemDel, ShopCommonEnum.DELETE_1.getValue()) .eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()) .eq(StoreOrderDO::getStatus, OrderInfoEnum.STATUS_0.getValue()); if( OrderTypeEnum.TYPE_WORK.getValue().equals(reqVO.getType())){ wrapper.and(i->i.eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_1.getValue()) .or(j->j.eq(StoreOrderDO::getOrderType, OrderLogEnum.ORDER_TAKE_DESK.getValue()) .eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_0.getValue()))); }else { wrapper.eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_1.getValue()); } break; //待收货 case STATUS_2: wrapper.ne(StoreOrderDO::getIsSystemDel, ShopCommonEnum.DELETE_1.getValue()) .eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_1.getValue()) .eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()) .eq(StoreOrderDO::getStatus, OrderInfoEnum.STATUS_1.getValue()); break; //待评价 case STATUS_3: wrapper.ne(StoreOrderDO::getIsSystemDel, ShopCommonEnum.DELETE_1.getValue()) .eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_1.getValue()) .eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()) .eq(StoreOrderDO::getStatus, OrderInfoEnum.STATUS_2.getValue()); break; //已完成 case STATUS_4: wrapper.ne(StoreOrderDO::getIsSystemDel, ShopCommonEnum.DELETE_1.getValue()) .eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_1.getValue()) .eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()) .eq(StoreOrderDO::getStatus, OrderInfoEnum.STATUS_3.getValue()); break; //退款单 case STATUS_5: String[] strs = {"1", "2"}; wrapper.ne(StoreOrderDO::getIsSystemDel, ShopCommonEnum.DELETE_1.getValue()) .in(StoreOrderDO::getRefundStatus, strs); break; //已删除 case STATUS_6: wrapper.eq(StoreOrderDO::getIsSystemDel, ShopCommonEnum.DELETE_1.getValue()); break; default: } } if (StrUtil.isNotEmpty(reqVO.getPayStatus())) { wrapper.eq(StoreOrderDO::getPayType,reqVO.getPayStatus()); } return selectPage(reqVO, wrapper); } default List selectList(StoreOrderExportReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .eqIfPresent(StoreOrderDO::getOrderId, reqVO.getOrderId()) .likeIfPresent(StoreOrderDO::getRealName, reqVO.getRealName()) .eqIfPresent(StoreOrderDO::getUserPhone, reqVO.getUserPhone()) .eqIfPresent(StoreOrderDO::getUserAddress, reqVO.getUserAddress()) .betweenIfPresent(StoreOrderDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(StoreOrderDO::getId)); } @Select("select IFNULL(sum(pay_price),0) from yshop_store_order " + "where paid=1 and deleted=0 and refund_status=0 and uid=#{uid}") double sumPrice(@Param("uid") Long uid); @Select("SELECT IFNULL(sum(pay_price),0) " + " FROM yshop_store_order ${ew.customSqlSegment}") Double todayPrice(@Param(Constants.WRAPPER) Wrapper wrapper); @Select( "select IFNULL(sum(pay_price),0) from yshop_store_order " + "where refund_status=0 and deleted=0 and paid=1") Double sumTotalPrice(); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/dal/mysql/storeordercartinfo/StoreOrderCartInfoMapper.java ================================================ package co.yixiang.yshop.module.order.dal.mysql.storeordercartinfo; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.order.dal.dataobject.storeordercartinfo.StoreOrderCartInfoDO; import org.apache.ibatis.annotations.Mapper; /** * 订单购物详情 Mapper * * @author yshop */ @Mapper public interface StoreOrderCartInfoMapper extends BaseMapperX { } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/dal/mysql/storeorderstatus/StoreOrderStatusMapper.java ================================================ package co.yixiang.yshop.module.order.dal.mysql.storeorderstatus; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.order.dal.dataobject.storeorderstatus.StoreOrderStatusDO; import org.apache.ibatis.annotations.Mapper; /** * 订单操作记录 Mapper * * @author yshop */ @Mapper public interface StoreOrderStatusMapper extends BaseMapperX { } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/dal/redis/RedisKeyConstants.java ================================================ package co.yixiang.yshop.module.order.dal.redis; import co.yixiang.yshop.framework.redis.core.RedisKeyDefine; import co.yixiang.yshop.module.order.service.storeorder.dto.CacheDto; import static co.yixiang.yshop.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING; /** * System Redis Key 枚举类 * * @author yshop */ public interface RedisKeyConstants { RedisKeyDefine YSHOP_ORDER_CACHE_KEY = new RedisKeyDefine("确认订单数据缓存", "yshop_order_cache:%s", // 参数为访问uid+key STRING, CacheDto.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); RedisKeyDefine YSHOP_ORDER_SALE_STATUS_KEY = new RedisKeyDefine("售后订单数据缓存", "yshop_after_order_cache:%s", // 参数为访问uid+key STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); RedisKeyDefine YSHOP_ORDER_COUNT_CACHE_KEY = new RedisKeyDefine("统计订单数据缓存", "yshop_order_count_cache:%s", // 参数为访问uid STRING, CacheDto.class, RedisKeyDefine.TimeoutTypeEnum.FOREVER); RedisKeyDefine YSHOP_ADMIN_ORDER_COUNT_CACHE_KEY = new RedisKeyDefine("后台统计订单数据缓存", "yshop_admin_order_count_cache:", // 参数为访问uid STRING, CacheDto.class, RedisKeyDefine.TimeoutTypeEnum.FOREVER); RedisKeyDefine YSHOP_WEB_PRINT_MECHINE_KEY = new RedisKeyDefine("打印机token", "yshop_web_print_machine_cache:%s", // 参数为访问shopid STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/dal/redis/ofterorder/AfterOrderRedisDAO.java ================================================ package co.yixiang.yshop.module.order.dal.redis.ofterorder; import co.yixiang.yshop.module.order.service.storeorder.dto.CacheDto; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Repository; import jakarta.annotation.Resource; import static co.yixiang.yshop.module.order.dal.redis.RedisKeyConstants.YSHOP_ORDER_SALE_STATUS_KEY; /** * {@link CacheDto} 的 RedisDAO * * @author yshop */ @Repository public class AfterOrderRedisDAO { @Resource private StringRedisTemplate stringRedisTemplate; public String get(String key,Long uid) { String redisKey = formatKey(key+uid); return stringRedisTemplate.opsForValue().get(redisKey); } public void set(Long uid,String key,String o) { String redisKey = formatKey(key + uid); stringRedisTemplate.opsForValue().set(redisKey, o); } public void delete(String key,Long uid) { String redisKey = formatKey(key+uid); stringRedisTemplate.delete(redisKey); } private static String formatKey(String key) { return String.format(YSHOP_ORDER_SALE_STATUS_KEY.getKeyTemplate(), key); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/dal/redis/order/AsyncCountRedisDAO.java ================================================ package co.yixiang.yshop.module.order.dal.redis.order; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserOrderCountVo; import co.yixiang.yshop.module.order.controller.admin.storeorder.vo.ShoperOrderTimeDataVo; import co.yixiang.yshop.module.order.service.storeorder.dto.OrderTimeDataDto; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Repository; import jakarta.annotation.Resource; import static co.yixiang.yshop.module.order.dal.redis.RedisKeyConstants.YSHOP_ADMIN_ORDER_COUNT_CACHE_KEY; /** * {@link AppUserOrderCountVo} 的 RedisDAO * * @author yshop */ @Repository public class AsyncCountRedisDAO { @Resource private StringRedisTemplate stringRedisTemplate; public OrderTimeDataDto get() { String redisKey = formatKey(); return JsonUtils.parseObject(stringRedisTemplate.opsForValue().get(redisKey), OrderTimeDataDto.class); } public void set(OrderTimeDataDto orderTimeDataDto) { String redisKey = formatKey(); stringRedisTemplate.opsForValue().set(redisKey, JsonUtils.toJsonString(orderTimeDataDto)); } public void delete(Long uid) { String redisKey = YSHOP_ADMIN_ORDER_COUNT_CACHE_KEY.getKeyTemplate(); stringRedisTemplate.delete(redisKey); } private static String formatKey() { return String.format(YSHOP_ADMIN_ORDER_COUNT_CACHE_KEY.getKeyTemplate()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/dal/redis/order/AsyncOrderRedisDAO.java ================================================ package co.yixiang.yshop.module.order.dal.redis.order; import cn.hutool.core.util.IdUtil; import co.yixiang.yshop.framework.common.constant.ShopConstants; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserOrderCountVo; import co.yixiang.yshop.module.order.service.storeorder.dto.CacheDto; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Repository; import jakarta.annotation.Resource; import java.util.concurrent.TimeUnit; import static co.yixiang.yshop.module.order.dal.redis.RedisKeyConstants.YSHOP_ORDER_CACHE_KEY; import static co.yixiang.yshop.module.order.dal.redis.RedisKeyConstants.YSHOP_ORDER_COUNT_CACHE_KEY; /** * {@link AppUserOrderCountVo} 的 RedisDAO * * @author yshop */ @Repository public class AsyncOrderRedisDAO { @Resource private StringRedisTemplate stringRedisTemplate; public AppUserOrderCountVo get(Long uid) { String redisKey = formatKey(""+uid); return JsonUtils.parseObject(stringRedisTemplate.opsForValue().get(redisKey), AppUserOrderCountVo.class); } public void set(AppUserOrderCountVo appUserOrderCountVo, Long uid) { String redisKey = formatKey("" + uid); stringRedisTemplate.opsForValue().set(redisKey, JsonUtils.toJsonString(appUserOrderCountVo)); } public void delete(Long uid) { String redisKey = formatKey(""+uid); stringRedisTemplate.delete(redisKey); } private static String formatKey(String key) { return String.format(YSHOP_ORDER_COUNT_CACHE_KEY.getKeyTemplate(), key); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/dal/redis/order/OrderRedisDAO.java ================================================ package co.yixiang.yshop.module.order.dal.redis.order; import cn.hutool.core.util.IdUtil; import co.yixiang.yshop.framework.common.constant.ShopConstants; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.module.order.service.storeorder.dto.CacheDto; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Repository; import jakarta.annotation.Resource; import java.util.concurrent.TimeUnit; import static co.yixiang.yshop.module.order.dal.redis.RedisKeyConstants.YSHOP_ORDER_CACHE_KEY; /** * {@link CacheDto} 的 RedisDAO * * @author yshop */ @Repository public class OrderRedisDAO { @Resource private StringRedisTemplate stringRedisTemplate; public CacheDto get(String key,Long uid) { String redisKey = formatKey(key+uid); return JsonUtils.parseObject(stringRedisTemplate.opsForValue().get(redisKey), CacheDto.class); } public String set(CacheDto cacheDto,Long uid) { String key = IdUtil.simpleUUID(); String redisKey = formatKey(key + uid); long time = ShopConstants.YSHOP_ORDER_CACHE_TIME; stringRedisTemplate.opsForValue().set(redisKey, JsonUtils.toJsonString(cacheDto), time, TimeUnit.SECONDS); return key; } public void delete(String key,Long uid) { String redisKey = formatKey(key+uid); stringRedisTemplate.delete(redisKey); } private static String formatKey(String key) { return String.format(YSHOP_ORDER_CACHE_KEY.getKeyTemplate(), key); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/dal/redis/order/PrintMechinRedisDAO.java ================================================ package co.yixiang.yshop.module.order.dal.redis.order; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Repository; import jakarta.annotation.Resource; import java.util.concurrent.TimeUnit; import static co.yixiang.yshop.module.order.dal.redis.RedisKeyConstants.YSHOP_WEB_PRINT_MECHINE_KEY; import static co.yixiang.yshop.module.store.dal.redis.RedisKeyConstants.YSHOP_WEB_PRINT_TOKEN_KEY; /** * * @author yshop */ @Repository public class PrintMechinRedisDAO { @Resource private StringRedisTemplate stringRedisTemplate; public String get(Long shopId) { String redisKey = formatKey(shopId); return stringRedisTemplate.opsForValue().get(redisKey); } public void set(Long shopId,String o) { String redisKey = formatKey(shopId); stringRedisTemplate.opsForValue().set(redisKey, o); } public void delete(Long shopId) { String redisKey = formatKey(shopId); stringRedisTemplate.delete(redisKey); } private static String formatKey(Long shopId) { return String.format(YSHOP_WEB_PRINT_MECHINE_KEY.getKeyTemplate(), shopId); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/handle/OrderAutoConfirmListener.java ================================================ package co.yixiang.yshop.module.order.handle; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.constant.ShopConstants; import co.yixiang.yshop.framework.tenant.core.util.TenantUtils; import co.yixiang.yshop.module.message.redismq.DelayedQueueListener; import co.yixiang.yshop.module.message.redismq.msg.OrderMsg; import co.yixiang.yshop.module.order.service.storeorder.AppStoreOrderService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; /** * 自动收货订单监听 */ @Component @Slf4j public class OrderAutoConfirmListener implements DelayedQueueListener { @Resource private AppStoreOrderService appStoreOrderService; @Override public String delayedQueueKey() { return ShopConstants.REDIS_ORDER_OUTTIME_UNCONFIRM; } @Override public void consume(OrderMsg message) throws Exception { if(ObjectUtil.isNotNull(message) && StrUtil.isNotEmpty(message.getOrderId())) { TenantUtils.executeIgnore(() -> { appStoreOrderService.takeOrder(message.getOrderId(),null); log.info("订单编号:{}自动确认收货成功",message.getOrderId()); }); } } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/handle/OrderUnPayListener.java ================================================ package co.yixiang.yshop.module.order.handle; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.constant.ShopConstants; import co.yixiang.yshop.framework.tenant.core.util.TenantUtils; import co.yixiang.yshop.module.message.redismq.DelayedQueueListener; import co.yixiang.yshop.module.message.redismq.msg.OrderMsg; import co.yixiang.yshop.module.order.service.storeorder.AppStoreOrderService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; /** * 未支付订单监听 */ @Component @Slf4j public class OrderUnPayListener implements DelayedQueueListener { @Resource private AppStoreOrderService appStoreOrderService; @Override public String delayedQueueKey() { return ShopConstants.REDIS_ORDER_OUTTIME_UNPAY_QUEUE; } @Override public void consume(OrderMsg message) throws Exception { if(ObjectUtil.isNotNull(message) && StrUtil.isNotEmpty(message.getOrderId())) { TenantUtils.executeIgnore(() -> { appStoreOrderService.cancelOrder(message.getOrderId(),null); log.info("订单编号:{}自动取消订单成功",message.getOrderId()); }); } } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/handle/RedisDelayHandle.java ================================================ package co.yixiang.yshop.module.order.handle; import cn.hutool.core.thread.ExecutorBuilder; import cn.hutool.core.thread.ThreadFactoryBuilder; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.constant.ShopConstants; import co.yixiang.yshop.module.order.service.storeorder.AppStoreOrderService; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.redisson.api.RBlockingDeque; import org.redisson.api.RedissonClient; import org.springframework.stereotype.Component; import jakarta.annotation.PostConstruct; import jakarta.annotation.Resource; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadFactory; /** * 延时队列消费 已经被废弃 * @author hupeng * @date 2023.7.27 */ //@Component @Slf4j @Deprecated public class RedisDelayHandle { @Resource private RedissonClient redissonClient; @Resource private AppStoreOrderService appStoreOrderService; @PostConstruct public void startJobTimer() { ThreadFactory threadFactory = new ThreadFactoryBuilder().setNamePrefix("delay-job-service").build(); log.info("========延时队列开始========="); ExecutorService executorService = ExecutorBuilder.create() .setCorePoolSize(2) .setMaxPoolSize(10) .setKeepAliveTime(0) .setThreadFactory(threadFactory) .build(); executorService.execute(new ExecutorTaskUnPay()); executorService.execute(new ExecutorTaskUnConfirm()); } class ExecutorTaskUnPay implements Runnable { @SneakyThrows @Override public void run() { RBlockingDeque blockingDeque = redissonClient .getBlockingDeque(ShopConstants.REDIS_ORDER_OUTTIME_UNPAY_QUEUE); while (true) { try { log.info("======延迟队列监听未支付订单======"); String orderId = blockingDeque.take(); log.info("延迟队列监听未支付订单订单编号:{}", orderId); if(StrUtil.isNotEmpty(orderId)) { appStoreOrderService.cancelOrder(orderId,null); } } catch (Exception e) { log.error("Redission延迟队列监测异常中断,忽略此消息:{}", e.getMessage()); return; } } } } class ExecutorTaskUnConfirm implements Runnable { @SneakyThrows @Override public void run() { RBlockingDeque blockingDeque = redissonClient .getBlockingDeque(ShopConstants.REDIS_ORDER_OUTTIME_UNCONFIRM); while (true) { try { log.info("======延迟队列监听收货超时订单======"); String orderId = blockingDeque.take(); log.info("延迟队列监听收货超时订单订单编号:{}", orderId); if(StrUtil.isNotEmpty(orderId)) { appStoreOrderService.takeOrder(orderId,null); } } catch (Exception e) { log.error("Redission延迟队列监测异常中断,忽略此消息:{}", e.getMessage()); return; } } } } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/mq/consumer/PayNoticeConsumer.java ================================================ package co.yixiang.yshop.module.order.mq.consumer; import co.yixiang.yshop.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener; import co.yixiang.yshop.module.order.service.storeorder.AppStoreOrderService; import co.yixiang.yshop.module.pay.mq.message.PayNoticeMessage; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; /** * 消息队列处理支付消息 */ @Component @Slf4j public class PayNoticeConsumer extends AbstractRedisStreamMessageListener { @Resource private AppStoreOrderService appStoreOrderService; @Override public void onMessage(PayNoticeMessage message) { log.info("[onMessage][支付消息内容({})]", message); appStoreOrderService.paySuccess(message.getOrderId(),message.getPayType()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/AppStoreOrderService.java ================================================ package co.yixiang.yshop.module.order.service.storeorder; import co.yixiang.yshop.module.order.controller.app.order.param.AppComputeOrderParam; import co.yixiang.yshop.module.order.controller.app.order.param.AppOrderParam; import co.yixiang.yshop.module.order.controller.app.order.param.AppPayParam; import co.yixiang.yshop.module.order.controller.app.order.vo.AppConfirmOrderVo; import co.yixiang.yshop.module.order.controller.app.order.vo.AppStoreOrderQueryVo; import co.yixiang.yshop.module.order.dal.dataobject.storeorder.StoreOrderDO; import com.baomidou.mybatisplus.extension.service.IService; import java.math.BigDecimal; import java.util.List; import java.util.Map; /** * 订单 Service 接口 * * @author yshop */ public interface AppStoreOrderService extends IService { /** * 订单信息 * @param unique 唯一值或者单号 * @param uid 用户id * @return YxStoreOrderQueryVo */ AppStoreOrderQueryVo getOrderInfo(String unique, Long uid); /** * 创建订单 * @param uid 用户uid * @param param param * @return map */ Map createOrder(Long uid, AppOrderParam param); /** * 第三方支付 * @param param 订单 * @param uid 用户id */ Map pay(Long uid, AppPayParam param); /** * 余额支付 * @param orderId 订单号 * @param uid 用户id */ void yuePay(String orderId,Long uid); /** * 支付成功后操作 * @param orderId 订单号 * @param payType 支付方式 */ void paySuccess(String orderId,String payType); /** * 订单列表 * @param uid 用户id * @param type OrderStatusEnum * @param page page * @param limit limit * @return list */ List orderList(Long uid, int type, int page, int limit); /** * 处理订单返回的状态 * @param order order * @return YxStoreOrderQueryVo */ AppStoreOrderQueryVo handleOrder(AppStoreOrderQueryVo order); /** * 订单确认收货 * @param orderId 单号 * @param uid uid */ void takeOrder(String orderId,Long uid); /** * 申请退款 * @param explain 退款备注 * @param Img 图片 * @param text 理由 * @param orderId 订单号 * @param uid uid */ void orderApplyRefund(String explain,String Img,String text,String orderId, Long uid); /** * 删除订单 * @param orderId 单号 * @param uid uid */ void removeOrder(String orderId,Long uid); /** * 未付款取消订单 * @param orderId 订单号 * @param uid 用户id */ void cancelOrder(String orderId,Long uid); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/AppStoreOrderServiceImpl.java ================================================ package co.yixiang.yshop.module.order.service.storeorder; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.constant.ShopConstants; import co.yixiang.yshop.framework.common.enums.OrderInfoEnum; import co.yixiang.yshop.framework.common.enums.PayIdEnum; import co.yixiang.yshop.framework.common.enums.ShopCommonEnum; import co.yixiang.yshop.framework.common.exception.ErrorCode; import co.yixiang.yshop.framework.tenant.core.aop.TenantIgnore; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import co.yixiang.yshop.module.coupon.dal.dataobject.couponuser.CouponUserDO; import co.yixiang.yshop.module.coupon.service.couponuser.AppCouponUserService; import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserQueryVo; import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO; import co.yixiang.yshop.module.member.dal.dataobject.useraddress.UserAddressDO; import co.yixiang.yshop.module.member.dal.dataobject.userbill.UserBillDO; import co.yixiang.yshop.module.member.enums.BillDetailEnum; import co.yixiang.yshop.module.member.service.user.MemberUserService; import co.yixiang.yshop.module.member.service.useraddress.AppUserAddressService; import co.yixiang.yshop.module.member.service.userbill.UserBillService; import co.yixiang.yshop.module.message.enums.WechatTempateEnum; import co.yixiang.yshop.module.message.mq.producer.WeixinNoticeProducer; import co.yixiang.yshop.module.message.redismq.msg.OrderMsg; import co.yixiang.yshop.module.order.controller.app.order.param.AppOrderParam; import co.yixiang.yshop.module.order.controller.app.order.param.AppPayParam; import co.yixiang.yshop.module.order.controller.app.order.vo.AppStoreOrderQueryVo; import co.yixiang.yshop.module.order.convert.storeorder.StoreOrderConvert; import co.yixiang.yshop.module.order.dal.dataobject.ordernumber.OrderNumberDO; import co.yixiang.yshop.module.order.dal.dataobject.storeorder.StoreOrderDO; import co.yixiang.yshop.module.order.dal.dataobject.storeordercartinfo.StoreOrderCartInfoDO; import co.yixiang.yshop.module.order.dal.mysql.ordernumber.OrderNumberMapper; import co.yixiang.yshop.module.order.dal.mysql.storeorder.StoreOrderMapper; import co.yixiang.yshop.module.order.enums.AppFromEnum; import co.yixiang.yshop.module.order.enums.OrderLogEnum; import co.yixiang.yshop.module.order.enums.OrderStatusEnum; import co.yixiang.yshop.module.order.enums.PayTypeEnum; import co.yixiang.yshop.module.order.service.storeorder.dto.StatusDto; import co.yixiang.yshop.module.order.service.storeordercartinfo.StoreOrderCartInfoService; import co.yixiang.yshop.module.order.service.storeorderstatus.StoreOrderStatusService; import co.yixiang.yshop.module.pay.dal.dataobject.merchantdetails.MerchantDetailsDO; import co.yixiang.yshop.module.pay.service.merchantdetails.MerchantDetailsService; import co.yixiang.yshop.module.product.dal.dataobject.storeproduct.StoreProductDO; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattrvalue.StoreProductAttrValueDO; import co.yixiang.yshop.module.product.service.storeproduct.AppStoreProductService; import co.yixiang.yshop.module.product.service.storeproductattrvalue.StoreProductAttrValueService; import co.yixiang.yshop.module.store.convert.storeshop.StoreShopConvert; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; import co.yixiang.yshop.module.store.service.storeshop.AppStoreShopService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.egzosn.pay.spring.boot.core.PayServiceManager; import com.egzosn.pay.spring.boot.core.bean.MerchantPayOrder; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.redisson.api.RBlockingDeque; import org.redisson.api.RDelayedQueue; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.redisson.client.codec.StringCodec; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.net.URLEncoder; import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.member.enums.ErrorCodeConstants.COUPON_NOT_CONDITION; import static co.yixiang.yshop.module.member.enums.ErrorCodeConstants.USER_ADDRESS_NOT_EXISTS; import static co.yixiang.yshop.module.order.enums.ErrorCodeConstants.*; /** * 订单 Service 实现类 * * @author yshop */ @Slf4j @Service @Validated public class AppStoreOrderServiceImpl extends ServiceImpl implements AppStoreOrderService { @Resource private StoreOrderMapper storeOrderMapper; @Resource private AppUserAddressService appUserAddressService; @Resource private MemberUserService userService; @Resource private AppStoreProductService appStoreProductService; @Resource private StoreOrderCartInfoService storeOrderCartInfoService; @Resource private StoreOrderStatusService storeOrderStatusService; @Resource private UserBillService billService; @Resource private PayServiceManager manager; @Resource private WeixinNoticeProducer weixinNoticeProducer; @Resource private RedissonClient redissonClient; @Resource private StoreProductAttrValueService storeProductAttrValueService; @Resource private AppStoreShopService appStoreShopService; @Resource private OrderNumberMapper orderNumberMapper; @Resource private AppCouponUserService appCouponUserService; @Resource private AsyncStoreOrderService asyncStoreOrderService; @Resource private MerchantDetailsService merchantDetailsService; private static final String LOCK_KEY = "cart:check:stock:lock"; private static final String STOCK_LOCK_KEY = "cart:do:stock:lock"; /** * 订单信息 * * @param unique 唯一值或者单号 * @param uid 用户id * @return YxStoreOrderQueryVo */ @Override public AppStoreOrderQueryVo getOrderInfo(String unique, Long uid) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.and( i -> i.eq(StoreOrderDO::getOrderId, unique).or().eq(StoreOrderDO::getUnique, unique).or() .eq(StoreOrderDO::getExtendOrderId, unique)); if (uid != null) { wrapper.eq(StoreOrderDO::getUid, uid); } AppStoreOrderQueryVo appStoreOrderQueryVo = StoreOrderConvert.INSTANCE.convert1(storeOrderMapper.selectOne(wrapper)); return appStoreOrderQueryVo; } /** * 创建订单 * * @param uid 用户uid * @param param param * @return YxStoreOrder */ @Override @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public Map createOrder(Long uid, AppOrderParam param) { if(OrderLogEnum.ORDER_TAKE_DESK.getValue().equals(param.getOrderType()) && StrUtil.isBlank(param.getDeskNumber())){ throw exception(STORE_ORDER_DESK_NOT); } //转换参数 List productIds = param.getProductId(); List numbers = param.getNumber(); List specs = param.getSpec(); Integer totalNum = 0; List cartIds = new ArrayList<>(); StoreShopDO storeShopDO = appStoreShopService.getById(param.getShopId()); BigDecimal sumPrice = BigDecimal.ZERO; BigDecimal couponPrice = BigDecimal.ZERO; BigDecimal postagePrice = storeShopDO.getDeliveryPrice(); BigDecimal deductionPrice = BigDecimal.ZERO; //对库存检查加锁 RLock lock = redissonClient.getLock(LOCK_KEY); if (lock.tryLock()){ try { for (int i = 0;i < productIds.size();i++){ String newSku = StrUtil.replace(specs.get(i),"|",","); appStoreProductService.checkProductStock(uid, Long.valueOf(productIds.get(i)), Integer.valueOf(numbers.get(i)), newSku); totalNum += Integer.valueOf(numbers.get(i)); StoreProductAttrValueDO storeProductAttrValue = storeProductAttrValueService .getOne(Wrappers.lambdaQuery() .eq(StoreProductAttrValueDO::getSku, newSku) .eq(StoreProductAttrValueDO::getProductId, productIds.get(i))); sumPrice = NumberUtil.add(sumPrice, NumberUtil.mul(numbers.get(i), storeProductAttrValue.getPrice().toString())); } }catch (Exception ex) { log.error("[checkProductStock][执行异常]", ex); throw exception(new ErrorCode(999999,ex.getMessage())); } finally { lock.unlock(); } } //计算优惠券价格 if(StrUtil.isNotBlank(param.getCouponId())){ CouponUserDO couponUserDO = appCouponUserService.getById(param.getCouponId()); if(couponUserDO != null){ if(couponUserDO.getLeast().compareTo(sumPrice) > 0) { throw exception(COUPON_NOT_CONDITION); } couponPrice = couponUserDO.getValue(); //使用了优惠券扣优惠券 couponUserDO.setStatus(ShopCommonEnum.IS_STATUS_1.getValue()); appCouponUserService.updateById(couponUserDO); } } BigDecimal payPrice = BigDecimal.ZERO; //计算最终支付价格 if(OrderLogEnum.ORDER_TAKE_OUT.getValue().equals(param.getOrderType())){ payPrice = NumberUtil.sub(NumberUtil.add(sumPrice,postagePrice),couponPrice,deductionPrice); }else{ payPrice = NumberUtil.sub(sumPrice,couponPrice,deductionPrice); } //计算奖励积分 BigDecimal gainIntegral = this.getGainIntegral(productIds); StoreOrderDO storeOrder = new StoreOrderDO(); String orderSn = ""; //todo 桌面点餐功能 商业版本才有 官网地址:https://www.yixiang.co if(OrderLogEnum.ORDER_TAKE_DESK.getValue().equals(param.getOrderType()) && StrUtil.isNotBlank(param.getOrderId())){ }else{ //生成分布式唯一值 orderSn = IdUtil.getSnowflake(0, 0).nextIdStr(); //添加取餐表 OrderNumberDO orderNumberDO = OrderNumberDO.builder().orderId(orderSn).build(); orderNumberMapper.insert(orderNumberDO); //组合数据 LocalDateTime localDateTime = LocalDateTime.now(); storeOrder.setGetTime(localDateTime.plusMinutes(param.getGettime())); storeOrder.setNumberId(orderNumberDO.getId()); storeOrder.setShopId(storeShopDO.getId()); storeOrder.setShopName(storeShopDO.getName()); storeOrder.setUid(uid); storeOrder.setOrderId(orderSn); //处理如果是外卖 地址 if(OrderLogEnum.ORDER_TAKE_OUT.getValue().equals(param.getOrderType())){ if (StrUtil.isEmpty(param.getAddressId())) { throw exception(SELECT_ADDRESS); } UserAddressDO userAddress = appUserAddressService.getById(param.getAddressId()); if (ObjectUtil.isNull(userAddress)) { throw exception(USER_ADDRESS_NOT_EXISTS); } storeOrder.setRealName(userAddress.getRealName()); storeOrder.setUserPhone(userAddress.getPhone()); storeOrder.setUserAddress(userAddress.getAddress() + " " + userAddress.getDetail()); } storeOrder.setCartId(StrUtil.join(",", cartIds)); storeOrder.setTotalNum(totalNum); storeOrder.setTotalPrice(sumPrice); storeOrder.setTotalPostage(storeShopDO.getDeliveryPrice()); storeOrder.setCouponId(StrUtil.isBlank(param.getCouponId()) ? 0 : Integer.valueOf(param.getCouponId())); storeOrder.setCouponPrice(couponPrice); storeOrder.setPayPrice(payPrice); storeOrder.setPayPostage(storeShopDO.getDeliveryPrice()); storeOrder.setDeductionPrice(deductionPrice); storeOrder.setPaid(OrderInfoEnum.PAY_STATUS_0.getValue()); storeOrder.setPayType(param.getPayType()); storeOrder.setUseIntegral(BigDecimal.ZERO); storeOrder.setBackIntegral(BigDecimal.ZERO); storeOrder.setGainIntegral(gainIntegral); storeOrder.setMark(param.getRemark()); storeOrder.setCost(BigDecimal.ZERO); //storeOrder.setUnique(key); storeOrder.setShippingType(OrderInfoEnum.SHIPPIING_TYPE_1.getValue()); storeOrder.setOrderType(param.getOrderType()); boolean res = this.save(storeOrder); if (!res) { throw exception(ORDER_GEN_FAIL); } } // 减库存加销量 this.deStockIncSale(productIds,numbers,specs); //保存购物车商品信息,异步执行 storeOrderCartInfoService.saveCartInfo(storeOrder.getId(), storeOrder.getOrderId(),productIds,numbers,specs); ////todo 桌面点餐功能 商业版本才有 官网地址:https://www.yixiang.co异步更新桌面信息 //增加状态 storeOrderStatusService.create(uid,storeOrder.getId(), OrderLogEnum.CREATE_ORDER.getValue(), OrderLogEnum.CREATE_ORDER.getDesc()); //堂食点餐不需要 if(!OrderLogEnum.ORDER_TAKE_DESK.getValue().equals(param.getOrderType())) { //加入延时队列,30分钟自动取消 try { RBlockingDeque blockingDeque = redissonClient.getBlockingDeque(ShopConstants.REDIS_ORDER_OUTTIME_UNPAY_QUEUE ); RDelayedQueue delayedQueue = redissonClient.getDelayedQueue(blockingDeque); delayedQueue.offer(OrderMsg.builder().orderId(orderSn).build(), ShopConstants.ORDER_OUTTIME_UNPAY, TimeUnit.MINUTES); String s = TimeUnit.SECONDS.toSeconds(ShopConstants.ORDER_OUTTIME_UNPAY) + "分钟"; log.info("添加延时队列成功 ,延迟时间:" + s + "订单编号:" + orderSn); } catch (Exception e) { log.error("添加延时队列失败:{}",e.getMessage()); } } Map map = new HashMap<>(); map.put("orderId",orderSn); return map; } /** * 第三方支付 * @param uid 用户id * @param param 订单参数 * @return */ @Override public Map pay(Long uid, AppPayParam param) { AppStoreOrderQueryVo orderInfo = getOrderInfo(param.getUni(), uid); UserBillDO userBillDO = billService.getOne(new LambdaQueryWrapper().eq(UserBillDO::getUid,uid) .eq(UserBillDO::getExtendField,param.getUni())); if (ObjectUtil.isNull(orderInfo) && ObjectUtil.isNull(userBillDO)) { throw exception(STORE_ORDER_NOT_EXISTS); } if(ObjectUtil.isNotNull(orderInfo) && orderInfo.getPaid().equals(OrderInfoEnum.PAY_STATUS_1.getValue())) { throw exception(ORDER_PAY_FINISH); } if(ObjectUtil.isNotNull(userBillDO) && userBillDO.getStatus().equals(OrderInfoEnum.PAY_STATUS_1.getValue())) { throw exception(ORDER_PAY_FINISH); } MemberUserDO memberUserDO = userService.getUser(uid); Map map = new LinkedHashMap<>(); BigDecimal price = BigDecimal.ZERO; String msg = ""; String detailsId = ""; if(orderInfo != null) { price = orderInfo.getPayPrice(); msg = "商品购买"; }else if(userBillDO != null){ //todo 充值商业版本才有 官网购买:https://www.yixiang.co } switch (PayTypeEnum.toType(param.getPaytype())){ case WEIXIN: if(AppFromEnum.H5.getValue().equals(param.getFrom())){ detailsId = PayIdEnum.WX_WECHAT.getValue(); //todo 如果启用微信H5支付充值 需要另外增加一个配置用于同步跳转页面 比如下面的增加了一个id=5的配置,微信支付公众号与H%配置是一样 基本 // if(orderInfo != null) { // detailsId = "4"; // }else{ // detailsId = "5"; // } MerchantPayOrder payOrder = new MerchantPayOrder(detailsId, "MWEB", msg, msg, price, param.getUni()); Map payOrderInfo = manager.getOrderInfo(payOrder); MerchantDetailsDO merchantDetailsDO = merchantDetailsService.getMerchantDetails(detailsId); String url = merchantDetailsDO.getReturnUrl(); String newUrl = ""; try { newUrl = String.format("%s%s", payOrderInfo.get("mweb_url"), "&redirect_url=" + URLEncoder.encode(url,"UTF-8")); } catch (UnsupportedEncodingException e) { log.error(e.getMessage()); } map.put("data",newUrl); map.put("trade_type","MWEB"); } else if(AppFromEnum.WECHAT.getValue().equals(param.getFrom())){//微信公众号 // MerchantPayOrder payOrder = new MerchantPayOrder("4", "JSAPI", msg, // msg, price, param.getUni()); MerchantPayOrder payOrder = new MerchantPayOrder(PayIdEnum.WX_WECHAT.getValue(), "JSAPI", msg, msg, price, param.getUni()); payOrder.setOpenid(memberUserDO.getOpenid()); map.put("data",manager.getOrderInfo(payOrder)); map.put("trade_type","W-JSAPI"); } else {//微信小程序 MerchantPayOrder payOrder = new MerchantPayOrder(PayIdEnum.WX_MINIAPP.getValue(), "JSAPI", msg, msg, price, param.getUni()); payOrder.setOpenid(memberUserDO.getRoutineOpenid()); map.put("data",manager.getOrderInfo(payOrder)); map.put("trade_type","JSAPI"); } break; case YUE: this.yuePay(param.getUni(), uid); map.put("status","ok"); break; case ALI: //h5支付 detailsId = PayIdEnum.ALI_H5.getValue(); //todo 如果启用支付宝H5支付充值 需要另外增加一个配置用于同步跳转页面 比如下面的增加了一个id=6的配置 // if(orderInfo != null) { // detailsId = "1"; // }else{ // detailsId = "6"; // } MerchantPayOrder payOrder = new MerchantPayOrder(detailsId, "WAP", msg, msg, price, param.getUni()); map.put("data",manager.toPay(payOrder)); default: } return map; } /** * 余额支付 * * @param orderId 订单号 * @param uid 用户id */ @Override @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public void yuePay(String orderId, Long uid) { AppStoreOrderQueryVo orderInfo = getOrderInfo(orderId, uid); if (ObjectUtil.isNull(orderInfo)) { throw exception(STORE_ORDER_NOT_EXISTS); } if (OrderInfoEnum.PAY_STATUS_1.getValue().equals(orderInfo.getPaid())) { throw exception(ORDER_PAY_FINISH); } AppUserQueryVo userInfo = userService.getAppUser(uid); if (userInfo.getNowMoney().compareTo(orderInfo.getPayPrice()) < 0) { throw exception(PAY_YUE_NOT); } userService.decPrice(uid, orderInfo.getPayPrice()); //支付成功后处理 this.paySuccess(orderInfo.getOrderId(), PayTypeEnum.YUE.getValue()); } /** * 支付成功后操作 * * @param orderId 订单号 * @param payType 支付方式 */ @Override @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) @TenantIgnore public void paySuccess(String orderId, String payType) { //处理充值与会员卡订单 UserBillDO userBillDO = billService.getOne(new LambdaQueryWrapper() .eq(UserBillDO::getExtendField,orderId)); if(userBillDO != null) { userBillDO.setStatus(ShopCommonEnum.IS_STATUS_1.getValue()); billService.updateById(userBillDO); if(BillDetailEnum.TYPE_1.getValue().equals(userBillDO.getType())){ //充值 userService.incMoney(userBillDO.getUid(), userBillDO.getNumber()); } return; } log.info("orderId:[{}]",orderId); AppStoreOrderQueryVo orderInfo = getOrderInfo(orderId, null); log.info("orderInfo:[{}]",orderInfo); if(orderInfo == null){ return; } //更新订单状态 LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StoreOrderDO::getOrderId, orderId); StoreOrderDO storeOrder = new StoreOrderDO(); storeOrder.setPaid(OrderInfoEnum.PAY_STATUS_1.getValue()); storeOrder.setPayType(payType); storeOrder.setPayTime(LocalDateTime.now()); this.update(storeOrder, wrapper); //增加用户购买次数 userService.incPayCount(orderInfo.getUid()); //增加状态 storeOrderStatusService.create(orderInfo.getUid(),orderInfo.getId(), OrderLogEnum.PAY_ORDER_SUCCESS.getValue(), OrderLogEnum.PAY_ORDER_SUCCESS.getDesc()); MemberUserDO userInfo = userService.getUser(orderInfo.getUid()); //增加流水 String payTypeMsg = PayTypeEnum.WEIXIN.getDesc(); if (PayTypeEnum.YUE.getValue().equals(payType)) { payTypeMsg = PayTypeEnum.YUE.getDesc(); }else if (PayTypeEnum.ALI.getValue().equals(payType)) { payTypeMsg = PayTypeEnum.ALI.getDesc(); }else if(PayTypeEnum.CASH.getValue().equals(payType)){ payTypeMsg = PayTypeEnum.CASH.getDesc(); } billService.expend(userInfo.getId(), "购买商品", BillDetailEnum.CATEGORY_1.getValue(), BillDetailEnum.TYPE_3.getValue(), orderInfo.getPayPrice().doubleValue(), userInfo.getNowMoney().doubleValue(), payTypeMsg + orderInfo.getPayPrice() + "元购买商品"); //发送消息队列进行推送消息,堂食不需要 if(!OrderLogEnum.ORDER_TAKE_DESK.getValue().equals(orderInfo.getOrderType()) && userInfo.getLoginType().equals(AppFromEnum.ROUNTINE.getValue())){ List storeOrderCartInfoDOList = storeOrderCartInfoService .list(new LambdaQueryWrapper() .eq(StoreOrderCartInfoDO::getOid,orderInfo.getId())); List names = storeOrderCartInfoDOList.stream().map(StoreOrderCartInfoDO::getTitle) .collect(Collectors.toList()); String productName = StrUtil.join(",",names); weixinNoticeProducer.sendNoticeMessage(orderInfo.getUid(),WechatTempateEnum.PAY_SUCCESS.getValue(), WechatTempateEnum.SUBSCRIBE.getValue(),orderInfo.getOrderId(), "","","","",orderInfo.getId(),orderInfo.getNumberId(), productName,orderInfo.getShopName()); } } /** * 减库存增加销量 * * @param productIds 商品id * @param numbers 商品数量 * @param specs 商品规格 */ @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public void deStockIncSale(List productIds,List numbers,List specs) { log.info("========减库存增加销量start========="); //对库存加锁 RLock lock = redissonClient.getLock(STOCK_LOCK_KEY); if (lock.tryLock()) { try { for (int i = 0;i < productIds.size();i++){ String newSku = StrUtil.replace(specs.get(i),"|",","); appStoreProductService.decProductStock(Integer.valueOf(numbers.get(i)), Long.valueOf(productIds.get(i)), newSku, 0L, ""); } }catch (Exception ex) { log.error("[deStockIncSale][执行异常]", ex); throw exception(new ErrorCode(999999,ex.getMessage())); } finally { lock.unlock(); } } } /** * 订单列表 * * @param uid 用户id * @param type OrderStatusEnum * @param page page * @param limit limit * @return list */ @Override public List orderList(Long uid, int type, int page, int limit) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); if (uid != null) { wrapper.eq(StoreOrderDO::getUid, uid); } wrapper.eq(StoreOrderDO::getIsSystemDel,ShopCommonEnum.DELETE_0.getValue()).orderByDesc(StoreOrderDO::getId); // wrapper.eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_1.getValue()) //.eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()) //.eq(StoreOrderDO::getStatus, OrderInfoEnum.STATUS_0.getValue()) // .orderByDesc(StoreOrderDO::getId); switch (OrderStatusEnum.toType(type)) { case STATUS__1: break; //未支付 case STATUS_0: wrapper.eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_0.getValue()) .eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()) .eq(StoreOrderDO::getStatus, OrderInfoEnum.STATUS_0.getValue()); break; //已经支付 case STATUS_1: wrapper.eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_1.getValue()) .eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()) .eq(StoreOrderDO::getStatus, OrderInfoEnum.STATUS_0.getValue()); break; //待收货 case STATUS_2: wrapper.eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_1.getValue()) .eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()) .eq(StoreOrderDO::getStatus, OrderInfoEnum.STATUS_1.getValue()); break; // //待评价 // case STATUS_3: // wrapper.eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_1.getValue()) // .eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()) // .eq(StoreOrderDO::getStatus, OrderInfoEnum.STATUS_2.getValue()); // break; //已完成 case STATUS_4: wrapper.eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_1.getValue()) .eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()) .ge(StoreOrderDO::getStatus, OrderInfoEnum.STATUS_2.getValue()); break; //退款 case STATUS_MINUS_3: String[] strs = {"1", "2"}; wrapper.in(StoreOrderDO::getRefundStatus, Arrays.asList(strs)); break; default: } Page pageModel = new Page<>(page, limit); IPage pageList = storeOrderMapper.selectPage(pageModel, wrapper); List list = StoreOrderConvert.INSTANCE.convertList01(pageList.getRecords()); return list.stream().map(this::handleOrder).collect(Collectors.toList()); } /** * 处理订单返回的状态 * * @param order order * @return YxStoreOrderQueryVo */ @Override public AppStoreOrderQueryVo handleOrder(AppStoreOrderQueryVo order) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StoreOrderCartInfoDO::getOid, order.getId()); List cartInfos = storeOrderCartInfoService.list(wrapper); order.setCartInfo(cartInfos); StoreShopDO storeShopDO = appStoreShopService.getById(order.getShopId()); order.setShop(StoreShopConvert.INSTANCE.convert02(storeShopDO)); long count = storeOrderMapper.selectCount(new LambdaQueryWrapper() .eq(StoreOrderDO::getShopId,order.getShopId()) .lt(StoreOrderDO::getCreateTime,order.getCreateTime()) .eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_1.getValue()) .eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()) .eq(StoreOrderDO::getStatus, OrderInfoEnum.STATUS_0.getValue())); order.setPreNum(count); StatusDto statusDTO = new StatusDto(); if (OrderStatusEnum.STATUS_0.getValue().equals(order.getPaid())) { //计算未支付到自动取消订 时间 int offset = Integer.valueOf(String.valueOf(ShopConstants.ORDER_OUTTIME_UNPAY)); //Date time = DateUtil.offsetMinute(order.getCreateTime()., offset); statusDTO.setYClass("nobuy"); //statusDTO.setMsg(StrUtil.format("请在{}前完成支付", DateUtil.formatDateTime(time))); statusDTO.setType("0"); statusDTO.setTitle("未支付"); } else if (OrderInfoEnum.REFUND_STATUS_1.getValue().equals(order.getRefundStatus())) { statusDTO.setYClass("state-sqtk"); statusDTO.setMsg("商家审核中,请耐心等待"); statusDTO.setType("-1"); statusDTO.setTitle("申请退款中"); } else if (OrderInfoEnum.REFUND_STATUS_2.getValue().equals(order.getRefundStatus())) { statusDTO.setYClass("state-sqtk"); statusDTO.setMsg("已为您退款,感谢您的支持"); statusDTO.setType("-2"); statusDTO.setTitle("已退款"); } else if (OrderInfoEnum.STATUS_0.getValue().equals(order.getStatus())) { // 拼团 todo if (order.getPinkId() > 0) { } else { statusDTO.setYClass("state-nfh"); statusDTO.setMsg("商家未发货,请耐心等待"); statusDTO.setType("1"); statusDTO.setTitle("制作中"); } } else if (OrderInfoEnum.STATUS_1.getValue().equals(order.getStatus())) { if(OrderLogEnum.ORDER_TAKE_OUT.getValue().equals(order.getOrderType())){ statusDTO.setTitle("配送中"); }else{ statusDTO.setTitle("待取餐"); } statusDTO.setYClass("state-ysh"); statusDTO.setMsg("服务商已发货"); statusDTO.setType("2"); } else if (OrderInfoEnum.STATUS_2.getValue().equals(order.getStatus())) { if(OrderLogEnum.ORDER_TAKE_OUT.getValue().equals(order.getOrderType())){ statusDTO.setTitle("已收货"); }else{ statusDTO.setTitle("已取餐"); } statusDTO.setYClass("state-ypj"); statusDTO.setMsg("已收货,快去评价一下吧"); statusDTO.setType("3"); } else if (OrderInfoEnum.STATUS_3.getValue().equals(order.getStatus())) { statusDTO.setYClass("state-ytk"); statusDTO.setMsg("交易完成,感谢您的支持"); statusDTO.setType("4"); statusDTO.setTitle("交易完成"); } if (PayTypeEnum.WEIXIN.getValue().equals(order.getPayType())) { statusDTO.setPayType("微信支付"); } else if (PayTypeEnum.YUE.getValue().equals(order.getPayType())) { statusDTO.setPayType("余额支付"); } else if (PayTypeEnum.ALI.getValue().equals(order.getPayType())) { statusDTO.setPayType("支付宝支付"); }else { statusDTO.setPayType("积分支付"); } order.setStatusDto(statusDTO); return order; } /** * 订单确认收货 * * @param orderId 单号 * @param uid uid */ @Override public void takeOrder(String orderId, Long uid) { AppStoreOrderQueryVo order = this.getOrderInfo(orderId, uid); if (ObjectUtil.isNull(order)) { throw exception(STORE_ORDER_NOT_EXISTS); } if (OrderInfoEnum.PAY_STATUS_0.getValue().equals(order.getPaid())) { throw exception(ORDER_STATUS_ERROR); } if (OrderInfoEnum.STATUS_3.getValue().equals(order.getStatus())){ throw exception(ORDER_STATUS_FINISH); } order = handleOrder(order); // if (order.getOrderType().equals(OrderLogEnum.ORDER_TAKE_OUT.getValue()) // && !OrderStatusEnum.STATUS_2.getValue().toString().equals(order.get_status().get_type())) { // throw exception(ORDER_STATUS_ERROR); // } StoreOrderDO storeOrder = new StoreOrderDO(); storeOrder.setStatus(OrderInfoEnum.STATUS_3.getValue()); storeOrder.setId(order.getId()); this.updateById(storeOrder); //增加状态 storeOrderStatusService.create(order.getUid(),order.getId(), OrderLogEnum.TAKE_ORDER_DELIVERY.getValue(), OrderLogEnum.TAKE_ORDER_DELIVERY.getDesc()); //奖励积分 this.gainUserIntegral(order); //分销计算 todo } /** * 申请退款 * * @param explain 退款备注 * @param Img 图片 * @param text 理由 * @param orderId 订单号 * @param uid uid */ @Override public void orderApplyRefund(String explain, String Img, String text, String orderId, Long uid) { AppStoreOrderQueryVo order = this.getOrderInfo(orderId, uid); if (ObjectUtil.isNull(order)) { throw exception(STORE_ORDER_NOT_EXISTS); } if (OrderInfoEnum.REFUND_STATUS_2.getValue().equals(order.getRefundStatus())) { throw exception(ORDER_REFUNDED); } if (OrderInfoEnum.REFUND_STATUS_1.getValue().equals(order.getRefundStatus())) { throw exception(ORDER_REFUNDING); } StoreOrderDO storeOrder = new StoreOrderDO(); storeOrder.setRefundStatus(OrderInfoEnum.REFUND_STATUS_1.getValue()); storeOrder.setRefundReasonTime(LocalDateTime.now()); storeOrder.setRefundReasonWapExplain(explain); storeOrder.setRefundReasonWapImg(Img); storeOrder.setRefundReasonWap(text); storeOrder.setId(order.getId()); this.updateById(storeOrder); //增加状态 storeOrderStatusService.create(order.getUid(),order.getId(), OrderLogEnum.REFUND_ORDER_APPLY.getValue(), "用户申请退款,原因:" + text); //todo 消息推送 } /** * 删除订单 * * @param orderId 单号 * @param uid uid */ @Override public void removeOrder(String orderId, Long uid) { AppStoreOrderQueryVo order = this.getOrderInfo(orderId, uid); if (order == null) { throw exception(STORE_ORDER_NOT_EXISTS); } order = handleOrder(order); if (!OrderInfoEnum.STATUS_3.getValue().equals(order.getStatus())) { throw exception(ORDER_NOT_DELETE); } this.removeById(order.getId()); //增加状态 storeOrderStatusService.create(uid,order.getId(), OrderLogEnum.REMOVE_ORDER.getValue(), OrderLogEnum.REMOVE_ORDER.getDesc()); } /** * 未付款取消订单 * * @param orderId 订单号 * @param uid 用户id */ @Override @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public void cancelOrder(String orderId, Long uid) { log.info("订单取消:({})",orderId); AppStoreOrderQueryVo order = this.getOrderInfo(orderId, uid); if (ObjectUtil.isNull(order)) { throw exception(STORE_ORDER_NOT_EXISTS); } if (order.getPaid() != 0) { throw exception(ORDER_NOT_CANCEL); } this.regressionStock(order); this.regressionCoupon(order, 0); storeOrderMapper.deleteById(order.getId()); } /** * 退回积分 * * @param order 订单 */ private void regressionIntegral(AppStoreOrderQueryVo order, Integer type) { if (OrderInfoEnum.PAY_STATUS_1.getValue().equals(order.getPaid()) || OrderStatusEnum.STATUS_MINUS_2.getValue().equals(order.getStatus())) { return; } if (order.getPayIntegral().compareTo(BigDecimal.ZERO) > 0) { order.setUseIntegral(order.getPayIntegral()); } if (order.getUseIntegral().compareTo(BigDecimal.ZERO) <= 0) { return; } if (!OrderStatusEnum.STATUS_MINUS_2.getValue().equals(order.getStatus()) && !OrderInfoEnum.REFUND_STATUS_2.getValue().equals(order.getRefundStatus()) && order.getBackIntegral().compareTo(BigDecimal.ZERO) > 0) { return; } MemberUserDO yxUser = userService.getById(order.getUid()); //增加积分 BigDecimal newIntegral = NumberUtil.add(order.getUseIntegral(), yxUser.getIntegral()); yxUser.setIntegral(newIntegral); userService.updateById(yxUser); //增加流水 billService.income(yxUser.getId(), "积分回退", BillDetailEnum.CATEGORY_2.getValue(), BillDetailEnum.TYPE_8.getValue(), order.getUseIntegral().doubleValue(), newIntegral.doubleValue(), "购买商品失败,回退积分" + order.getUseIntegral(), order.getId().toString()); //更新回退积分 StoreOrderDO storeOrder = new StoreOrderDO(); storeOrder.setBackIntegral(order.getUseIntegral()); storeOrder.setId(order.getId()); this.updateById(storeOrder); } /** * 退回优惠券 * * @param order 订单 todo */ private void regressionCoupon(AppStoreOrderQueryVo order, Integer type) { if (OrderInfoEnum.PAY_STATUS_1.getValue().equals(order.getPaid()) || OrderStatusEnum.STATUS_MINUS_2.getValue().equals(order.getStatus())) { return; } if (order.getCouponId() != null && order.getCouponId() > 0) { CouponUserDO couponUser = appCouponUserService .getOne(Wrappers.lambdaQuery() .eq(CouponUserDO::getId, order.getCouponId()) .eq(CouponUserDO::getStatus, ShopCommonEnum.IS_STATUS_1.getValue()) .eq(CouponUserDO::getUserId, order.getUid())); if (ObjectUtil.isNotNull(couponUser)) { couponUser.setStatus(ShopCommonEnum.IS_STATUS_0.getValue()); appCouponUserService.updateById(couponUser); } } } /** * 退回库存 * * @param order 订单 */ private void regressionStock(AppStoreOrderQueryVo order) { if (OrderInfoEnum.PAY_STATUS_1.getValue().equals(order.getPaid()) || OrderStatusEnum.STATUS_MINUS_2.getValue().equals(order.getStatus())) { return; } LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StoreOrderCartInfoDO::getOid, order.getId()); List cartInfoList = storeOrderCartInfoService.list(wrapper); for (StoreOrderCartInfoDO cartInfo : cartInfoList) { String newSku = StrUtil.replace(cartInfo.getSpec(),"|",","); appStoreProductService.incProductStock(cartInfo.getNumber(), cartInfo.getProductId() ,newSku, 0L, null); } } /** * 奖励积分 * * @param order 订单 */ private void gainUserIntegral(AppStoreOrderQueryVo order) { if (order.getGainIntegral().compareTo(BigDecimal.ZERO) > 0) { MemberUserDO user = userService.getUser(order.getUid()); BigDecimal newIntegral = NumberUtil.add(user.getIntegral(), order.getGainIntegral()); user.setIntegral(newIntegral); user.setId(order.getUid()); userService.updateById(user); //增加流水 billService.income(user.getId(), "购买商品赠送积分", BillDetailEnum.CATEGORY_2.getValue(), BillDetailEnum.TYPE_9.getValue(), order.getGainIntegral().doubleValue(), newIntegral.doubleValue(), "购买商品赠送" + order.getGainIntegral() + "积分", order.getId().toString()); } } /** * 计算奖励的积分 * * @param productIds * @return double */ private BigDecimal getGainIntegral(List productIds) { BigDecimal gainIntegral = BigDecimal.ZERO; for (int i = 0;i < productIds.size();i++){ StoreProductDO storeProductDO = appStoreProductService.getById(productIds.get(i)); if(storeProductDO.getGiveIntegral().intValue() == 0){ continue; } gainIntegral = NumberUtil.add(gainIntegral, storeProductDO.getGiveIntegral()); } return gainIntegral; } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/AsynStoreOrderServiceImpl.java ================================================ package co.yixiang.yshop.module.order.service.storeorder; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.enums.OrderInfoEnum; import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserOrderCountVo; import co.yixiang.yshop.module.member.service.user.MemberUserService; import co.yixiang.yshop.module.order.controller.admin.storeorder.vo.ShoperOrderTimeDataVo; import co.yixiang.yshop.module.order.controller.app.order.vo.AppStoreOrderQueryVo; import co.yixiang.yshop.module.order.dal.dataobject.storeorder.StoreOrderDO; import co.yixiang.yshop.module.order.dal.dataobject.storeordercartinfo.StoreOrderCartInfoDO; import co.yixiang.yshop.module.order.dal.mysql.storeorder.StoreOrderMapper; import co.yixiang.yshop.module.order.dal.mysql.storeordercartinfo.StoreOrderCartInfoMapper; import co.yixiang.yshop.module.order.dal.redis.order.AsyncCountRedisDAO; import co.yixiang.yshop.module.order.dal.redis.order.AsyncOrderRedisDAO; import co.yixiang.yshop.module.order.dal.redis.order.PrintMechinRedisDAO; import co.yixiang.yshop.module.order.enums.OrderLogEnum; import co.yixiang.yshop.module.order.service.storeorder.dto.OrderTimeDataDto; import co.yixiang.yshop.module.product.service.storeproduct.StoreProductService; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; import co.yixiang.yshop.module.store.dal.mysql.storeshop.StoreShopMapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.math.BigDecimal; import java.util.Date; import java.util.List; /** * 异步订单 Service 实现类 * * @author yshop */ @Service @Validated @Slf4j public class AsynStoreOrderServiceImpl implements AsyncStoreOrderService { @Resource private StoreOrderMapper storeOrderMapper; @Resource private AsyncOrderRedisDAO asyncOrderRedisDAO; @Resource private AsyncCountRedisDAO asyncCountRedisDAO; @Resource private MemberUserService userService; @Resource private StoreProductService productService; @Resource private PrintMechinRedisDAO printMechinRedisDAO; @Resource private StoreShopMapper storeShopMapper; @Resource private StoreOrderCartInfoMapper storeOrderCartInfoMapper; /** * 获取某个用户的订单统计数据 * * @param uid uid>0 取用户 否则取所有 * @return */ @Override @Async public void orderData(Long uid) { log.info("========获取某个用户的订单统计数据========="); //订单支付没有退款 数量 LambdaQueryWrapper wrapperOne = new LambdaQueryWrapper<>(); if (uid != null) { wrapperOne.eq(StoreOrderDO::getUid, uid); } wrapperOne.eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()) .eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_1.getValue()); Long orderCount = storeOrderMapper.selectCount(wrapperOne); //订单支付没有退款 支付总金额 double sumPrice = storeOrderMapper.sumPrice(uid); //订单待支付 数量 LambdaQueryWrapper wrapperTwo = new LambdaQueryWrapper<>(); if (uid != null) { wrapperTwo.eq(StoreOrderDO::getUid, uid); } wrapperTwo.eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_0.getValue()) .eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()) .eq(StoreOrderDO::getStatus, OrderInfoEnum.STATUS_0.getValue()); Long unpaidCount = storeOrderMapper.selectCount(wrapperTwo); //订单待发货 数量 LambdaQueryWrapper wrapperThree = new LambdaQueryWrapper<>(); if (uid != null) { wrapperThree.eq(StoreOrderDO::getUid, uid); } wrapperThree.eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_1.getValue()) .eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()) .eq(StoreOrderDO::getStatus, OrderInfoEnum.STATUS_0.getValue()); Long unshippedCount = storeOrderMapper.selectCount(wrapperThree); //订单待收货 数量 LambdaQueryWrapper wrapperFour = new LambdaQueryWrapper<>(); if (uid != null) { wrapperFour.eq(StoreOrderDO::getUid, uid); } wrapperFour.eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_1.getValue()) .eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()) .eq(StoreOrderDO::getStatus, OrderInfoEnum.STATUS_1.getValue()); Long receivedCount = storeOrderMapper.selectCount(wrapperFour); //订单待评价 数量 LambdaQueryWrapper wrapperFive = new LambdaQueryWrapper<>(); if (uid != null) { wrapperFive.eq(StoreOrderDO::getUid, uid); } wrapperFive.eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_1.getValue()) .eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()) .eq(StoreOrderDO::getStatus, OrderInfoEnum.STATUS_2.getValue()); Long evaluatedCount = storeOrderMapper.selectCount(wrapperFive); //订单已完成 数量 LambdaQueryWrapper wrapperSix = new LambdaQueryWrapper<>(); if (uid != null) { wrapperSix.eq(StoreOrderDO::getUid, uid); } wrapperSix.eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_1.getValue()) .eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()) .eq(StoreOrderDO::getStatus, OrderInfoEnum.STATUS_3.getValue()); Long completeCount = storeOrderMapper.selectCount(wrapperSix); //售后退款 Long salesCount = 0L; AppUserOrderCountVo appUserOrderCountVo = AppUserOrderCountVo.builder() .orderCount(orderCount) .sumPrice(sumPrice) .unpaidCount(unpaidCount) .unshippedCount(unshippedCount) .receivedCount(receivedCount) .evaluatedCount(evaluatedCount) .completeCount(completeCount) .refundCount(salesCount) .build(); //存redis asyncOrderRedisDAO.set(appUserOrderCountVo,uid); this.getOrderTimeData(); } /** * 首页订单/用户等统计 * * @return OrderTimeDataDto */ @Async @Override public void getOrderTimeData() { OrderTimeDataDto orderTimeDataDto = new OrderTimeDataDto(); ShoperOrderTimeDataVo shoperOrderTimeData = this.getShoperOrderTimeData(); BeanUtil.copyProperties(shoperOrderTimeData, orderTimeDataDto); orderTimeDataDto.setUserCount(userService.count()); orderTimeDataDto.setOrderCount(storeOrderMapper.selectCount()); orderTimeDataDto.setPriceCount(storeOrderMapper.sumTotalPrice()); orderTimeDataDto.setGoodsCount(productService.count()); asyncCountRedisDAO.set(orderTimeDataDto); } /** * 异步后台统计 */ public ShoperOrderTimeDataVo getShoperOrderTimeData() { Date today = DateUtil.beginOfDay(new Date()); Date yesterday = DateUtil.beginOfDay(DateUtil.yesterday()); Date nowMonth = DateUtil.beginOfMonth(new Date()); Date lastWeek = DateUtil.beginOfDay(DateUtil.lastWeek()); ShoperOrderTimeDataVo orderTimeDataVo = new ShoperOrderTimeDataVo(); //今日成交额 LambdaQueryWrapper wrapperOne = new LambdaQueryWrapper<>(); wrapperOne .ge(StoreOrderDO::getPayTime, today) .eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_1.getValue()) .eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()); orderTimeDataVo.setTodayPrice(storeOrderMapper.todayPrice(wrapperOne)); //今日订单数 orderTimeDataVo.setTodayCount(storeOrderMapper.selectCount(wrapperOne)); //昨日成交额 LambdaQueryWrapper wrapperTwo = new LambdaQueryWrapper<>(); wrapperTwo .lt(StoreOrderDO::getPayTime, today) .ge(StoreOrderDO::getPayTime, yesterday) .eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_1.getValue()) .eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()); orderTimeDataVo.setProPrice(storeOrderMapper.todayPrice(wrapperTwo)); //昨日订单数 orderTimeDataVo.setProCount(storeOrderMapper.selectCount(wrapperTwo)); //本月成交额 LambdaQueryWrapper wrapperThree = new LambdaQueryWrapper<>(); wrapperThree .ge(StoreOrderDO::getPayTime, nowMonth) .eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_1.getValue()) .eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()); orderTimeDataVo.setMonthPrice(storeOrderMapper.todayPrice(wrapperThree)); //本月订单数 orderTimeDataVo.setMonthCount(storeOrderMapper.selectCount(wrapperThree)); //上周成交额 LambdaQueryWrapper wrapperLastWeek = new LambdaQueryWrapper<>(); wrapperLastWeek .lt(StoreOrderDO::getPayTime, today) .ge(StoreOrderDO::getPayTime, lastWeek) .eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_1.getValue()) .eq(StoreOrderDO::getRefundStatus, OrderInfoEnum.REFUND_STATUS_0.getValue()); orderTimeDataVo.setLastWeekPrice(storeOrderMapper.todayPrice(wrapperLastWeek)); //上周订单数 orderTimeDataVo.setLastWeekCount(storeOrderMapper.selectCount(wrapperLastWeek)); return orderTimeDataVo; } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/AsyncStoreOrderService.java ================================================ package co.yixiang.yshop.module.order.service.storeorder; import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserOrderCountVo; import co.yixiang.yshop.module.order.controller.admin.storeorder.vo.ShoperOrderTimeDataVo; import co.yixiang.yshop.module.order.controller.app.order.vo.AppStoreOrderQueryVo; import co.yixiang.yshop.module.order.dal.dataobject.storeorder.StoreOrderDO; import co.yixiang.yshop.module.order.service.storeorder.dto.OrderTimeDataDto; /** * 异步订单 Service 接口 * * @author yshop */ public interface AsyncStoreOrderService { /** * 计算某个用户的订单统计数据 * @param uid uid>0 取用户 否则取所有 * @return UserOrderCountVo */ void orderData(Long uid); /** * 异步后台数据统计 */ void getOrderTimeData(); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/StoreOrderService.java ================================================ package co.yixiang.yshop.module.order.service.storeorder; import java.math.BigDecimal; import java.util.*; import jakarta.validation.*; import co.yixiang.yshop.module.order.controller.admin.storeorder.vo.*; import co.yixiang.yshop.module.order.dal.dataobject.storeorder.StoreOrderDO; import co.yixiang.yshop.framework.common.pojo.PageResult; /** * 订单 Service 接口 * * @author yshop */ public interface StoreOrderService { /** * 创建订单 * * @param createReqVO 创建信息 * @return 编号 */ Long createStoreOrder(@Valid StoreOrderCreateReqVO createReqVO); /** * 更新订单 * * @param updateReqVO 更新信息 */ void updateStoreOrder(@Valid StoreOrderUpdateReqVO updateReqVO); /** * 删除订单 * * @param id 编号 */ void deleteStoreOrder(Long id); /** * 订单线下支付 * * @param id 编号 */ void payStoreOrder(Long id); /** * 确认收货 * * @param id 编号 */ void takeStoreOrder(Long id); /** * 获得订单 * * @param id 编号 * @return 订单 */ StoreOrderRespVO getStoreOrder(Long id); /** * 获得订单列表 * * @param ids 编号 * @return 订单列表 */ List getStoreOrderList(Collection ids); /** * 获得订单分页 * * @param pageReqVO 分页查询 * @return 订单分页 */ PageResult getStoreOrderPage(StoreOrderPageReqVO pageReqVO); /** * 获得订单列表, 用于 Excel 导出 * * @param exportReqVO 查询条件 * @return 订单列表 */ List getStoreOrderList(StoreOrderExportReqVO exportReqVO); /** * 确认订单退款 * * @param id 单号 * @param price 金额 * @param type ShopCommonEnum * @param salesId 售后id */ void orderRefund(Long id, BigDecimal price, Integer type, Long salesId); /** * 订单30s内通知 * @return */ Long orderNotice(); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/StoreOrderServiceImpl.java ================================================ package co.yixiang.yshop.module.order.service.storeorder; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.constant.ShopConstants; import co.yixiang.yshop.framework.common.enums.OrderInfoEnum; import co.yixiang.yshop.framework.common.enums.PayIdEnum; import co.yixiang.yshop.framework.common.enums.ShopCommonEnum; import co.yixiang.yshop.framework.common.exception.ErrorCode; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import co.yixiang.yshop.module.member.controller.admin.user.vo.UserRespVO; import co.yixiang.yshop.module.member.convert.user.UserConvert; import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO; import co.yixiang.yshop.module.member.dal.mysql.user.MemberUserMapper; import co.yixiang.yshop.module.member.enums.BillDetailEnum; import co.yixiang.yshop.module.member.service.user.MemberUserService; import co.yixiang.yshop.module.member.service.userbill.UserBillService; import co.yixiang.yshop.module.message.mq.producer.WeixinNoticeProducer; import co.yixiang.yshop.module.message.redismq.msg.OrderMsg; import co.yixiang.yshop.module.order.controller.admin.storeorder.vo.*; import co.yixiang.yshop.module.order.convert.storeorder.StoreOrderConvert; import co.yixiang.yshop.module.order.dal.dataobject.storeorder.StoreOrderDO; import co.yixiang.yshop.module.order.dal.dataobject.storeordercartinfo.StoreOrderCartInfoDO; import co.yixiang.yshop.module.order.dal.mysql.storeorder.StoreOrderMapper; import co.yixiang.yshop.module.order.dal.mysql.storeordercartinfo.StoreOrderCartInfoMapper; import co.yixiang.yshop.module.order.enums.OrderLogEnum; import co.yixiang.yshop.module.order.enums.PayTypeEnum; import co.yixiang.yshop.module.order.enums.UpdateOrderEnum; import co.yixiang.yshop.module.order.service.storeorderstatus.StoreOrderStatusService; import co.yixiang.yshop.module.product.service.storeproduct.AppStoreProductService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.egzosn.pay.common.bean.RefundOrder; import com.egzosn.pay.common.bean.RefundResult; import com.egzosn.pay.spring.boot.core.PayServiceManager; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.redisson.api.RBlockingDeque; import org.redisson.api.RDelayedQueue; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.member.enums.ErrorCodeConstants.USER_NOT_EXISTS; import static co.yixiang.yshop.module.order.enums.ErrorCodeConstants.*; /** * 订单 Service 实现类 * * @author yshop */ @Slf4j @Service @Validated public class StoreOrderServiceImpl implements StoreOrderService { @Resource private StoreOrderMapper storeOrderMapper; @Resource private MemberUserMapper memberUserMapper; @Resource private StoreOrderCartInfoMapper storeOrderCartInfoMapper; @Resource private StoreOrderStatusService storeOrderStatusService; @Resource private AppStoreOrderService appStoreOrderService; @Resource private MemberUserService userService; @Resource private WeixinNoticeProducer weixinNoticeProducer; @Resource private RedissonClient redissonClient; @Resource private UserBillService billService; @Resource private AppStoreProductService appStoreProductService; @Resource private PayServiceManager manager; @Resource private AsyncStoreOrderService asyncStoreOrderService; @Value("${yshop.demo}") private Boolean isDemo; @Override public Long createStoreOrder(StoreOrderCreateReqVO createReqVO) { // 插入 StoreOrderDO storeOrder = StoreOrderConvert.INSTANCE.convert(createReqVO); storeOrderMapper.insert(storeOrder); // 返回 return storeOrder.getId(); } @Override public void updateStoreOrder(StoreOrderUpdateReqVO updateReqVO) { // 校验存在 // 更新 StoreOrderDO updateObj = StoreOrderConvert.INSTANCE.convert(updateReqVO); StoreOrderDO updateObj2 = storeOrderMapper.selectById(updateReqVO.getId()); if (updateObj2 == null) { throw exception(STORE_ORDER_NOT_EXISTS); } //发货自取模式 直接收货 if(UpdateOrderEnum.ORDER_SEND.getValue().equals(updateReqVO.getUpdateType()) && updateObj.getOrderType().equals(OrderLogEnum.ORDER_TAKE_IN.getValue())){ this.takeStoreOrder(updateObj.getId()); //todo 异步打印小票 商业版本才有 官网购买:https://www.yixiang.co return; } //发货-外卖模式 if(UpdateOrderEnum.ORDER_SEND.getValue().equals(updateReqVO.getUpdateType()) && updateObj.getOrderType().equals(OrderLogEnum.ORDER_TAKE_OUT.getValue())){ updateObj.setStatus(OrderInfoEnum.STATUS_1.getValue()); //todo 异步打印小票 商业版本才有 官网购买:https://www.yixiang.co } //堂食模式 if(UpdateOrderEnum.ORDER_SEND.getValue().equals(updateReqVO.getUpdateType()) && updateObj.getOrderType().equals(OrderLogEnum.ORDER_TAKE_DESK.getValue())){ //todo 异步打印小票 商业版本才有 官网购买:https://www.yixiang.co } storeOrderMapper.updateById(updateObj); if(UpdateOrderEnum.ORDER_SEND.getValue().equals(updateReqVO.getUpdateType())){ //增加状态 storeOrderStatusService.create(updateObj.getUid(),updateObj.getId(), OrderLogEnum.DELIVERY_GOODS.getValue(), "已发货 快递公司:" + updateObj.getDeliveryName() + "快递单号:" + updateObj.getDeliveryId()); MemberUserDO userInfo = userService.getUser(updateObj.getUid()); //发送消息队列进行推送消息 // if(userInfo.getLoginType().equals(AppFromEnum.ROUNTINE.getValue())){ // weixinNoticeProducer.sendNoticeMessage(updateObj.getUid(), WechatTempateEnum.DELIVERY_SUCCESS.getValue(), // WechatTempateEnum.SUBSCRIBE.getValue(),updateObj.getOrderId(), // updateObj.getPayPrice().toString(),"",updateObj.getDeliveryName(), // updateObj.getDeliveryId()); // }else if(userInfo.getLoginType().equals(AppFromEnum.WECHAT.getValue())){ // weixinNoticeProducer.sendNoticeMessage(updateObj.getUid(),WechatTempateEnum.PAY_SUCCESS.getValue(), // WechatTempateEnum.TEMPLATES.getValue(),updateObj.getOrderId(), // updateObj.getPayPrice().toString(),"",updateObj.getDeliveryName(), // updateObj.getDeliveryId()); // } //延时队列 1小时候自动收货 try { RBlockingDeque blockingDeque = redissonClient.getBlockingDeque(ShopConstants.REDIS_ORDER_OUTTIME_UNCONFIRM); RDelayedQueue delayedQueue = redissonClient.getDelayedQueue(blockingDeque); delayedQueue.offer(OrderMsg.builder().orderId(updateObj.getOrderId()).build(), ShopConstants.ORDER_OUTTIME_UNCONFIRM, TimeUnit.MINUTES); String s = TimeUnit.SECONDS.toSeconds(ShopConstants.ORDER_OUTTIME_UNCONFIRM) + "分钟"; log.info("添加延时队列成功 ,延迟时间:" + s); } catch (Exception e) { log.error(e.getMessage()); } } } @Override public void deleteStoreOrder(Long id) { // 校验存在 validateStoreOrderExists(id); // 删除 StoreOrderDO storeOrderDO = StoreOrderDO.builder() .isSystemDel(ShopCommonEnum.DELETE_1.getValue()) .id(id) .build(); storeOrderMapper.updateById(storeOrderDO); } @Override public void payStoreOrder(Long id) { // 校验存在 validateStoreOrderExists(id); StoreOrderDO storeOrderDO = storeOrderMapper.selectById(id); //下线支付 appStoreOrderService.paySuccess(storeOrderDO.getOrderId(),PayTypeEnum.CASH.getValue()); } @Override public void takeStoreOrder(Long id) { StoreOrderDO storeOrderDO = storeOrderMapper.selectById(id); appStoreOrderService.takeOrder(storeOrderDO.getOrderId(),storeOrderDO.getUid()); } private void validateStoreOrderExists(Long id) { if (storeOrderMapper.selectById(id) == null) { throw exception(STORE_ORDER_NOT_EXISTS); } } @Override public StoreOrderRespVO getStoreOrder(Long id) { StoreOrderDO storeOrderDO = storeOrderMapper.selectById(id); StoreOrderRespVO storeOrderRespVO = StoreOrderConvert.INSTANCE.convert(storeOrderDO); MemberUserDO memberUserDO = memberUserMapper.selectById(storeOrderRespVO.getUid()); UserRespVO userRespVO = UserConvert.INSTANCE.convert4(memberUserDO); storeOrderRespVO.setUserRespVO(userRespVO); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StoreOrderCartInfoDO::getOid,storeOrderDO.getId()); List storeOrderCartInfoDOList = storeOrderCartInfoMapper.selectList(wrapper); storeOrderRespVO.setStoreOrderCartInfoDOList(storeOrderCartInfoDOList); storeOrderRespVO.setStatusStr(this.handleOrderStatus(storeOrderRespVO.getPaid() ,storeOrderRespVO.getStatus(),storeOrderRespVO.getRefundStatus(),storeOrderRespVO.getIsSystemDel())); return storeOrderRespVO; } @Override public List getStoreOrderList(Collection ids) { return storeOrderMapper.selectBatchIds(ids); } /** * 订单查询 * @param pageReqVO 分页查询 * @return */ @Override public PageResult getStoreOrderPage(StoreOrderPageReqVO pageReqVO) { PageResult pageResult = storeOrderMapper.selectPage(pageReqVO); PageResult storeOrderRespVO = StoreOrderConvert.INSTANCE.convertPage(pageResult); for (StoreOrderRespVO storeOrderRespVO1 : storeOrderRespVO.getList()) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StoreOrderCartInfoDO::getOid,storeOrderRespVO1.getId()); List storeOrderCartInfoDOList = storeOrderCartInfoMapper.selectList(wrapper); MemberUserDO memberUserDO = memberUserMapper.selectById(storeOrderRespVO1.getUid()); UserRespVO userRespVO = UserConvert.INSTANCE.convert4(memberUserDO); storeOrderRespVO1.setStoreOrderCartInfoDOList(storeOrderCartInfoDOList); storeOrderRespVO1.setUserRespVO(userRespVO); storeOrderRespVO1.setStatusStr(this.handleOrderStatus(storeOrderRespVO1.getPaid() ,storeOrderRespVO1.getStatus(),storeOrderRespVO1.getRefundStatus(),storeOrderRespVO1.getIsSystemDel())); } return storeOrderRespVO; } @Override public List getStoreOrderList(StoreOrderExportReqVO exportReqVO) { return storeOrderMapper.selectList(exportReqVO); } /** * 确认订单退款 * * @param id 单号 * @param price 金额 * @param type ShopCommonEnum * @param salesId 售后id */ @Override @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public void orderRefund(Long id, BigDecimal price, Integer type, Long salesId) { StoreOrderDO storeOrderDO = storeOrderMapper.selectById(id); if (storeOrderDO == null) { throw exception(STORE_ORDER_NOT_EXISTS); } MemberUserDO userQueryVo = userService.getById(storeOrderDO.getUid()); if (ObjectUtil.isNull(userQueryVo)) { throw exception(USER_NOT_EXISTS); } if (OrderInfoEnum.REFUND_STATUS_2.getValue().equals(storeOrderDO.getRefundStatus())) { throw exception(ORDER_REFUNDED); } if (storeOrderDO.getPayPrice().compareTo(price) < 0) { throw exception(ORDER_PRICE_ERROR); } storeOrderDO.setRefundStatus(OrderInfoEnum.REFUND_STATUS_2.getValue()); storeOrderDO.setRefundPrice(price); //生成分布式唯一值用于退款订单 String orderSn = IdUtil.getSnowflake(0, 0).nextIdStr(); BigDecimal balance = userQueryVo.getNowMoney(); //根据支付类型不同退款不同 if (PayTypeEnum.YUE.getValue().equals(storeOrderDO.getPayType())) { //退款到余额 userService.incMoney(storeOrderDO.getUid(), price); balance = balance.add(price); } else if (PayTypeEnum.WEIXIN.getValue().equals(storeOrderDO.getPayType())) { if(isDemo){ throw exception(new ErrorCode(888888,"演示模式没有配置证书无法微信退款的哦!")); } log.error("{},{},{},{}",orderSn,storeOrderDO.getOrderId(),price,storeOrderDO.getPayPrice()); RefundOrder refundOrder = new RefundOrder(orderSn,"",storeOrderDO.getOrderId(),price,storeOrderDO.getPayPrice()); //查询微信退款 if(StrUtil.isNotEmpty(storeOrderDO.getOutTradeNo())){ RefundOrder refundQueryOrder = new RefundOrder(); refundQueryOrder.setRefundNo(storeOrderDO.getOutTradeNo()); Map objectMap = manager.refundQuery(PayIdEnum.WX_MINIAPP.getValue(), refundQueryOrder); if(objectMap.get("'return_code'").toString().equals("SUCCESS")){ storeOrderMapper.updateById(storeOrderDO); return; } } RefundResult refundResult = manager.refund(PayIdEnum.WX_MINIAPP.getValue(), refundOrder); if(refundResult.getCode().equals("FAIL")){ log.error("支付退款错误:{}",refundResult.getMsg()); throw exception(new ErrorCode(999999,refundResult.getMsg())); }else { if(refundResult.getResultCode().equals("FAIL")){ log.error("支付退款错误:{}",refundResult.getResultMsg()); throw exception(new ErrorCode(999998,refundResult.getResultMsg())); } } storeOrderDO.setOutTradeNo(orderSn); }else if (PayTypeEnum.ALI.getValue().equals(storeOrderDO.getPayType())){ throw exception(new ErrorCode(999997,"支付宝暂时不支持退款")); } storeOrderMapper.updateById(storeOrderDO); //增加流水 billService.income(storeOrderDO.getUid(), "商品退款", BillDetailEnum.CATEGORY_1.getValue(), BillDetailEnum.TYPE_5.getValue(), price.doubleValue(), balance.doubleValue(), "订单退款到余额" + price + "元", storeOrderDO.getId().toString(),orderSn); storeOrderStatusService.create(storeOrderDO.getUid(), storeOrderDO.getId(), OrderLogEnum.REFUND_ORDER_SUCCESS.getValue(), "退款给用户:" + price + "元"); //退库存 this.regressionStock(storeOrderDO,0); } /** * 订单30s内通知 * @return */ @Override public Long orderNotice() { LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); Long shopId = SecurityFrameworkUtils.getLoginUser().getShopId(); if(shopId > 0) { wrapper.eq(StoreOrderDO::getShopId,shopId); } wrapper.eq(StoreOrderDO::getPaid, OrderInfoEnum.PAY_STATUS_1.getValue()); LocalDateTime nowTime = LocalDateTime.now(); wrapper.ge(StoreOrderDO::getCreateTime,nowTime.minusSeconds(30)); return storeOrderMapper.selectCount(wrapper); } /** * 退回库存 * * @param order 订单 */ private void regressionStock(StoreOrderDO order, Integer type) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.in(StoreOrderCartInfoDO::getOid, order.getId()); List cartInfoList = storeOrderCartInfoMapper.selectList(wrapper); for (StoreOrderCartInfoDO cartInfo : cartInfoList) { appStoreProductService.incProductStock(cartInfo.getNumber(), cartInfo.getProductId(), cartInfo.getSpec(), 0L, null); } } /** * 处理订单状态 * @param payStatus * @param status * @param refundStatus * @param del * @return */ private String handleOrderStatus(Integer payStatus,Integer status,Integer refundStatus,Integer del) { String statusName = ""; if (del == 1){ statusName = "已删除"; }else if (payStatus == 0 && status == 0) { statusName = "未支付"; } else if (payStatus == 1 && status == 0 && refundStatus == 0) { statusName = "未发货"; } else if (payStatus == 1 && status == 1 && refundStatus == 0) { statusName = "待收货"; } else if (payStatus == 1 && status == 2 && refundStatus == 0) { statusName = "待评价"; } else if (payStatus == 1 && status == 3 && refundStatus == 0) { statusName = "已完成"; } else if (payStatus == 1 && refundStatus == 2) { statusName = "已退款"; } else if (payStatus == 1 && refundStatus == 1) { statusName = "退款中"; } return statusName; } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/dto/CacheDto.java ================================================ package co.yixiang.yshop.module.order.service.storeorder.dto; import lombok.Data; import java.io.Serializable; /** * @ClassName CacheDto * @Author hupeng <610796224@qq.com> * @Date 2023/6/19 **/ @Data public class CacheDto implements Serializable { private PriceGroupDto priceGroup; private OtherDto other; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/dto/ChartDataDto.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.order.service.storeorder.dto; import lombok.Data; /** * @ClassName ChartDataDTO * @Author hupeng <610796224@qq.com> * @Date 2019/11/25 **/ @Data public class ChartDataDto { // @Value("#{target.adminCount}") private Double num; private String time; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/dto/CountDto.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.order.service.storeorder.dto; import lombok.Data; @Data public class CountDto { private String catename; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/dto/OrderCountDto.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.order.service.storeorder.dto; import lombok.Data; import java.util.List; @Data public class OrderCountDto { private List column; private List orderCountDatas; @Data public static class OrderCountData{ private String name; private Integer value; } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/dto/OrderExtendDto.java ================================================ package co.yixiang.yshop.module.order.service.storeorder.dto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.Map; /** * @ClassName OrderExtendDto * @Author hupeng <610796224@qq.com> * @Date 2023/6/19 **/ @Data @AllArgsConstructor @NoArgsConstructor @Builder public class OrderExtendDto implements Serializable { @Schema(description = "唯一的key", required = true) private String key; @Schema(description = "订单ID", required = true) private String orderId; @Schema(description = "微信相关配置", required = true) private Map jsConfig; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/dto/OrderTimeDataDto.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.order.service.storeorder.dto; import lombok.Data; import java.io.Serializable; /** * @ClassName OrderTimeDataDTO * @Author hupeng <610796224@qq.com> * @Date 2019/11/25 **/ @Data public class OrderTimeDataDto implements Serializable { private Double todayPrice; //今日成交额 private Integer todayCount; //今日订单数 private Double proPrice; //昨日成交额 private Integer proCount;//昨日订单数 private Double monthPrice;//本月成交额 private Integer monthCount;//本月订单数 private Integer lastWeekCount;//上周 private Double lastWeekPrice; //上周 private Long userCount; private Long orderCount; private Double priceCount; private Long goodsCount; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/dto/OtherDto.java ================================================ package co.yixiang.yshop.module.order.service.storeorder.dto; import lombok.Data; import java.io.Serializable; /** * @ClassName OtherDto * @Author hupeng <610796224@qq.com> * @Date 2019/10/27 **/ @Data public class OtherDto implements Serializable { //线下包邮 private String offlinePostage; //积分抵扣 private String integralRatio; //最大 private String integralMax; //满多少 private String integralFull; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/dto/PriceGroupDto.java ================================================ package co.yixiang.yshop.module.order.service.storeorder.dto; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import lombok.Data; import java.math.BigDecimal; /** * @ClassName PriceGroup * @Author hupeng <610796224@qq.com> * @Date 2023/6/18 **/ @Data public class PriceGroupDto { private BigDecimal costPrice; private BigDecimal storeFreePostage; private BigDecimal storePostage; private BigDecimal totalPrice; private BigDecimal vipPrice; private BigDecimal payIntegral; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/dto/ProductAttrDto.java ================================================ package co.yixiang.yshop.module.order.service.storeorder.dto; import lombok.Data; import java.io.Serializable; /** * @ClassName ProductAttrDto * @Author hupeng <610796224@qq.com> * @Date 2023/6/27 **/ @Data public class ProductAttrDto implements Serializable { private Long productId; private String sku; private Double price; private String image; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/dto/ProductDto.java ================================================ package co.yixiang.yshop.module.order.service.storeorder.dto; import lombok.Data; import java.io.Serializable; /** * @ClassName ProductVo * @Author hupeng <610796224@qq.com> * @Date 20123/6/27 **/ @Data public class ProductDto implements Serializable { private String image; private Double price; private String storeName; private ProductAttrDto attrInfo; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/dto/StatusDto.java ================================================ package co.yixiang.yshop.module.order.service.storeorder.dto; import lombok.Data; import java.io.Serializable; /** * @ClassName StatusDto * @Author hupeng <610796224@qq.com> * @Date 2024/1/19 **/ @Data public class StatusDto implements Serializable { private String yClass; private String msg; private String payType; private String title; private String type; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/dto/StoreOrderCartInfoDto.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.order.service.storeorder.dto; import lombok.Data; import java.util.Map; /** * @ClassName StoreOrderCartInfo * @Author hupeng <610796224@qq.com> * @Date 2019/10/14 **/ @Data public class StoreOrderCartInfoDto { private Integer id; private Integer oid; private Integer cartId; private String cartInfo; private String unique; private Map cartInfoMap; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/dto/TemplateDto.java ================================================ package co.yixiang.yshop.module.order.service.storeorder.dto; import lombok.*; import java.math.BigDecimal; /** * @ClassName TemplateDTO * @Author hupeng <610796224@qq.com> * @Date 2024/2/17 **/ @Getter @Setter @Builder @AllArgsConstructor @NoArgsConstructor public class TemplateDto { private Double number; private BigDecimal price; private Double first; private BigDecimal firstPrice; private Double yContinue; private BigDecimal continuePrice; private Integer tempId; private Integer cityId; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/dto/YxExpressDto.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.order.service.storeorder.dto; import lombok.Data; import java.io.Serializable; /** * @author hupeng * @date 2020-05-12 */ @Data public class YxExpressDto implements Serializable { /** 快递公司id */ private Integer id; /** 快递公司简称 */ private String code; /** 快递公司全称 */ private String name; /** 排序 */ private Integer sort; /** 是否显示 */ private Integer isShow; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/dto/YxOrderNowOrderStatusDto.java ================================================ package co.yixiang.yshop.module.order.service.storeorder.dto; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import java.io.Serializable; import java.util.Date; /** * @author :LionCity * @date :Created in 2020-05-29 11:16 * @description: * @modified By: * @version: */ @Data public class YxOrderNowOrderStatusDto implements Serializable { @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8") private Date cacheKeyCreateOrder; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8") private Date paySuccess; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8") private Date deliveryGoods; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8") private Date orderVerific; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8") private Date userTakeDelivery; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8") private Date checkOrderOver; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8") private Date applyRefund; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8") private Date refundOrderSuccess; private int size; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/dto/YxStoreOrderCartInfoDto.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.order.service.storeorder.dto; import lombok.Data; import java.io.Serializable; /** * @author hupeng * @date 2020-05-12 */ @Data public class YxStoreOrderCartInfoDto implements Serializable { private Integer id; /** 订单id */ private Integer oid; /** * 订单号 */ private String orderId; /** 购物车id */ private Integer cartId; /** 商品ID */ private Integer productId; /** 购买东西的详细信息 */ private String cartInfo; /** 唯一id */ private String unique; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/dto/YxStoreOrderStatusDto.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.order.service.storeorder.dto; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import org.springframework.format.annotation.DateTimeFormat; import java.io.Serializable; import java.util.Date; /** * @author hupeng * @date 2020-05-12 */ @Data public class YxStoreOrderStatusDto implements Serializable { private Integer id; /** 订单id */ private Integer oid; /** 操作类型 */ private String changeType; /** 操作备注 */ private String changeMessage; /** 操作时间 */ @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8") private Date changeTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeordercartinfo/StoreOrderCartInfoService.java ================================================ package co.yixiang.yshop.module.order.service.storeordercartinfo; import co.yixiang.yshop.module.order.dal.dataobject.storeorder.StoreOrderDO; import co.yixiang.yshop.module.order.dal.dataobject.storeordercartinfo.StoreOrderCartInfoDO; import co.yixiang.yshop.module.product.controller.app.cart.vo.AppStoreCartQueryVo; import com.baomidou.mybatisplus.extension.service.IService; import java.util.List; /** * 订单购物详情 Service 接口 * * @author yshop */ public interface StoreOrderCartInfoService extends IService { /** * 添加购物车商品信息 * @param oid 订单id * @param orderId 订单号 * @param productIds 商品id * @param numbers 商品数量 * @param specs 商品规格 */ void saveCartInfo(Long oid, String orderId,List productIds,List numbers,List specs); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeordercartinfo/StoreOrderCartInfoServiceImpl.java ================================================ package co.yixiang.yshop.module.order.service.storeordercartinfo; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.module.order.dal.dataobject.storeordercartinfo.StoreOrderCartInfoDO; import co.yixiang.yshop.module.order.dal.mysql.storeordercartinfo.StoreOrderCartInfoMapper; import co.yixiang.yshop.module.product.dal.dataobject.storeproduct.StoreProductDO; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattrvalue.StoreProductAttrValueDO; import co.yixiang.yshop.module.product.service.storeproduct.AppStoreProductService; import co.yixiang.yshop.module.product.service.storeproductattrvalue.StoreProductAttrValueService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.util.ArrayList; import java.util.List; /** * 订单购物详情 Service 实现类 * * @author yshop */ @Slf4j @Service @Validated public class StoreOrderCartInfoServiceImpl extends ServiceImpl implements StoreOrderCartInfoService { @Resource private AppStoreProductService appStoreProductService; @Resource private StoreProductAttrValueService storeProductAttrValueService; /** * 添加购物车商品信息 * @param oid 订单id * @param orderId * @param productIds 商品id * @param numbers 商品数量 * @param specs 商品规格 */ @Async @Override public void saveCartInfo(Long oid, String orderId, List productIds,List numbers,List specs) { log.info("==========添加购物车商品信息start==========="); List list = new ArrayList<>(); for (int i = 0;i < productIds.size();i++){ String newSku = StrUtil.replace(specs.get(i),"|",","); StoreProductDO storeProductDO = appStoreProductService.getById(productIds.get(i)); StoreProductAttrValueDO storeProductAttrValue = storeProductAttrValueService .getOne(Wrappers.lambdaQuery() .eq(StoreProductAttrValueDO::getSku, newSku) .eq(StoreProductAttrValueDO::getProductId, productIds.get(i))); StoreOrderCartInfoDO info = new StoreOrderCartInfoDO(); info.setOid(oid); info.setOrderId(orderId); info.setCartId(0L); info.setProductId(Long.valueOf(productIds.get(i))); info.setCartInfo(""); info.setUnique(IdUtil.simpleUUID()); info.setIsAfterSales(1); info.setTitle(storeProductDO.getStoreName()); info.setImage(storeProductDO.getImage()); info.setNumber(Integer.valueOf(numbers.get(i))); info.setSpec(specs.get(i)); info.setPrice(storeProductAttrValue.getPrice()); list.add(info); } this.saveBatch(list); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorderstatus/StoreOrderStatusService.java ================================================ package co.yixiang.yshop.module.order.service.storeorderstatus; import co.yixiang.yshop.module.order.dal.dataobject.storeorderstatus.StoreOrderStatusDO; import com.baomidou.mybatisplus.extension.service.IService; /** * 订单操作记录 Service 接口 * * @author yshop */ public interface StoreOrderStatusService extends IService { /** * 添加订单操作记录 * @param oid 订单id * @param changetype 操作状态 * @param changeMessage 操作内容 */ void create(Long uid,Long oid,String changetype,String changeMessage); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorderstatus/StoreOrderStatusServiceImpl.java ================================================ package co.yixiang.yshop.module.order.service.storeorderstatus; import co.yixiang.yshop.module.order.dal.dataobject.storeorderstatus.StoreOrderStatusDO; import co.yixiang.yshop.module.order.dal.mysql.storeorderstatus.StoreOrderStatusMapper; import co.yixiang.yshop.module.order.service.storeorder.AsyncStoreOrderService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; /** * 订单操作记录 Service 实现类 * * @author yshop */ @Service @Validated public class StoreOrderStatusServiceImpl extends ServiceImpl implements StoreOrderStatusService { @Resource private StoreOrderStatusMapper storeOrderStatusMapper; @Resource private AsyncStoreOrderService asyncStoreOrderService; /** * 添加订单操作记录 * @param oid 订单id * @param changetype 操作状态 * @param changeMessage 操作内容 */ @Override @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public void create(Long uid,Long oid, String changetype, String changeMessage) { StoreOrderStatusDO storeOrderStatus = new StoreOrderStatusDO(); storeOrderStatus.setOid(oid); storeOrderStatus.setChangeType(changetype); storeOrderStatus.setChangeMessage(changeMessage); this.baseMapper.insert(storeOrderStatus); //异步统计 asyncStoreOrderService.orderData(uid); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/resources/mapper/express/ExpressMapper.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/resources/mapper/storeorder/StoreOrderMapper.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/resources/mapper/storeordercartinfo/StoreOrderCartInfoMapper.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/resources/mapper/storeorderstatus/StoreOrderStatusMapper.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-api/pom.xml ================================================ 4.0.0 co.yixiang.boot yshop-module-mall ${revision} yshop-module-product-api jar ${project.artifactId} product 模块 API,暴露给其它模块调用 co.yixiang.boot yshop-common org.springframework.boot spring-boot-starter-validation true io.swagger.core.v3 swagger-annotations 2.2.8 compile ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-api/src/main/java/co/yixiang/yshop/module/product/api/package-info.java ================================================ /** * 占位 */ package co.yixiang.yshop.module.product.api; ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-api/src/main/java/co/yixiang/yshop/module/product/api/product/ProductApi.java ================================================ package co.yixiang.yshop.module.product.api.product; import java.util.List; public interface ProductApi { // /** // * 检测商品/秒杀/砍价/拼团库存 // * @param uid 用户ID // * @param productId 产品ID // * @param cartNum 购买数量 // * @param productAttrUnique 商品属性Unique // */ // void checkProductStock(Long uid, Long productId, Integer cartNum, String productAttrUnique); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-api/src/main/java/co/yixiang/yshop/module/product/api/property/ProductPropertyValueApi.java ================================================ package co.yixiang.yshop.module.product.api.property; import co.yixiang.yshop.module.product.api.property.dto.ProductPropertyValueDetailRespDTO; import java.util.Collection; import java.util.List; /** * 商品属性值 API 接口 * * @author yshop */ public interface ProductPropertyValueApi { /** * 根据编号数组,获得属性值列表 * * @param ids 编号数组 * @return 属性值明细列表 */ List getPropertyValueDetailList(Collection ids); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-api/src/main/java/co/yixiang/yshop/module/product/api/property/dto/ProductPropertyValueDetailRespDTO.java ================================================ package co.yixiang.yshop.module.product.api.property.dto; import lombok.Data; /** * 商品属性项的明细 Response DTO * * @author yshop */ @Data public class ProductPropertyValueDetailRespDTO { /** * 属性的编号 */ private Long propertyId; /** * 属性的名称 */ private String propertyName; /** * 属性值的编号 */ private Long valueId; /** * 属性值的名称 */ private String valueName; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-api/src/main/java/co/yixiang/yshop/module/product/enums/ErrorCodeConstants.java ================================================ package co.yixiang.yshop.module.product.enums; import co.yixiang.yshop.framework.common.exception.ErrorCode; /** * Product 错误码枚举类 * * product 系统,使用 1-008-000-000 段 */ public interface ErrorCodeConstants { ErrorCode PARAM_ERROR = new ErrorCode(1008001000, "参数非法"); // ========== 商品分类相关 1008001000 ============ ErrorCode CATEGORY_NOT_EXISTS = new ErrorCode(1008001000, "商品分类不存在"); ErrorCode CATEGORY_PARENT_NOT_EXISTS = new ErrorCode(1008001001, "父分类不存在"); ErrorCode CATEGORY_PARENT_NOT_FIRST_LEVEL = new ErrorCode(1008001002, "分类最多只有二级哦"); ErrorCode CATEGORY_EXISTS_CHILDREN = new ErrorCode(1008001003, "存在子分类,无法删除"); ErrorCode CATEGORY_DISABLED = new ErrorCode(1008001004, "商品分类({})已禁用,无法使用"); // ========== 商品品牌相关编号 1008002000 ========== ErrorCode BRAND_NOT_EXISTS = new ErrorCode(1008002000, "品牌不存在"); ErrorCode BRAND_DISABLED = new ErrorCode(1008002001, "品牌不存在"); ErrorCode BRAND_NAME_EXISTS = new ErrorCode(1008002002, "品牌名称已存在"); // ========== 商品 1008003000 ========== ErrorCode STORE_PRODUCT_NOT_EXISTS = new ErrorCode(1008003000, "商品不存在"); ErrorCode STORE_PRODUCT_ATTR_NOT_EXISTS = new ErrorCode(1008003001, "商品属性不存在"); ErrorCode STORE_PRODUCT_ATTR_RESULT_NOT_EXISTS = new ErrorCode(1008003002, "商品属性详情不存在"); ErrorCode STORE_PRODUCT_ATTR_VALUE_NOT_EXISTS = new ErrorCode(1008003003, "商品属性值不存在"); ErrorCode STORE_PRODUCT_RULE_NOT_EXISTS = new ErrorCode(1008003004, "商品规则值(规格)不存在"); ErrorCode STORE_PRODUCT_RULE_NEED = new ErrorCode(1008003005, "请至少添加一个规格值哦"); ErrorCode STORE_PRODUCT_RULE_RE = new ErrorCode(1008003006, "规格值里包含'-',请重新添加"); ErrorCode STORE_PRODUCT_STOCK_ERROR = new ErrorCode(1008003007, "库存不能低于0"); ErrorCode STORE_PRODUCT_SLIDER_ERROR = new ErrorCode(1008003008, "请上传轮播图"); ErrorCode STORE_PRODUCT_ATTR_NEED = new ErrorCode(1008003009, "请设置至少一个属性"); // ========== 运费模板 1008004000 ========== ErrorCode SHIPPING_TEMPLATES_NOT_EXISTS = new ErrorCode(1008004000, "运费模板不存在"); ErrorCode SHIPPING_TEMPLATES_FREE_NOT_EXISTS = new ErrorCode(1008004001, "请添加包邮区域"); ErrorCode SHIPPING_TEMPLATES_REGION_NOT_EXISTS = new ErrorCode(1008004002, "请添加区域"); ErrorCode SHIPPING_TEMPLATES_FREE_NEED = new ErrorCode(1008004000, "请指定包邮地区"); // ========== 评论 1008005000 ========== ErrorCode STORE_PRODUCT_REPLY_NOT_EXISTS = new ErrorCode(1008005000, "评论不存在"); ErrorCode STORE_PRODUCT_RELATION_NOT_EXISTS = new ErrorCode(1008005001, "商品点赞和收藏不存在"); ErrorCode STORE_PRODUCT_RELATION_EXISTS = new ErrorCode(1008005002, "已经收藏过"); ErrorCode PRODUCT_STOCK_LESS = new ErrorCode(1008005003, "商品库存不足"); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-api/src/main/java/co/yixiang/yshop/module/product/enums/ProductConstants.java ================================================ package co.yixiang.yshop.module.product.enums; import co.yixiang.yshop.framework.common.exception.ErrorCode; /** * Product 错误码枚举类 * * product 系统,使用 1-008-000-000 段 */ public interface ProductConstants { String NO_COMMENT_CONTENT = "此用户没有填写评价"; String FREE_POSTAGE = "全国包邮"; String POSTAGE_TEMP_NOT = "运费模板不存在"; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-api/src/main/java/co/yixiang/yshop/module/product/enums/comment/ProductCommentAuditStatusEnum.java ================================================ package co.yixiang.yshop.module.product.enums.comment; import co.yixiang.yshop.framework.common.core.IntArrayValuable; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.Arrays; /** * 商品评论的审批状态枚举 * * @author yshop */ @Getter @AllArgsConstructor public enum ProductCommentAuditStatusEnum implements IntArrayValuable { NONE(1, "待审核"), APPROVE(2, "审批通过"), REJECT(2, "审批不通过"),; public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ProductCommentAuditStatusEnum::getStatus).toArray(); /** * 审批状态 */ private final Integer status; /** * 状态名 */ private final String name; @Override public int[] array() { return ARRAYS; } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-api/src/main/java/co/yixiang/yshop/module/product/enums/delivery/DeliveryTypeEnum.java ================================================ package co.yixiang.yshop.module.product.enums.delivery; import co.yixiang.yshop.framework.common.core.IntArrayValuable; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.Arrays; /** * 配送方式枚举 * * @author yshop */ @Getter @AllArgsConstructor public enum DeliveryTypeEnum implements IntArrayValuable { // TODO yshop:英文单词,需要再想下; EXPRESS(1, "快递发货"), USER(2, "用户自提"),; public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(DeliveryTypeEnum::getMode).toArray(); /** * 配送方式 */ private final Integer mode; /** * 状态名 */ private final String name; @Override public int[] array() { return ARRAYS; } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-api/src/main/java/co/yixiang/yshop/module/product/enums/group/ProductGroupStyleEnum.java ================================================ package co.yixiang.yshop.module.product.enums.group; import co.yixiang.yshop.framework.common.core.IntArrayValuable; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.Arrays; /** * 商品分组的样式枚举 * * @author yshop */ @Getter @AllArgsConstructor public enum ProductGroupStyleEnum implements IntArrayValuable { ONE(1, "每列一个"), TWO(2, "每列两个"), THREE(2, "每列三个"),; public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ProductGroupStyleEnum::getStyle).toArray(); /** * 列表样式 */ private final Integer style; /** * 状态名 */ private final String name; @Override public int[] array() { return ARRAYS; } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-api/src/main/java/co/yixiang/yshop/module/product/enums/product/DefaultEnum.java ================================================ package co.yixiang.yshop.module.product.enums.product; import lombok.AllArgsConstructor; import lombok.Getter; /** * @author hupeng * 产品相关规格类型枚举 */ @Getter @AllArgsConstructor public enum DefaultEnum { DEFAULT_0(0,"默认值0"), DEFAULT_1(1,"默认值1"); private Integer value; private String desc; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-api/src/main/java/co/yixiang/yshop/module/product/enums/product/ProductEnum.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.product.enums.product; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.stream.Stream; /** * @author hupeng * 产品相关枚举 */ @Getter @AllArgsConstructor public enum ProductEnum { TYPE_1(1,"精品推荐"), TYPE_2(2,"热门榜单"), TYPE_3(3,"首发新品"), TYPE_4(4,"猜你喜欢"); private Integer value; private String desc; public static ProductEnum toType(int value) { return Stream.of(ProductEnum.values()) .filter(p -> p.value == value) .findAny() .orElse(null); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-api/src/main/java/co/yixiang/yshop/module/product/enums/product/ProductTypeEnum.java ================================================ package co.yixiang.yshop.module.product.enums.product; import lombok.AllArgsConstructor; import lombok.Getter; /** * @author hupeng * 产品类型枚举 */ @Getter @AllArgsConstructor public enum ProductTypeEnum { PINK("pink","拼团"), SECKILL("seckill","秒杀"), COMBINATION("combination","拼团产品"); private String value; private String desc; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-api/src/main/java/co/yixiang/yshop/module/product/enums/product/RelationCateEnum.java ================================================ package co.yixiang.yshop.module.product.enums.product; import lombok.AllArgsConstructor; import lombok.Getter; /** * @author hupeng * 产品收藏类型枚举 */ @Getter @AllArgsConstructor public enum RelationCateEnum { COMMON("common","普通商品"), SECKILL("seckill","秒杀商品"), BARGAIN("bargain","砍价商品"), PINK("pink","拼团商品"); private String value; private String desc; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-api/src/main/java/co/yixiang/yshop/module/product/enums/product/RelationEnum.java ================================================ package co.yixiang.yshop.module.product.enums.product; import lombok.AllArgsConstructor; import lombok.Getter; /** * @author hupeng * 产品收藏类型枚举 */ @Getter @AllArgsConstructor public enum RelationEnum { COLLECT("collect","收藏"), FOOT("foot","足迹"); private String value; private String desc; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-api/src/main/java/co/yixiang/yshop/module/product/enums/product/ScoreEnum.java ================================================ package co.yixiang.yshop.module.product.enums.product; import lombok.AllArgsConstructor; import lombok.Getter; /** * @author hupeng * 产品相关规格类型枚举 */ @Getter @AllArgsConstructor public enum ScoreEnum { DEFAULT_0(0,"0分数"), DEFAULT_1(1,"1分数"), DEFAULT_2(2,"2分数"), DEFAULT_3(3,"3分数"), DEFAULT_4(4,"4分数"), DEFAULT_5(5,"5分数"); private Integer value; private String desc; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-api/src/main/java/co/yixiang/yshop/module/product/enums/product/SpecTypeEnum.java ================================================ package co.yixiang.yshop.module.product.enums.product; import lombok.AllArgsConstructor; import lombok.Getter; /** * @author hupeng * 产品相关规格类型枚举 */ @Getter @AllArgsConstructor public enum SpecTypeEnum { TYPE_0(0,"单规格"), TYPE_1(1,"多规格"); private Integer value; private String desc; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-api/src/main/java/co/yixiang/yshop/module/product/enums/spu/ProductSpuSpecTypeEnum.java ================================================ package co.yixiang.yshop.module.product.enums.spu; import co.yixiang.yshop.framework.common.core.IntArrayValuable; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.Arrays; /** * 商品 SPU 规格类型 * * @author yshop */ @Getter @AllArgsConstructor public enum ProductSpuSpecTypeEnum implements IntArrayValuable { RECYCLE(1, "统一规格"), DISABLE(2, "多规格"); public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ProductSpuSpecTypeEnum::getType).toArray(); /** * 规格类型 */ private final Integer type; /** * 规格名称 */ private final String name; @Override public int[] array() { return ARRAYS; } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-api/src/main/java/co/yixiang/yshop/module/product/enums/spu/ProductSpuStatusEnum.java ================================================ package co.yixiang.yshop.module.product.enums.spu; import co.yixiang.yshop.framework.common.core.IntArrayValuable; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.Arrays; /** * 商品 SPU 状态 * * @author yshop */ @Getter @AllArgsConstructor public enum ProductSpuStatusEnum implements IntArrayValuable { RECYCLE(-1, "回收站"), DISABLE(0, "下架"), ENABLE(1, "上架"),; public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ProductSpuStatusEnum::getStatus).toArray(); /** * 状态 */ private final Integer status; /** * 状态名 */ private final String name; @Override public int[] array() { return ARRAYS; } /** * 判断是否处于【上架】状态 * * @param status 状态 * @return 是否处于【上架】状态 */ public static boolean isEnable(Integer status) { return ENABLE.getStatus().equals(status); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/pom.xml ================================================ co.yixiang.boot yshop-module-mall ${revision} 4.0.0 yshop-module-product-biz jar ${project.artifactId} product 模块,主要实现商品相关功能 例如:品牌、商品分类、spu、sku等功能。 co.yixiang.boot yshop-module-product-api ${revision} co.yixiang.boot yshop-module-store-biz ${revision} co.yixiang.boot yshop-spring-boot-starter-web co.yixiang.boot yshop-spring-boot-starter-security co.yixiang.boot yshop-spring-boot-starter-mybatis co.yixiang.boot yshop-spring-boot-starter-test co.yixiang.boot yshop-spring-boot-starter-excel ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/api/package-info.java ================================================ package co.yixiang.yshop.module.product.api; ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/api/product/ProductApiImpl.java ================================================ package co.yixiang.yshop.module.product.api.product; import co.yixiang.yshop.framework.common.enums.ShopCommonEnum; import co.yixiang.yshop.framework.common.exception.ErrorCode; import co.yixiang.yshop.module.product.dal.dataobject.storeproduct.StoreProductDO; import co.yixiang.yshop.module.product.enums.product.ProductTypeEnum; import co.yixiang.yshop.module.product.service.storeproduct.AppStoreProductService; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import java.util.Date; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.product.enums.ErrorCodeConstants.STORE_PRODUCT_NOT_EXISTS; @Service public class ProductApiImpl implements ProductApi { // // @Resource // private AppStoreProductService appStoreProductService; // /** // * 检测商品库存 // * // * @param uid 用户ID // * @param productId 产品ID // * @param cartNum 购买数量 // * @param productAttrUnique 商品属性Unique // * @param combinationId 拼团产品ID // */ // @Override // public void checkProductStock(Long uid, Long productId, Integer cartNum, String productAttrUnique) { // StoreProductDO product = appStoreProductService // .lambdaQuery().eq(StoreProductDO::getId, productId) // .eq(StoreProductDO::getIsShow, ShopCommonEnum.SHOW_1.getValue()) // .one(); // if (product == null) { // throw exception(STORE_PRODUCT_NOT_EXISTS); // } // // int stock = appStoreProductService.getProductStock(productId, productAttrUnique, ProductTypeEnum.PINK.getValue()); // if (stock < cartNum) { // throw exception(new ErrorCode(1008003010, product.getStoreName() + "库存不足" + cartNum)); // } // // } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/category/ProductCategoryController.java ================================================ package co.yixiang.yshop.module.product.controller.admin.category; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import co.yixiang.yshop.module.product.controller.admin.category.vo.ProductCategoryCreateReqVO; import co.yixiang.yshop.module.product.controller.admin.category.vo.ProductCategoryListReqVO; import co.yixiang.yshop.module.product.controller.admin.category.vo.ProductCategoryRespVO; import co.yixiang.yshop.module.product.controller.admin.category.vo.ProductCategoryUpdateReqVO; import co.yixiang.yshop.module.product.convert.category.ProductCategoryConvert; import co.yixiang.yshop.module.product.dal.dataobject.category.ProductCategoryDO; import co.yixiang.yshop.module.product.service.category.ProductCategoryService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import java.util.Comparator; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 商品分类") @RestController @RequestMapping("/product/category") @Validated public class ProductCategoryController { @Resource private ProductCategoryService categoryService; @PostMapping("/create") @Operation(summary = "创建商品分类") @PreAuthorize("@ss.hasPermission('product:category:create')") public CommonResult createCategory(@Valid @RequestBody ProductCategoryCreateReqVO createReqVO) { return success(categoryService.createCategory(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新商品分类") @PreAuthorize("@ss.hasPermission('product:category:update')") public CommonResult updateCategory(@Valid @RequestBody ProductCategoryUpdateReqVO updateReqVO) { categoryService.updateCategory(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除商品分类") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('product:category:delete')") public CommonResult deleteCategory(@RequestParam("id") Long id) { categoryService.deleteCategory(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得商品分类") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('product:category:query')") public CommonResult getCategory(@RequestParam("id") Long id) { ProductCategoryDO category = categoryService.getCategory(id); return success(ProductCategoryConvert.INSTANCE.convert(category)); } @GetMapping("/list") @Operation(summary = "获得商品分类列表") @PreAuthorize("@ss.hasPermission('product:category:query')") public CommonResult> getCategoryList(@Valid ProductCategoryListReqVO treeListReqVO) { List list = categoryService.getEnableCategoryList(treeListReqVO); list.sort(Comparator.comparing(ProductCategoryDO::getSort)); return success(ProductCategoryConvert.INSTANCE.convertList(list)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/category/vo/ProductCategoryBaseVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.category.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; /** * 商品分类 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class ProductCategoryBaseVO { /** * 店铺id */ private Integer shopId; /** * 店铺名称 */ private String shopName; @Schema(description = "父分类编号", required = true, example = "1") //@NotNull(message = "父分类编号不能为空") private Long parentId; @Schema(description = "分类名称", required = true, example = "办公文具") @NotBlank(message = "分类名称不能为空") private String name; @Schema(description = "分类图片", required = true) @NotBlank(message = "分类图片不能为空") private String picUrl; @Schema(description = "分类排序", required = true, example = "1") private Integer sort; @Schema(description = "分类描述", required = true, example = "描述") private String description; @Schema(description = "开启状态", required = true, example = "0") @NotNull(message = "开启状态不能为空") private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/category/vo/ProductCategoryCreateReqVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.category.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @Schema(description = "管理后台 - 商品分类创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ProductCategoryCreateReqVO extends ProductCategoryBaseVO { } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/category/vo/ProductCategoryListReqVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.category.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "管理后台 - 商品分类列表查询 Request VO") @Data public class ProductCategoryListReqVO { @Schema(description = "分类名称", example = "办公文具") private String name; @Schema(description = "店铺名称", example = "办公文具") private String shopName; private Integer shopId; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/category/vo/ProductCategoryRespVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.category.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import java.time.LocalDateTime; @Schema(description = "管理后台 - 商品分类 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ProductCategoryRespVO extends ProductCategoryBaseVO { @Schema(description = "分类编号", required = true, example = "2") private Long id; @Schema(description = "创建时间", required = true) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/category/vo/ProductCategoryUpdateReqVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.category.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 商品分类更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ProductCategoryUpdateReqVO extends ProductCategoryBaseVO { @Schema(description = "分类编号", required = true, example = "2") @NotNull(message = "分类编号不能为空") private Long id; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/storeproduct/StoreProductController.java ================================================ package co.yixiang.yshop.module.product.controller.admin.storeproduct; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.product.controller.admin.storeproduct.vo.*; import co.yixiang.yshop.module.product.convert.storeproduct.StoreProductConvert; import co.yixiang.yshop.module.product.dal.dataobject.storeproduct.StoreProductDO; import co.yixiang.yshop.module.product.service.storeproduct.StoreProductService; import co.yixiang.yshop.module.product.service.storeproduct.dto.StoreProductDto; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import java.io.IOException; import java.util.Collection; import java.util.List; import java.util.Map; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 商品") @RestController @RequestMapping("/product/store-product") @Validated public class StoreProductController { @Resource private StoreProductService storeProductService; @PostMapping("/create") @Operation(summary = "创建商品") @PreAuthorize("@ss.hasPermission('shop:store-product:create')") public CommonResult createStoreProduct(@Validated @RequestBody StoreProductDto storeProductDto) { storeProductService.insertAndEditYxStoreProduct(storeProductDto); return success(true); } @PutMapping("/update") @Operation(summary = "更新商品") @PreAuthorize("@ss.hasPermission('shop:store-product:update')") public CommonResult updateStoreProduct(@Valid @RequestBody StoreProductUpdateReqVO updateReqVO) { storeProductService.updateStoreProduct(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除商品") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('shop:store-product:delete')") public CommonResult deleteStoreProduct(@RequestParam("id") Long id) { storeProductService.deleteStoreProduct(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得商品") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('shop:store-product:query')") public CommonResult getStoreProduct(@RequestParam("id") Long id) { StoreProductDO storeProduct = storeProductService.getStoreProduct(id); return success(StoreProductConvert.INSTANCE.convert(storeProduct)); } @GetMapping("/list") @Operation(summary = "获得商品列表") @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") @PreAuthorize("@ss.hasPermission('shop:store-product:query')") public CommonResult> getStoreProductList(@RequestParam("ids") Collection ids) { List list = storeProductService.getStoreProductList(ids); return success(StoreProductConvert.INSTANCE.convertList(list)); } @GetMapping("/page") @Operation(summary = "获得商品分页") @PreAuthorize("@ss.hasPermission('shop:store-product:query')") public CommonResult> getStoreProductPage(@Valid StoreProductPageReqVO pageVO) { PageResult pageResult = storeProductService.getStoreProductPage(pageVO); return success(StoreProductConvert.INSTANCE.convertPage(pageResult)); } @GetMapping("/export-excel") @Operation(summary = "导出商品 Excel") @PreAuthorize("@ss.hasPermission('shop:store-product:export')") public void exportStoreProductExcel(@Valid StoreProductExportReqVO exportReqVO, HttpServletResponse response) throws IOException { List list = storeProductService.getStoreProductList(exportReqVO); // 导出 Excel List datas = StoreProductConvert.INSTANCE.convertList02(list); ExcelUtils.write(response, "商品.xls", "数据", StoreProductExcelVO.class, datas); } @Operation(summary = "获取商品信息") @GetMapping(value = "/info/{id}") public CommonResult> info(@PathVariable Long id){ return success(storeProductService.getProductInfo(id)); } @Operation(summary = "生成属性") @PostMapping(value = "/isFormatAttr/{id}") public CommonResult> isFormatAttr(@PathVariable Long id,@RequestBody String jsonStr){ return success(storeProductService.getFormatAttr(id,jsonStr,false)); } @Operation(summary = "商品上架/下架") @GetMapping(value = "/sale") public CommonResult onSale(@RequestParam("id") Long id,@RequestParam("type") int status){ storeProductService.onSale(id,status); return success(true); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/storeproduct/vo/StoreProductBaseVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.storeproduct.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; /** * 商品 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class StoreProductBaseVO { @Schema(description = "商品图片", required = true) @NotNull(message = "商品图片不能为空") private String image; private String shopName; private Integer shopId; @Schema(description = "商品名称", required = true, example = "张三") @NotNull(message = "商品名称不能为空") private String storeName; @Schema(description = "商品价格", required = true, example = "18735") @NotNull(message = "商品价格不能为空") private BigDecimal price; @Schema(description = "单位名", example = "李四") private String unitName; @Schema(description = "销量") private Integer sales; @Schema(description = "库存") private Integer stock; @Schema(description = "上架状态") private Integer isShow; @Schema(description = "是否包邮") private Integer isPostage; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/storeproduct/vo/StoreProductCreateReqVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.storeproduct.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; @Schema(description = "管理后台 - 商品创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StoreProductCreateReqVO extends StoreProductBaseVO { @Schema(description = "轮播图", required = true) @NotNull(message = "轮播图不能为空") private String sliderImage; @Schema(description = "商品简介", required = true) @NotNull(message = "商品简介不能为空") private String storeInfo; @Schema(description = "关键字", required = true) @NotNull(message = "关键字不能为空") private String keyword; @Schema(description = "产品条码(一维码)") private String barCode; @Schema(description = "分类id", required = true, example = "4928") @NotNull(message = "分类id不能为空") private String cateId; @Schema(description = "会员价格", example = "18248") private BigDecimal vipPrice; @Schema(description = "市场价", example = "14818") private BigDecimal otPrice; @Schema(description = "邮费") private BigDecimal postage; @Schema(description = "排序") private Short sort; @Schema(description = "状态(0:未上架,1:上架)") private Integer isShow; @Schema(description = "是否热卖") private Boolean isHot; @Schema(description = "是否优惠") private Boolean isBenefit; @Schema(description = "是否精品") private Boolean isBest; @Schema(description = "是否新品") private Integer isNew; @Schema(description = "产品描述", example = "你说的对") private String description; @Schema(description = "商户是否代理 0不可代理1可代理") private Byte merUse; @Schema(description = "获得积分") private BigDecimal giveIntegral; @Schema(description = "成本价") private BigDecimal cost; @Schema(description = "秒杀状态 0 未开启 1已开启") private Byte isSeckill; @Schema(description = "砍价状态 0未开启 1开启") private Byte isBargain; @Schema(description = "是否优品推荐") private Boolean isGood; @Schema(description = "虚拟销量") private Integer ficti; @Schema(description = "浏览量") private Integer browse; @Schema(description = "产品二维码地址(用户小程序海报)", required = true) private String codePath; @Schema(description = "是否单独分佣") private Boolean isSub; @Schema(description = "运费模板ID", example = "18065") private Integer tempId; @Schema(description = "规格 0单 1多", example = "1") private Integer specType; @Schema(description = "是开启积分兑换") private Byte isIntegral; @Schema(description = "需要多少积分兑换 只在开启积分兑换时生效") private Integer integral; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/storeproduct/vo/StoreProductExcelVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.storeproduct.vo; import com.alibaba.excel.annotation.ExcelProperty; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; /** * 商品 Excel VO * * @author yshop */ @Data public class StoreProductExcelVO { @ExcelProperty("商品id") private Long id; @ExcelProperty("商品图片") private String image; @ExcelProperty("商品名称") private String storeName; @ExcelProperty("商品价格") private BigDecimal price; @ExcelProperty("单位名") private String unitName; @ExcelProperty("销量") private Integer sales; @ExcelProperty("库存") private Integer stock; @ExcelProperty("添加时间") private LocalDateTime createTime; @ExcelProperty("是否包邮") private Integer isPostage; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/storeproduct/vo/StoreProductExportReqVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.storeproduct.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "管理后台 - 商品 Excel 导出 Request VO,参数和 StoreProductPageReqVO 是一致的") @Data public class StoreProductExportReqVO { @Schema(description = "商品名称", example = "张三") private String storeName; @Schema(description = "是否包邮") private Byte isPostage; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/storeproduct/vo/StoreProductPageReqVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.storeproduct.vo; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import java.util.List; @Schema(description = "管理后台 - 商品分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StoreProductPageReqVO extends PageParam { @Schema(description = "商品名称", example = "张三") private String storeName; private String shopName; @Schema(description = "是否包邮") private Byte isPostage; @Schema(description = "上下架", example = "1") private String isShow; @Schema(description = "库存售罄", example = "0") private String stock; @Schema(description = "库存售罄", example = "0") private String cateId; private List catIds; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/storeproduct/vo/StoreProductRespVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.storeproduct.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; @Schema(description = "管理后台 - 商品 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StoreProductRespVO extends StoreProductBaseVO { @Schema(description = "商品id", required = true, example = "1175") private Long id; @Schema(description = "添加时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/storeproduct/vo/StoreProductUpdateReqVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.storeproduct.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; @Schema(description = "管理后台 - 商品更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StoreProductUpdateReqVO extends StoreProductBaseVO { @Schema(description = "商品id", required = true, example = "1175") @NotNull(message = "商品id不能为空") private Long id; @Schema(description = "轮播图", required = true) @NotNull(message = "轮播图不能为空") private String sliderImage; @Schema(description = "商品简介", required = true) @NotNull(message = "商品简介不能为空") private String storeInfo; @Schema(description = "关键字", required = true) @NotNull(message = "关键字不能为空") private String keyword; @Schema(description = "产品条码(一维码)") private String barCode; @Schema(description = "分类id", required = true, example = "4928") @NotNull(message = "分类id不能为空") private String cateId; @Schema(description = "会员价格", example = "18248") private BigDecimal vipPrice; @Schema(description = "市场价", example = "14818") private BigDecimal otPrice; @Schema(description = "邮费") private BigDecimal postage; @Schema(description = "排序") private Short sort; @Schema(description = "状态(0:未上架,1:上架)") private Integer isShow; @Schema(description = "是否热卖") private Boolean isHot; @Schema(description = "是否优惠") private Boolean isBenefit; @Schema(description = "是否精品") private Boolean isBest; @Schema(description = "是否新品") private Integer isNew; @Schema(description = "产品描述", example = "你说的对") private String description; @Schema(description = "商户是否代理 0不可代理1可代理") private Byte merUse; @Schema(description = "获得积分") private BigDecimal giveIntegral; @Schema(description = "成本价") private BigDecimal cost; @Schema(description = "秒杀状态 0 未开启 1已开启") private Byte isSeckill; @Schema(description = "砍价状态 0未开启 1开启") private Byte isBargain; @Schema(description = "是否优品推荐") private Boolean isGood; @Schema(description = "虚拟销量") private Integer ficti; @Schema(description = "浏览量") private Integer browse; @Schema(description = "产品二维码地址(用户小程序海报)", required = true) @NotNull(message = "产品二维码地址(用户小程序海报)不能为空") private String codePath; @Schema(description = "是否单独分佣") private Boolean isSub; @Schema(description = "运费模板ID", example = "18065") private Integer tempId; @Schema(description = "规格 0单 1多", example = "1") private Integer specType; @Schema(description = "是开启积分兑换") private Byte isIntegral; @Schema(description = "需要多少积分兑换 只在开启积分兑换时生效") private Integer integral; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/storeproductreply/StoreProductReplyController.java ================================================ package co.yixiang.yshop.module.product.controller.admin.storeproductreply; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.product.controller.admin.storeproductreply.vo.StoreProductReplyPageReqVO; import co.yixiang.yshop.module.product.controller.admin.storeproductreply.vo.StoreProductReplyRespVO; import co.yixiang.yshop.module.product.controller.admin.storeproductreply.vo.StoreProductReplyUpdateReqVO; import co.yixiang.yshop.module.product.controller.app.product.vo.AppStoreProductReplyQueryVo; import co.yixiang.yshop.module.product.convert.storeproductreply.StoreProductReplyConvert; import co.yixiang.yshop.module.product.dal.dataobject.storeproductreply.StoreProductReplyDO; import co.yixiang.yshop.module.product.service.storeproductreply.StoreProductReplyService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 评论") @RestController @RequestMapping("/product/store-product-reply") @Validated public class StoreProductReplyController { @Resource private StoreProductReplyService storeProductReplyService; @PutMapping("/update") @Operation(summary = "更新评论") @PreAuthorize("@ss.hasPermission('product:store-product-reply:update')") public CommonResult updateStoreProductReply(@Valid @RequestBody StoreProductReplyUpdateReqVO updateReqVO) { storeProductReplyService.updateStoreProductReply(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除评论") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('product:store-product-reply:delete')") public CommonResult deleteStoreProductReply(@RequestParam("id") Long id) { storeProductReplyService.deleteStoreProductReply(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得评论") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('product:store-product-reply:query')") public CommonResult getStoreProductReply(@RequestParam("id") Long id) { StoreProductReplyDO storeProductReply = storeProductReplyService.getStoreProductReply(id); return success(StoreProductReplyConvert.INSTANCE.convert(storeProductReply)); } @GetMapping("/page") @Operation(summary = "获得评论分页") @PreAuthorize("@ss.hasPermission('product:store-product-reply:query')") public CommonResult> getStoreProductReplyPage(@Valid StoreProductReplyPageReqVO pageVO) { PageResult pageResult = storeProductReplyService.getStoreProductReplyPage(pageVO); return success(pageResult); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/storeproductreply/vo/StoreProductReplyBaseVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.storeproductreply.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import java.time.LocalDateTime; import jakarta.validation.constraints.*; import org.springframework.format.annotation.DateTimeFormat; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; /** * 评论 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class StoreProductReplyBaseVO { @Schema(description = "用户ID", required = true, example = "12573") @NotNull(message = "用户ID不能为空") private Long uid; @Schema(description = "订单ID", required = true, example = "5646") @NotNull(message = "订单ID不能为空") private Long oid; @Schema(description = "唯一id", required = true) @NotNull(message = "唯一id不能为空") private String unique; @Schema(description = "产品id", required = true, example = "23509") @NotNull(message = "产品id不能为空") private Long productId; @Schema(description = "某种商品类型(普通商品、秒杀商品)", required = true, example = "2") @NotNull(message = "某种商品类型(普通商品、秒杀商品)不能为空") private String replyType; @Schema(description = "商品分数", required = true) @NotNull(message = "商品分数不能为空") private Integer productScore; @Schema(description = "服务分数", required = true) @NotNull(message = "服务分数不能为空") private Integer serviceScore; @Schema(description = "评论内容", required = true) @NotNull(message = "评论内容不能为空") private String comment; @Schema(description = "评论图片", required = true) @NotNull(message = "评论图片不能为空") private String pics; @Schema(description = "管理员回复内容") private String merchantReplyContent; @Schema(description = "管理员回复时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime merchantReplyTime; @Schema(description = "0未回复1已回复", required = true) @NotNull(message = "0未回复1已回复不能为空") private Integer isReply; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/storeproductreply/vo/StoreProductReplyPageReqVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.storeproductreply.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 评论分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StoreProductReplyPageReqVO extends PageParam { @Schema(description = "用户昵称", example = "12573") private String nickname; @Schema(description = "用户ID", example = "12573") private Long uid; @Schema(description = "订单ID", example = "5646") private Long oid; @Schema(description = "唯一id") private String unique; @Schema(description = "产品id", example = "23509") private Long productId; @Schema(description = "某种商品类型(普通商品、秒杀商品)", example = "2") private String replyType; @Schema(description = "商品分数") private Boolean productScore; @Schema(description = "服务分数") private Boolean serviceScore; @Schema(description = "评论内容") private String comment; @Schema(description = "评论图片") private String pics; @Schema(description = "评论时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; @Schema(description = "管理员回复内容") private String merchantReplyContent; @Schema(description = "管理员回复时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] merchantReplyTime; @Schema(description = "0未回复1已回复") private Boolean isReply; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/storeproductreply/vo/StoreProductReplyRespVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.storeproductreply.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; @Schema(description = "管理后台 - 评论 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StoreProductReplyRespVO extends StoreProductReplyBaseVO { @Schema(description = "评论ID", required = true, example = "7419") private Long id; @Schema(description = "评论时间", required = true) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/storeproductreply/vo/StoreProductReplyUpdateReqVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.storeproductreply.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 评论更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StoreProductReplyUpdateReqVO extends StoreProductReplyBaseVO { @Schema(description = "评论ID", required = true, example = "7419") @NotNull(message = "评论ID不能为空") private Long id; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/storeproductrule/StoreProductRuleController.java ================================================ package co.yixiang.yshop.module.product.controller.admin.storeproductrule; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import org.springframework.security.access.prepost.PreAuthorize; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.*; import java.util.*; import java.io.IOException; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.pojo.CommonResult; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.product.controller.admin.storeproductrule.vo.*; import co.yixiang.yshop.module.product.dal.dataobject.storeproductrule.StoreProductRuleDO; import co.yixiang.yshop.module.product.convert.storeproductrule.StoreProductRuleConvert; import co.yixiang.yshop.module.product.service.storeproductrule.StoreProductRuleService; @Tag(name = "管理后台 - 商品规则值(规格)") @RestController @RequestMapping("/product/store-product-rule") @Validated public class StoreProductRuleController { @Resource private StoreProductRuleService storeProductRuleService; @PostMapping("/save/{id}") @Operation(summary = "创建与更新商品规则值(规格)") @PreAuthorize("@ss.hasPermission('shop:store-product-rule:create')") public CommonResult createStoreProductRule(@Valid @RequestBody StoreProductRuleCreateReqVO createReqVO,@PathVariable Integer id) { if(id != null && id > 0){ StoreProductRuleUpdateReqVO updateReqVO = new StoreProductRuleUpdateReqVO(); updateReqVO.setId(id); updateReqVO.setRuleName(createReqVO.getRuleName()); updateReqVO.setRuleValue(createReqVO.getRuleValue()); storeProductRuleService.updateStoreProductRule(updateReqVO); return success(1); }else{ return success(storeProductRuleService.createStoreProductRule(createReqVO)); } } @DeleteMapping("/delete") @Operation(summary = "删除商品规则值(规格)") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('shop:store-product-rule:delete')") public CommonResult deleteStoreProductRule(@RequestParam("id") Integer id) { storeProductRuleService.deleteStoreProductRule(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得商品规则值(规格)") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('shop:store-product-rule:query')") public CommonResult getStoreProductRule(@RequestParam("id") Integer id) { StoreProductRuleDO storeProductRule = storeProductRuleService.getStoreProductRule(id); return success(StoreProductRuleConvert.INSTANCE.convert(storeProductRule)); } @GetMapping("/list") @Operation(summary = "获得商品规则值(规格)列表") @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") @PreAuthorize("@ss.hasPermission('shop:store-product-rule:query')") public CommonResult> getStoreProductRuleList(@RequestParam("ids") Collection ids) { List list = storeProductRuleService.getStoreProductRuleList(ids); return success(StoreProductRuleConvert.INSTANCE.convertList(list)); } @GetMapping("/page") @Operation(summary = "获得商品规则值(规格)分页") @PreAuthorize("@ss.hasPermission('shop:store-product-rule:query')") public CommonResult> getStoreProductRulePage(@Valid StoreProductRulePageReqVO pageVO) { PageResult pageResult = storeProductRuleService.getStoreProductRulePage(pageVO); System.out.println("aa:"+pageResult); return success(StoreProductRuleConvert.INSTANCE.convertPage(pageResult)); } @GetMapping("/export-excel") @Operation(summary = "导出商品规则值(规格) Excel") @PreAuthorize("@ss.hasPermission('shop:store-product-rule:export')") public void exportStoreProductRuleExcel(@Valid StoreProductRuleExportReqVO exportReqVO, HttpServletResponse response) throws IOException { List list = storeProductRuleService.getStoreProductRuleList(exportReqVO); // 导出 Excel List datas = StoreProductRuleConvert.INSTANCE.convertList02(list); ExcelUtils.write(response, "商品规则值(规格).xls", "数据", StoreProductRuleExcelVO.class, datas); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/storeproductrule/vo/StoreProductRuleBaseVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.storeproductrule.vo; import com.alibaba.fastjson.JSONArray; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import jakarta.validation.constraints.*; /** * 商品规则值(规格) Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class StoreProductRuleBaseVO { @Schema(description = "id", required = false, example = "有值表示是更新没值是添加") private Integer id; @Schema(description = "规格名称", required = true, example = "赵六") @NotNull(message = "规格名称不能为空") private String ruleName; @Schema(description = "规格值", required = true) @NotNull(message = "规格值不能为空") private JSONArray ruleValue; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/storeproductrule/vo/StoreProductRuleCreateReqVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.storeproductrule.vo; import lombok.*; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "管理后台 - 商品规则值(规格)创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StoreProductRuleCreateReqVO extends StoreProductRuleBaseVO { } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/storeproductrule/vo/StoreProductRuleExcelVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.storeproductrule.vo; import com.alibaba.fastjson.JSONArray; import lombok.*; import java.time.LocalDateTime; import com.alibaba.excel.annotation.ExcelProperty; /** * 商品规则值(规格) Excel VO * * @author yshop */ @Data public class StoreProductRuleExcelVO { @ExcelProperty("id") private Integer id; @ExcelProperty("规格名称") private String ruleName; @ExcelProperty("规格值") private JSONArray ruleValue; @ExcelProperty("创建时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/storeproductrule/vo/StoreProductRuleExportReqVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.storeproductrule.vo; import lombok.*; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "管理后台 - 商品规则值(规格) Excel 导出 Request VO,参数和 StoreProductRulePageReqVO 是一致的") @Data public class StoreProductRuleExportReqVO { @Schema(description = "规格名称", example = "赵六") private String ruleName; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/storeproductrule/vo/StoreProductRulePageReqVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.storeproductrule.vo; import lombok.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; @Schema(description = "管理后台 - 商品规则值(规格)分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StoreProductRulePageReqVO extends PageParam { @Schema(description = "规格名称", example = "赵六") private String ruleName; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/storeproductrule/vo/StoreProductRuleRespVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.storeproductrule.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; @Schema(description = "管理后台 - 商品规则值(规格) Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StoreProductRuleRespVO extends StoreProductRuleBaseVO { @Schema(description = "id", required = true, example = "2067") private Integer id; @Schema(description = "创建时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/admin/storeproductrule/vo/StoreProductRuleUpdateReqVO.java ================================================ package co.yixiang.yshop.module.product.controller.admin.storeproductrule.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 商品规则值(规格)更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StoreProductRuleUpdateReqVO extends StoreProductRuleBaseVO { @Schema(description = "id", required = true, example = "2067") @NotNull(message = "id不能为空") private Integer id; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/app/cart/vo/AppStoreCartQueryVo.java ================================================ package co.yixiang.yshop.module.product.controller.app.cart.vo; import co.yixiang.yshop.module.product.controller.app.product.vo.AppStoreProductRespVo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; import java.math.BigDecimal; /** *

* 购物车表 查询结果对象 *

* * @author hupeng * @date 2023-6-13 */ @Data @Schema(description = "用户 APP - 购物车数据vo") public class AppStoreCartQueryVo implements Serializable { private static final long serialVersionUID = 1L; @Schema(description = "购物车表ID", required = true) private Long id; @Schema(description = "用户ID", required = true) private Long uid; @Schema(description = "类型", required = true) private String type; @Schema(description = "商品ID", required = true) private Long productId; @Schema(description = "商品属性", required = true) private String productAttrUnique; @Schema(description = "商品数量", required = true) private Integer cartNum; @Schema(description = "拼团id", required = true) private Long combinationId; @Schema(description = "秒杀产品ID", required = true) private Long seckillId; @Schema(description = "砍价id", required = true) private Long bargainId; @Schema(description = "商品信息", required = true) private AppStoreProductRespVo productInfo; @Schema(description = "成本价", required = true) private BigDecimal costPrice; @Schema(description = "真实价格", required = true) private BigDecimal truePrice; @Schema(description = "真实库存", required = true) private Integer trueStock; @Schema(description = "vip真实价格", required = true) private BigDecimal vipTruePrice; @Schema(description = "唯一id", required = true) private String unique; @Schema(description = "是否评价 大于0表示已经评价", required = true) private Long isReply; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/app/category/AppCategoryController.java ================================================ package co.yixiang.yshop.module.product.controller.app.category; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.module.product.controller.app.category.vo.AppCategoryRespVO; import co.yixiang.yshop.module.product.convert.category.ProductCategoryConvert; import co.yixiang.yshop.module.product.dal.dataobject.category.ProductCategoryDO; import co.yixiang.yshop.module.product.service.category.ProductCategoryService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import jakarta.annotation.Resource; import java.util.Comparator; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "用户 APP - 商品分类") @RestController @RequestMapping("/product/category") @Validated public class AppCategoryController { @Resource private ProductCategoryService categoryService; // @GetMapping("/list") // @Operation(summary = "获得商品分类列表") // public CommonResult> getProductCategoryList() { // List list = categoryService.getEnableCategoryList(); // list.sort(Comparator.comparing(ProductCategoryDO::getSort)); // return success(ProductCategoryConvert.INSTANCE.convertList03(list)); // } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/app/category/vo/AppCategoryRespVO.java ================================================ package co.yixiang.yshop.module.product.controller.app.category.vo; import co.yixiang.yshop.module.product.controller.app.product.vo.AppStoreProductRespVo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.util.List; @Data @Schema(description = "用户 APP - 商品分类 Response VO") public class AppCategoryRespVO { @Schema(description = "分类编号", required = true, example = "2") private Long id; @Schema(description = "父分类编号", required = true, example = "1") @NotNull(message = "父分类编号不能为空") private Long parentId; @Schema(description = "分类名称", required = true, example = "办公文具") @NotBlank(message = "分类名称不能为空") private String name; @Schema(description = "分类图片", required = true) @NotBlank(message = "分类图片不能为空") private String picUrl; @Schema(description = "商品列表", required = true) private List goodsList; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/app/product/AppStoreProductController.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co * 注意: * 本软件为www.yixiang.co开发研制,未经购买不得使用 * 购买后可获得全部源代码(禁止转卖、分享、上传到码云、github等开源平台) * 一经发现盗用、分享等行为,将追究法律责任,后果自负 */ package co.yixiang.yshop.module.product.controller.app.product; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.module.product.controller.app.category.vo.AppCategoryRespVO; import co.yixiang.yshop.module.product.controller.app.product.param.AppStoreProductQueryParam; import co.yixiang.yshop.module.product.controller.app.product.vo.AppStoreProductRespVo; import co.yixiang.yshop.module.product.service.storeproduct.AppStoreProductService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; /** *

* 商品控制器 *

* * @author hupeng * @since 2023-8-16 */ @Slf4j @RestController @Tag(name = "用户 APP - 商品") @RequiredArgsConstructor(onConstructor = @__(@Autowired)) @RequestMapping("/product") public class AppStoreProductController { private final AppStoreProductService storeProductService; /** * 获取产品列表 */ @GetMapping("/products") @Operation(summary = "商品列表") public CommonResult> goodsList(AppStoreProductQueryParam productQueryParam){ return success(storeProductService.getGoodsList(productQueryParam)); } /** * 获取产品详情 */ @GetMapping("/detail/{id}") @Operation(summary = "获取产品详情") public CommonResult goodsDetail(@PathVariable Long id){ return success(storeProductService.getStoreProductById(id)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/app/product/param/AppStoreProductQueryParam.java ================================================ package co.yixiang.yshop.module.product.controller.app.product.param; import co.yixiang.yshop.framework.common.params.QueryParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; /** *

* 商品表 查询参数对象 *

* * @author hupeng * @date 2023-6-12 */ @Data @EqualsAndHashCode(callSuper = true) @Schema(description = "用户 APP - 商品表查询参数") public class AppStoreProductQueryParam extends QueryParam { private static final long serialVersionUID = 1L; @Schema(description = "类别", required = true) private String type; @Schema(description = "分类ID", required = true) private String sid; @Schema(description = "是否新品,不为空的字符串即可", required = true) private String news; @Schema(description = "是否积分兑换商品", required = true) private Integer isIntegral; @Schema(description = "关键字", required = true) private String keyword; @Schema(description = "门店ID", required = true) private Integer shopId; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/app/product/vo/AppIndexVo.java ================================================ package co.yixiang.yshop.module.product.controller.app.product.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; @Data @Builder @AllArgsConstructor @NoArgsConstructor @Schema(description = "用户 APP - 首页数据") public class AppIndexVo { @Schema(description = "首发新品", required = true) private List firstList; @Schema(description = "热门榜单", required = true) private List likeInfo; @Schema(description = "地图key", required = true) private String mapKey; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/app/product/vo/AppProductVo.java ================================================ package co.yixiang.yshop.module.product.controller.app.product.vo; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattrvalue.StoreProductAttrValueDO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** *

* 商品dto *

* * @author hupeng * @date 2023-6-12 */ @Data @Schema(description = "用户 APP - 商品详情vo") public class AppProductVo { @Schema(description = "商品信息列表", required = true) private List goodList = new ArrayList(); @Schema(description = "价格", required = true) private String priceName = ""; @Schema(description = "产品规格", required = true) private List productAttr = new ArrayList(); @Schema(description = "属性集合", required = true) private Map productValue = new LinkedHashMap<>(); @Schema(description = "评论信息", required = true) private AppStoreProductReplyQueryVo reply; @Schema(description = "回复渠道", required = true) private String replyChance; @Schema(description = "回复数", required = true) private Long replyCount; @Schema(description = "商品信息", required = true) private AppStoreProductRespVo storeInfo; @Schema(description = "腾讯地图key", required = true) private String mapKey; @Schema(description = "用户ID", required = true) private Integer uid = 0; @Schema(description = "模版名称", required = true) private String tempName; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/app/product/vo/AppReplyCountVo.java ================================================ package co.yixiang.yshop.module.product.controller.app.product.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.io.Serializable; /** * @ClassName ReplyCount * @Author hupeng <610796224@qq.com> * @Date 2023/7/9 **/ @Getter @Setter @AllArgsConstructor @NoArgsConstructor @Builder public class AppReplyCountVo implements Serializable { @Schema(description = "总的评论数", required = true) private Long sumCount; @Schema(description = "好评数", required = true) private Long goodCount; @Schema(description = "中评数", required = true) private Long inCount; @Schema(description = "差评数", required = true) private Long poorCount; @Schema(description = "好评率", required = true) private String replyChance; @Schema(description = "好评星星数", required = true) private String replySstar; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/app/product/vo/AppStoreProductAttrQueryVo.java ================================================ package co.yixiang.yshop.module.product.controller.app.product.vo; import co.yixiang.yshop.module.product.service.storeproduct.dto.AttrValueDto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; import java.util.List; /** *

* 商品属性表 查询结果对象 *

* * @author hupeng * @date 2023-6-12 */ @Data @Schema(description = "用户 APP - 商品属性表vo") public class AppStoreProductAttrQueryVo implements Serializable { private static final long serialVersionUID = 1L; @Schema(description = "ID", required = true) private Long id; @Schema(description = "商品ID", required = true) private Long productId; @Schema(description = "属性名", required = true) private String attrName; @Schema(description = "属性值", required = true) private String attrValues; @Schema(description = "属性值集合", required = true) private List attrValue; @Schema(description = "属性列表", required = true) private List attrValueArr; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/app/product/vo/AppStoreProductReplyQueryVo.java ================================================ package co.yixiang.yshop.module.product.controller.app.product.vo; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; import java.util.Date; /** *

* 评论表 查询结果对象 *

* * @author hupeng * @date 2023-6-12 */ @Data @Schema(description = "用户 APP - 评论表vo") public class AppStoreProductReplyQueryVo implements Serializable { private static final long serialVersionUID = 1L; @Schema(description = "评论ID", required = true) private Long id; @Schema(description = "产品id", required = true) private Long productId; @Schema(description = "某种商品类型(普通商品、秒杀商品)", required = true) private String replyType; @Schema(description = "商品分数", required = true) private Integer productScore; @Schema(description = "服务分数", required = true) private Integer serviceScore; @Schema(description = "评论内容", required = true) private String comment; @Schema(description = "评论图片", required = true) private String[] pics; @Schema(description = "评论图片,字符串", required = true) private String pictures; @Schema(description = "管理员回复内容", required = true) private String merchantReplyContent; @Schema(description = "管理员回复时间", required = true) private Date merchantReplyTime; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8") @Schema(description = "发布时间", required = true) private Date createTime; @Schema(description = "评价星星数", required = true) private String star; @Schema(description = "用户昵称", required = true) private String nickname; @Schema(description = "用户头像", required = true) private String avatar; @Schema(description = "商品sku", required = true) private String sku; @JsonIgnore private String cartInfo; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/controller/app/product/vo/AppStoreProductRespVo.java ================================================ package co.yixiang.yshop.module.product.controller.app.product.vo; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattrvalue.StoreProductAttrValueDO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; import java.math.BigDecimal; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** *

* 商品表 查询结果对象 *

* * @author hupeng * @date 2023-6-12 */ @Data @Schema(description = "商品 APP - 获取商品列表查询结果数") public class AppStoreProductRespVo implements Serializable { private static final long serialVersionUID = 1L; @Schema(description = "id", required = true) private Long id; @Schema(description = "分类id", required = true) private String cateId; @Schema(description = "商品图片", required = true) private String image; @Schema(description = "是否收藏", required = true) private Boolean userCollect = false; @Schema(description = "是否喜欢", required = true) private Boolean userLike = false; @Schema(description = "轮播图,多个用,分割", required = true) private String sliderImage; @Schema(description = "商品属性信息", required = true) private StoreProductAttrValueDO attrInfo; @Schema(description = "商品名称", required = true) private String storeName; @Schema(description = "商品简介", required = true) private String storeInfo; @Schema(description = "关键字", required = true) private String keyword; @Schema(description = "商品价格", required = true) private BigDecimal price; @Schema(description = "会员价格", required = true) private BigDecimal vipPrice; @Schema(description = "市场价", required = true) private BigDecimal otPrice; @Schema(description = "邮费", required = true) private BigDecimal postage; @Schema(description = "单位名", required = true) private String unitName; @Schema(description = "销量", required = true) private Integer sales; @Schema(description = "库存", required = true) private Integer stock; @Schema(description = "产品描述", required = true) private String description; @Schema(description = "获得积分", required = true) private BigDecimal giveIntegral; @Schema(description = "需要多少积分", required = true) private Integer integral; @Schema(description = "状态(0:未上架,1:上架)", required = true) private Integer isShow; @Schema(description = "运费模板id", required = true) private Integer tempId; @Schema(description = "产品规格", required = true) private List productAttr = new ArrayList(); @Schema(description = "属性集合", required = true) private Map productValue = new LinkedHashMap<>(); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/convert/category/ProductCategoryConvert.java ================================================ package co.yixiang.yshop.module.product.convert.category; import co.yixiang.yshop.module.product.controller.admin.category.vo.ProductCategoryCreateReqVO; import co.yixiang.yshop.module.product.controller.admin.category.vo.ProductCategoryRespVO; import co.yixiang.yshop.module.product.controller.admin.category.vo.ProductCategoryUpdateReqVO; import co.yixiang.yshop.module.product.controller.app.category.vo.AppCategoryRespVO; import co.yixiang.yshop.module.product.dal.dataobject.category.ProductCategoryDO; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import java.util.List; /** * 商品分类 Convert * * @author yshop */ @Mapper public interface ProductCategoryConvert { ProductCategoryConvert INSTANCE = Mappers.getMapper(ProductCategoryConvert.class); ProductCategoryDO convert(ProductCategoryCreateReqVO bean); ProductCategoryDO convert(ProductCategoryUpdateReqVO bean); ProductCategoryRespVO convert(ProductCategoryDO bean); List convertList(List list); List convertList03(List list); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/convert/storeproduct/StoreProductConvert.java ================================================ package co.yixiang.yshop.module.product.convert.storeproduct; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.product.controller.app.product.vo.AppStoreProductRespVo; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import co.yixiang.yshop.module.product.controller.admin.storeproduct.vo.*; import co.yixiang.yshop.module.product.dal.dataobject.storeproduct.StoreProductDO; /** * 商品 Convert * * @author yshop */ @Mapper public interface StoreProductConvert { StoreProductConvert INSTANCE = Mappers.getMapper(StoreProductConvert.class); StoreProductDO convert(StoreProductCreateReqVO bean); StoreProductDO convert(StoreProductUpdateReqVO bean); StoreProductRespVO convert(StoreProductDO bean); AppStoreProductRespVo convert01(StoreProductDO bean); List convertList(List list); PageResult convertPage(PageResult page); List convertList02(List list); List convertList03(List list); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/convert/storeproductattr/StoreProductAttrConvert.java ================================================ package co.yixiang.yshop.module.product.convert.storeproductattr; import co.yixiang.yshop.module.product.controller.admin.storeproduct.vo.StoreProductRespVO; import co.yixiang.yshop.module.product.controller.app.product.vo.AppStoreProductAttrQueryVo; import co.yixiang.yshop.module.product.dal.dataobject.storeproduct.StoreProductDO; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattr.StoreProductAttrDO; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; /** * 商品属性 Convert * * @author yshop */ @Mapper public interface StoreProductAttrConvert { StoreProductAttrConvert INSTANCE = Mappers.getMapper(StoreProductAttrConvert.class); AppStoreProductAttrQueryVo convert(StoreProductAttrDO bean); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/convert/storeproductattrresult/StoreProductAttrResultConvert.java ================================================ package co.yixiang.yshop.module.product.convert.storeproductattrresult; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; /** * 商品属性详情 Convert * * @author yshop */ @Mapper public interface StoreProductAttrResultConvert { StoreProductAttrResultConvert INSTANCE = Mappers.getMapper(StoreProductAttrResultConvert.class); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/convert/storeproductattrvalue/StoreProductAttrValueConvert.java ================================================ package co.yixiang.yshop.module.product.convert.storeproductattrvalue; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; /** * 商品属性值 Convert * * @author yshop */ @Mapper public interface StoreProductAttrValueConvert { StoreProductAttrValueConvert INSTANCE = Mappers.getMapper(StoreProductAttrValueConvert.class); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/convert/storeproductreply/StoreProductReplyConvert.java ================================================ package co.yixiang.yshop.module.product.convert.storeproductreply; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import co.yixiang.yshop.module.product.controller.admin.storeproductreply.vo.*; import co.yixiang.yshop.module.product.dal.dataobject.storeproductreply.StoreProductReplyDO; /** * 评论 Convert * * @author yshop */ @Mapper public interface StoreProductReplyConvert { StoreProductReplyConvert INSTANCE = Mappers.getMapper(StoreProductReplyConvert.class); StoreProductReplyDO convert(StoreProductReplyUpdateReqVO bean); StoreProductReplyRespVO convert(StoreProductReplyDO bean); List convertList(List list); PageResult convertPage(PageResult page); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/convert/storeproductrule/StoreProductRuleConvert.java ================================================ package co.yixiang.yshop.module.product.convert.storeproductrule; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import co.yixiang.yshop.module.product.controller.admin.storeproductrule.vo.*; import co.yixiang.yshop.module.product.dal.dataobject.storeproductrule.StoreProductRuleDO; /** * 商品规则值(规格) Convert * * @author yshop */ @Mapper public interface StoreProductRuleConvert { StoreProductRuleConvert INSTANCE = Mappers.getMapper(StoreProductRuleConvert.class); StoreProductRuleDO convert(StoreProductRuleCreateReqVO bean); StoreProductRuleDO convert(StoreProductRuleUpdateReqVO bean); StoreProductRuleRespVO convert(StoreProductRuleDO bean); List convertList(List list); PageResult convertPage(PageResult page); List convertList02(List list); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/dal/dataobject/category/ProductCategoryDO.java ================================================ package co.yixiang.yshop.module.product.dal.dataobject.category; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; /** * 商品分类 DO * * 商品分类一共两类: * 1)一级分类:{@link #parentId} 等于 0 * 2)二级 + 三级分类:{@link #parentId} 不等于 0 * * @author yshop */ @TableName("yshop_store_product_category") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class ProductCategoryDO extends BaseDO { /** * 父分类编号 - 根分类 */ public static final Long PARENT_ID_NULL = 0L; /** * 分类编号 */ @TableId private Long id; /** * 店铺id */ private Integer shopId; /** * 店铺名称 */ private String shopName; /** * 父分类编号 */ private Long parentId; /** * 分类名称 */ private String name; /** * 分类图片 * * 一级分类:推荐 200 x 100 分辨率 * 二级 + 三级分类:推荐 100 x 100 分辨率 */ private String picUrl; /** * 分类排序 */ private Integer sort; /** * 分类描述 */ private String description; /** * 开启状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/dal/dataobject/storeproduct/StoreProductDO.java ================================================ package co.yixiang.yshop.module.product.dal.dataobject.storeproduct; import lombok.*; import java.util.*; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.LocalDateTime; import java.math.BigDecimal; import java.math.BigDecimal; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; /** * 商品 DO * * @author yshop */ @TableName("yshop_store_product") @KeySequence("yshop_store_product_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class StoreProductDO extends BaseDO { /** * 商品id */ @TableId private Long id; /** * 店铺id */ private Integer shopId; /** * 店铺名称 */ private String shopName; /** * 商品图片 */ private String image; /** * 轮播图 */ private String sliderImage; /** * 商品名称 */ private String storeName; /** * 商品简介 */ private String storeInfo; /** * 关键字 */ private String keyword; /** * 产品条码(一维码) */ private String barCode; /** * 分类id */ private String cateId; //品牌id private Long brandId; /** * 商品价格 */ private BigDecimal price; /** * 会员价格 */ private BigDecimal vipPrice; /** * 市场价 */ private BigDecimal otPrice; /** * 邮费 */ private BigDecimal postage; /** * 单位名 */ private String unitName; /** * 排序 */ private Short sort; /** * 销量 */ private Integer sales; /** * 库存 */ private Integer stock; /** * 状态(0:未上架,1:上架) */ private Integer isShow; /** * 是否热卖 */ private Boolean isHot; /** * 是否优惠 */ private Boolean isBenefit; /** * 是否精品 */ private Boolean isBest; /** * 是否新品 */ private Integer isNew; /** * 产品描述 */ private String description; /** * 是否包邮 */ private Integer isPostage; /** * 商户是否代理 0不可代理1可代理 */ private Byte merUse; /** * 获得积分 */ private BigDecimal giveIntegral; /** * 成本价 */ private BigDecimal cost; /** * 秒杀状态 0 未开启 1已开启 */ private Byte isSeckill; /** * 砍价状态 0未开启 1开启 */ private Byte isBargain; /** * 是否优品推荐 */ private Boolean isGood; /** * 虚拟销量 */ private Integer ficti; /** * 浏览量 */ private Integer browse; /** * 产品二维码地址(用户小程序海报) */ private String codePath; /** * 是否单独分佣 */ private Boolean isSub; /** * 运费模板ID */ private Integer tempId; /** * 规格 0单 1多 */ private Integer specType; /** * 是开启积分兑换 */ private Byte isIntegral; /** * 需要多少积分兑换 只在开启积分兑换时生效 */ private Integer integral; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/dal/dataobject/storeproductattr/StoreProductAttrDO.java ================================================ package co.yixiang.yshop.module.product.dal.dataobject.storeproductattr; import lombok.*; import java.util.*; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; /** * 商品属性 DO * * @author yshop */ @TableName("yshop_store_product_attr") @KeySequence("yshop_store_product_attr_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class StoreProductAttrDO{ /** * id */ @TableId private Long id; /** * 商品ID */ private Long productId; /** * 属性名 */ private String attrName; /** * 属性值 */ private String attrValues; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/dal/dataobject/storeproductattrresult/StoreProductAttrResultDO.java ================================================ package co.yixiang.yshop.module.product.dal.dataobject.storeproductattrresult; import lombok.*; import java.util.*; import java.time.LocalDateTime; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; /** * 商品属性详情 DO * * @author yshop */ @TableName("yshop_store_product_attr_result") @KeySequence("yshop_store_product_attr_result_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class StoreProductAttrResultDO { /** * id */ @TableId private Long id; /** * 商品ID */ private Long productId; /** * 商品属性参数 */ private String result; /** * 上次修改时间 */ private Date changeTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/dal/dataobject/storeproductattrvalue/StoreProductAttrValueDO.java ================================================ package co.yixiang.yshop.module.product.dal.dataobject.storeproductattrvalue; import lombok.*; import java.util.*; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import java.math.BigDecimal; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; /** * 商品属性值 DO * * @author yshop */ @TableName("yshop_store_product_attr_value") @KeySequence("yshop_store_product_attr_value_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class StoreProductAttrValueDO { /** * id */ @TableId private Long id; /** * 商品ID */ private Long productId; /** * 商品属性索引值 (attr_value|attr_value[|....]) */ private String sku; /** * 属性对应的库存 */ private Integer stock; /** * 销量 */ private Integer sales; /** * 属性金额 */ private BigDecimal price; /** * 图片 */ private String image; /** * 唯一值 */ @TableField(value = "`unique`") private String unique; /** * 成本价 */ private BigDecimal cost; /** * 商品条码 */ private String barCode; /** * 原价 */ private BigDecimal otPrice; /** * 重量 */ private BigDecimal weight; /** * 体积 */ private BigDecimal volume; /** * 一级返佣 */ private BigDecimal brokerage; /** * 二级返佣 */ private BigDecimal brokerageTwo; /** * 拼团价 */ private BigDecimal pinkPrice; /** * 拼团库存 */ private Integer pinkStock; /** * 秒杀价 */ private BigDecimal seckillPrice; /** * 秒杀库存 */ private Integer seckillStock; /** * 需要多少积分兑换 */ private Integer integral; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/dal/dataobject/storeproductreply/StoreProductReplyDO.java ================================================ package co.yixiang.yshop.module.product.dal.dataobject.storeproductreply; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; /** * 评论 DO * * @author yshop */ @TableName("yshop_store_product_reply") @KeySequence("yshop_store_product_reply_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class StoreProductReplyDO extends BaseDO { /** * 评论ID */ @TableId private Long id; /** * 用户ID */ private Long uid; /** * 订单ID */ private Long oid; /** * 唯一id */ @TableField(value = "`unique`") private String unique; /** * 产品id */ private Long productId; /** * 某种商品类型(普通商品、秒杀商品) */ private String replyType; /** * 商品分数 */ private Integer productScore; /** * 服务分数 */ private Integer serviceScore; /** * 评论内容 */ private String comment; /** * 评论图片 */ private String pics; /** * 管理员回复内容 */ private String merchantReplyContent; /** * 管理员回复时间 */ private LocalDateTime merchantReplyTime; /** * 0未回复1已回复 */ private Integer isReply; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/dal/dataobject/storeproductrule/StoreProductRuleDO.java ================================================ package co.yixiang.yshop.module.product.dal.dataobject.storeproductrule; import com.alibaba.fastjson.JSONArray; import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler; import lombok.*; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; /** * 商品规则值(规格) DO * * @author yshop */ @TableName(value = "yshop_store_product_rule",autoResultMap = true) @KeySequence("yshop_store_product_rule_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class StoreProductRuleDO extends BaseDO { /** * id */ @TableId private Integer id; /** * 规格名称 */ private String ruleName; /** * 规格值 */ @TableField(typeHandler = FastjsonTypeHandler.class) private JSONArray ruleValue; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/dal/mysql/category/ProductCategoryMapper.java ================================================ package co.yixiang.yshop.module.product.dal.mysql.category; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import co.yixiang.yshop.module.product.controller.admin.category.vo.ProductCategoryListReqVO; import co.yixiang.yshop.module.product.dal.dataobject.category.ProductCategoryDO; import org.apache.ibatis.annotations.Mapper; import java.util.List; /** * 商品分类 Mapper * * @author yshop */ @Mapper public interface ProductCategoryMapper extends BaseMapperX { default List selectList(ProductCategoryListReqVO listReqVO) { Long shopId = SecurityFrameworkUtils.getLoginUser().getShopId(); if(shopId == 0) { listReqVO.setShopId(null); }else { listReqVO.setShopId(shopId.intValue()); } return selectList(new LambdaQueryWrapperX() .likeIfPresent(ProductCategoryDO::getName, listReqVO.getName()) .likeIfPresent(ProductCategoryDO::getShopName, listReqVO.getShopName()) .eqIfPresent(ProductCategoryDO::getShopId,listReqVO.getShopId()) .orderByDesc(ProductCategoryDO::getId)); } default Long selectCountByParentId(Long parentId) { return selectCount(ProductCategoryDO::getParentId, parentId); } default List selectListByStatus(Integer status,Integer shopId) { return selectList(new LambdaQueryWrapperX() .eqIfPresent(ProductCategoryDO::getStatus, status) .eqIfPresent(ProductCategoryDO::getShopId, shopId) .orderByDesc(ProductCategoryDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/dal/mysql/storeproduct/StoreProductMapper.java ================================================ package co.yixiang.yshop.module.product.dal.mysql.storeproduct; import java.util.*; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.convert.Convert; import co.yixiang.yshop.framework.common.enums.ShopCommonEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import co.yixiang.yshop.module.product.dal.dataobject.storeproduct.StoreProductDO; import co.yixiang.yshop.module.product.enums.product.DefaultEnum; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.apache.ibatis.annotations.Mapper; import co.yixiang.yshop.module.product.controller.admin.storeproduct.vo.*; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; /** * 商品 Mapper * * @author yshop */ @Mapper public interface StoreProductMapper extends BaseMapperX { default PageResult selectPage(StoreProductPageReqVO reqVO) { LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX<>(); Long shopId = SecurityFrameworkUtils.getLoginUser().getShopId(); if(shopId > 0) { wrapper.eq(StoreProductDO::getShopId,shopId); } wrapper.likeIfPresent(StoreProductDO::getStoreName, reqVO.getStoreName()) .likeIfPresent(StoreProductDO::getShopName, reqVO.getShopName()) .eqIfPresent(StoreProductDO::getIsPostage, reqVO.getIsPostage()) .eqIfPresent(StoreProductDO::getCateId,reqVO.getCateId()) .orderByDesc(StoreProductDO::getId); wrapper.eq(StoreProductDO::getIsShow,Convert.toInt(reqVO.getIsShow())); if(DefaultEnum.DEFAULT_0.getValue().equals(Convert.toInt(reqVO.getStock()))){ wrapper.eq(StoreProductDO::getStock,DefaultEnum.DEFAULT_0.getValue()); } // // if(CollUtil.isNotEmpty(reqVO.getCatIds())){ // wrapper.in(StoreProductDO::getCateId,reqVO.getCatIds()); // } return selectPage(reqVO, wrapper); } default List selectList(StoreProductExportReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .likeIfPresent(StoreProductDO::getStoreName, reqVO.getStoreName()) .eqIfPresent(StoreProductDO::getIsPostage, reqVO.getIsPostage()) .orderByDesc(StoreProductDO::getId)); } @Update("update yshop_store_product set is_show = #{status} where id = #{id}") void updateOnsale(@Param("status") Integer status, @Param("id") Long id); /** * 正常商品库存 加库存 减销量 * @param num * @param productId * @return */ @Update("update yshop_store_product set stock=stock+#{num}, sales=sales-#{num}" + " where id=#{productId}") int incStockDecSales(@Param("num") Integer num,@Param("productId") Long productId); /** * 正常商品库存 减库存 加销量 * @param num * @param productId * @return */ @Update("update yshop_store_product set stock=stock-#{num}, sales=sales+#{num}" + " where id=#{productId} and stock >= #{num}") int decStockIncSales(@Param("num") Integer num,@Param("productId") Long productId); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/dal/mysql/storeproductattr/StoreProductAttrMapper.java ================================================ package co.yixiang.yshop.module.product.dal.mysql.storeproductattr; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattr.StoreProductAttrDO; import org.apache.ibatis.annotations.Mapper; /** * 商品属性 Mapper * * @author yshop */ @Mapper public interface StoreProductAttrMapper extends BaseMapperX { } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/dal/mysql/storeproductattrresult/StoreProductAttrResultMapper.java ================================================ package co.yixiang.yshop.module.product.dal.mysql.storeproductattrresult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattrresult.StoreProductAttrResultDO; import org.apache.ibatis.annotations.Mapper; /** * 商品属性详情 Mapper * * @author yshop */ @Mapper public interface StoreProductAttrResultMapper extends BaseMapperX { } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/dal/mysql/storeproductattrvalue/StoreProductAttrValueMapper.java ================================================ package co.yixiang.yshop.module.product.dal.mysql.storeproductattrvalue; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattrvalue.StoreProductAttrValueDO; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; /** * 商品属性值 Mapper * * @author yshop */ @Mapper public interface StoreProductAttrValueMapper extends BaseMapperX { /** * 普通商品 减库存 加销量 * @param num * @param productId * @param unique * @return */ @Update("update yshop_store_product_attr_value set stock=stock-#{num}, sales=sales+#{num}" + " where product_id=#{productId} and `sku`=#{unique} and stock >= #{num}") int decStockIncSales(@Param("num") Integer num, @Param("productId") Long productId, @Param("unique") String unique); /** * 正常商品 加库存 减销量 * @param num * @param productId * @param unique * @return */ @Update("update yshop_store_product_attr_value set stock=stock+#{num}, sales=sales-#{num}" + " where product_id=#{productId} and `sku`=#{unique}") int incStockDecSales(@Param("num") Integer num,@Param("productId") Long productId, @Param("unique") String unique); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/dal/mysql/storeproductreply/StoreProductReplyMapper.java ================================================ package co.yixiang.yshop.module.product.dal.mysql.storeproductreply; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.product.controller.app.product.vo.AppStoreProductReplyQueryVo; import co.yixiang.yshop.module.product.dal.dataobject.storeproductreply.StoreProductReplyDO; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.apache.ibatis.annotations.Mapper; import co.yixiang.yshop.module.product.controller.admin.storeproductreply.vo.*; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; /** * 评论 Mapper * * @author yshop */ @Mapper public interface StoreProductReplyMapper extends BaseMapperX { default PageResult selectPage(StoreProductReplyPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(StoreProductReplyDO::getUid, reqVO.getUid()) .eqIfPresent(StoreProductReplyDO::getOid, reqVO.getOid()) .eqIfPresent(StoreProductReplyDO::getUnique, reqVO.getUnique()) .eqIfPresent(StoreProductReplyDO::getProductId, reqVO.getProductId()) .eqIfPresent(StoreProductReplyDO::getReplyType, reqVO.getReplyType()) .eqIfPresent(StoreProductReplyDO::getProductScore, reqVO.getProductScore()) .eqIfPresent(StoreProductReplyDO::getServiceScore, reqVO.getServiceScore()) .eqIfPresent(StoreProductReplyDO::getComment, reqVO.getComment()) .eqIfPresent(StoreProductReplyDO::getPics, reqVO.getPics()) .betweenIfPresent(StoreProductReplyDO::getCreateTime, reqVO.getCreateTime()) .eqIfPresent(StoreProductReplyDO::getMerchantReplyContent, reqVO.getMerchantReplyContent()) .betweenIfPresent(StoreProductReplyDO::getMerchantReplyTime, reqVO.getMerchantReplyTime()) .eqIfPresent(StoreProductReplyDO::getIsReply, reqVO.getIsReply()) .orderByDesc(StoreProductReplyDO::getId)); } @Select("select A.product_score as productScore,A.service_score as serviceScore," + "A.comment,A.merchant_reply_content as merchantReplyContent," + "A.merchant_reply_time as merchantReplyTime,A.pics as pictures,A.create_Time as createTime," + "B.nickname,B.avatar,C.cart_info as cartInfo" + " from yshop_store_product_reply A left join yshop_user B " + "on A.uid = B.id left join yshop_store_order_cart_info C on A.`unique` = C.`unique`" + " where A.product_id=#{productId} and A.deleted=0 " + "order by A.create_Time DESC limit 1") AppStoreProductReplyQueryVo getReply(long productId); @Select("") List selectReplyList(Page page, @Param("productId") long productId, @Param("type") int type); @Select("") List allReplyList(Page page, @Param("nickname") String nickname); @Select("") Long allReplyListCount(@Param("nickname") String nickname); // } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/dal/mysql/storeproductrule/StoreProductRuleMapper.java ================================================ package co.yixiang.yshop.module.product.dal.mysql.storeproductrule; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.product.dal.dataobject.storeproductrule.StoreProductRuleDO; import org.apache.ibatis.annotations.Mapper; import co.yixiang.yshop.module.product.controller.admin.storeproductrule.vo.*; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; /** * 商品规则值(规格) Mapper * * @author yshop */ @Mapper public interface StoreProductRuleMapper extends BaseMapperX { default PageResult selectPage(StoreProductRulePageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(StoreProductRuleDO::getRuleName, reqVO.getRuleName()) .orderByDesc(StoreProductRuleDO::getId)); } default List selectList(StoreProductRuleExportReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .likeIfPresent(StoreProductRuleDO::getRuleName, reqVO.getRuleName()) .orderByDesc(StoreProductRuleDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/framework/package-info.java ================================================ /** * 属于 product 模块的 framework 封装 * * @author yshop */ package co.yixiang.yshop.module.product.framework; ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/framework/web/config/ProductWebConfiguration.java ================================================ package co.yixiang.yshop.module.product.framework.web.config; import co.yixiang.yshop.framework.swagger.config.YshopSwaggerAutoConfiguration; import org.springdoc.core.models.GroupedOpenApi; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * product 模块的 web 组件的 Configuration * * @author yshop */ @Configuration(proxyBeanMethods = false) public class ProductWebConfiguration { /** * product 模块的 API 分组 */ @Bean public GroupedOpenApi productGroupedOpenApi() { return YshopSwaggerAutoConfiguration.buildGroupedOpenApi("product"); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/framework/web/package-info.java ================================================ /** * product 模块的 web 配置 */ package co.yixiang.yshop.module.product.framework.web; ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/package-info.java ================================================ /** * trade 模块,主要实现交易相关功能 * 例如:订单、退款、购物车等功能。 * * 1. Controller URL:以 /product/ 开头,避免和其它 Module 冲突 * 2. DataObject 表名:以 product_ 开头,方便在数据库中区分 */ package co.yixiang.yshop.module.product; ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/category/ProductCategoryService.java ================================================ package co.yixiang.yshop.module.product.service.category; import co.yixiang.yshop.module.product.controller.admin.category.vo.ProductCategoryCreateReqVO; import co.yixiang.yshop.module.product.controller.admin.category.vo.ProductCategoryListReqVO; import co.yixiang.yshop.module.product.controller.admin.category.vo.ProductCategoryUpdateReqVO; import co.yixiang.yshop.module.product.dal.dataobject.category.ProductCategoryDO; import co.yixiang.yshop.module.product.dal.dataobject.storeproduct.StoreProductDO; import com.baomidou.mybatisplus.extension.service.IService; import jakarta.validation.Valid; import java.util.List; /** * 商品分类 Service 接口 * * @author yshop */ public interface ProductCategoryService extends IService { /** * 创建商品分类 * * @param createReqVO 创建信息 * @return 编号 */ Long createCategory(@Valid ProductCategoryCreateReqVO createReqVO); /** * 更新商品分类 * * @param updateReqVO 更新信息 */ void updateCategory(@Valid ProductCategoryUpdateReqVO updateReqVO); /** * 删除商品分类 * * @param id 编号 */ void deleteCategory(Long id); /** * 获得商品分类 * * @param id 编号 * @return 商品分类 */ ProductCategoryDO getCategory(Long id); /** * 校验商品分类 * * @param id 分类编号 */ void validateCategory(Long id); /** * 获得商品分类的层级 * * @param id 编号 * @return 商品分类的层级 */ Integer getCategoryLevel(Long id); /** * 获得商品分类列表 * * @param listReqVO 查询条件 * @return 商品分类列表 */ List getEnableCategoryList(ProductCategoryListReqVO listReqVO); /** * 获得开启状态的商品分类列表 * * @return 商品分类列表 */ List getEnableCategoryList(Integer shopId); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/category/ProductCategoryServiceImpl.java ================================================ package co.yixiang.yshop.module.product.service.category; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.module.product.controller.admin.category.vo.ProductCategoryCreateReqVO; import co.yixiang.yshop.module.product.controller.admin.category.vo.ProductCategoryListReqVO; import co.yixiang.yshop.module.product.controller.admin.category.vo.ProductCategoryUpdateReqVO; import co.yixiang.yshop.module.product.convert.category.ProductCategoryConvert; import co.yixiang.yshop.module.product.dal.dataobject.category.ProductCategoryDO; import co.yixiang.yshop.module.product.dal.dataobject.storeproduct.StoreProductDO; import co.yixiang.yshop.module.product.dal.mysql.category.ProductCategoryMapper; import co.yixiang.yshop.module.product.dal.mysql.storeproduct.StoreProductMapper; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; import co.yixiang.yshop.module.store.dal.mysql.storeshop.StoreShopMapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.List; import java.util.Objects; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.product.enums.ErrorCodeConstants.*; /** * 商品分类 Service 实现类 * * @author yshop */ @Service @Validated public class ProductCategoryServiceImpl extends ServiceImpl implements ProductCategoryService { @Resource private ProductCategoryMapper productCategoryMapper; @Resource private StoreShopMapper storeShopMapper; @Override public Long createCategory(ProductCategoryCreateReqVO createReqVO) { // 校验父分类存在 //validateParentProductCategory(createReqVO.getParentId()); // 插入 ProductCategoryDO category = ProductCategoryConvert.INSTANCE.convert(createReqVO); StoreShopDO storeShopDO = storeShopMapper.selectById(createReqVO.getShopId()); category.setShopName(storeShopDO.getName()); productCategoryMapper.insert(category); // 返回 return category.getId(); } @Override public void updateCategory(ProductCategoryUpdateReqVO updateReqVO) { // 校验分类是否存在 validateProductCategoryExists(updateReqVO.getId()); // 校验父分类存在 //validateParentProductCategory(updateReqVO.getParentId()); // 更新 ProductCategoryDO updateObj = ProductCategoryConvert.INSTANCE.convert(updateReqVO); StoreShopDO storeShopDO = storeShopMapper.selectById(updateObj.getShopId()); updateObj.setShopName(storeShopDO.getName()); productCategoryMapper.updateById(updateObj); } @Override public void deleteCategory(Long id) { // 校验分类是否存在 validateProductCategoryExists(id); // 校验是否还有子分类 if (productCategoryMapper.selectCountByParentId(id) > 0) { throw exception(CATEGORY_EXISTS_CHILDREN); } // TODO yshop 补充只有不存在商品才可以删除 // 删除 productCategoryMapper.deleteById(id); } private void validateParentProductCategory(Long id) { // 如果是根分类,无需验证 if (Objects.equals(id, ProductCategoryDO.PARENT_ID_NULL)) { return; } // 父分类不存在 ProductCategoryDO category = productCategoryMapper.selectById(id); if (category == null) { throw exception(CATEGORY_PARENT_NOT_EXISTS); } ProductCategoryDO storeCategory = productCategoryMapper.selectById(id); // 最多二级 if (!Objects.equals(storeCategory.getParentId(), ProductCategoryDO.PARENT_ID_NULL)) { throw exception(CATEGORY_PARENT_NOT_FIRST_LEVEL); } } private void validateProductCategoryExists(Long id) { ProductCategoryDO category = productCategoryMapper.selectById(id); if (category == null) { throw exception(CATEGORY_NOT_EXISTS); } } @Override public ProductCategoryDO getCategory(Long id) { return productCategoryMapper.selectById(id); } @Override public void validateCategory(Long id) { ProductCategoryDO category = productCategoryMapper.selectById(id); if (category == null) { throw exception(CATEGORY_NOT_EXISTS); } if (Objects.equals(category.getStatus(), CommonStatusEnum.DISABLE.getStatus())) { throw exception(CATEGORY_DISABLED, category.getName()); } } @Override public Integer getCategoryLevel(Long id) { if (Objects.equals(id, ProductCategoryDO.PARENT_ID_NULL)) { return 0; } int level = 1; for (int i = 0; i < 100; i++) { ProductCategoryDO category = productCategoryMapper.selectById(id); // 如果没有父节点,break 结束 if (category == null || Objects.equals(category.getParentId(), ProductCategoryDO.PARENT_ID_NULL)) { break; } // 继续递归父节点 level++; id = category.getParentId(); } return level; } @Override public List getEnableCategoryList(ProductCategoryListReqVO listReqVO) { return productCategoryMapper.selectList(listReqVO); } @Override public List getEnableCategoryList(Integer shopId) { return productCategoryMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus(),shopId); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproduct/AppStoreProductService.java ================================================ package co.yixiang.yshop.module.product.service.storeproduct; import co.yixiang.yshop.module.product.controller.app.category.vo.AppCategoryRespVO; import co.yixiang.yshop.module.product.controller.app.product.param.AppStoreProductQueryParam; import co.yixiang.yshop.module.product.controller.app.product.vo.AppProductVo; import co.yixiang.yshop.module.product.controller.app.product.vo.AppStoreProductRespVo; import co.yixiang.yshop.module.product.dal.dataobject.storeproduct.StoreProductDO; import com.baomidou.mybatisplus.extension.service.IService; import java.util.List; /** * 商品 AppService 接口 * * @author yshop */ public interface AppStoreProductService extends IService { /** * 商品列表 * @param page 页码 * @param limit 条数 * @param order ProductEnum * @return List */ List getList(int page, int limit, int order); /** * 商品列表 * @param productQueryParam AppStoreProductQueryParam * @return list */ List getGoodsList(AppStoreProductQueryParam productQueryParam); /** * 返回普通商品库存 * @param productId 商品id * @param unique sku唯一值 * @return int */ int getProductStock(Long productId, String unique,String type); /** * 获取单个商品 * @param id 商品id * @return YxStoreProductQueryVo */ AppStoreProductRespVo getStoreProductById(Long id); /** * 减少库存与增加销量 * @param num 数量 * @param productId 商品id * @param unique sku */ void decProductStock(int num, Long productId, String unique,Long activityId,String type); /** * 增加库存 减少销量 * @param num 数量 * @param productId 商品id * @param unique sku唯一值 */ void incProductStock(Integer num, Long productId, String unique,Long activityId, String type); /** * 检测商品/秒杀/砍价/拼团库存 * @param uid 用户ID * @param productId 产品ID * @param cartNum 购买数量 * @param productAttrUnique 商品属性Unique */ void checkProductStock(Long uid, Long productId, Integer cartNum, String productAttrUnique); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproduct/AppStoreProductServiceImpl.java ================================================ package co.yixiang.yshop.module.product.service.storeproduct; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.enums.ShopCommonEnum; import co.yixiang.yshop.framework.common.exception.ErrorCode; import co.yixiang.yshop.module.product.controller.app.category.vo.AppCategoryRespVO; import co.yixiang.yshop.module.product.controller.app.product.param.AppStoreProductQueryParam; import co.yixiang.yshop.module.product.controller.app.product.vo.AppStoreProductAttrQueryVo; import co.yixiang.yshop.module.product.controller.app.product.vo.AppStoreProductRespVo; import co.yixiang.yshop.module.product.convert.category.ProductCategoryConvert; import co.yixiang.yshop.module.product.convert.storeproduct.StoreProductConvert; import co.yixiang.yshop.module.product.dal.dataobject.category.ProductCategoryDO; import co.yixiang.yshop.module.product.dal.dataobject.storeproduct.StoreProductDO; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattrvalue.StoreProductAttrValueDO; import co.yixiang.yshop.module.product.dal.mysql.storeproduct.StoreProductMapper; import co.yixiang.yshop.module.product.dal.mysql.storeproductattrvalue.StoreProductAttrValueMapper; import co.yixiang.yshop.module.product.enums.product.ProductEnum; import co.yixiang.yshop.module.product.enums.product.ProductTypeEnum; import co.yixiang.yshop.module.product.service.category.ProductCategoryService; import co.yixiang.yshop.module.product.service.storeproductattr.AppStoreProductAttrService; import co.yixiang.yshop.module.product.service.storeproductattrvalue.StoreProductAttrValueService; import co.yixiang.yshop.module.product.service.storeproductreply.AppStoreProductReplyService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.Comparator; import java.util.List; import java.util.Map; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.product.enums.ErrorCodeConstants.PRODUCT_STOCK_LESS; import static co.yixiang.yshop.module.product.enums.ErrorCodeConstants.STORE_PRODUCT_NOT_EXISTS; /** * 商品 AppService 实现类 * * @author yshop */ @Service @Validated public class AppStoreProductServiceImpl extends ServiceImpl implements AppStoreProductService { @Resource private AppStoreProductAttrService appStoreProductAttrService; @Resource private AppStoreProductReplyService appStoreProductReplyService; @Resource private StoreProductAttrValueService storeProductAttrValueService; @Resource private StoreProductAttrValueMapper storeProductAttrValueMapper; @Resource private ProductCategoryService categoryService; /** * 商品列表 * * @param page 页码 * @param limit 条数 * @param order ProductEnum * @return List */ @Override public List getList(int page, int limit, int order) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StoreProductDO::getIsShow, ShopCommonEnum.SHOW_1.getValue()) //.eq(YxStoreProduct::getIsDel,ShopCommonEnum.DELETE_0.getValue()) .orderByDesc(StoreProductDO::getSort); wrapper.eq(StoreProductDO::getIsIntegral,0); // order switch (ProductEnum.toType(order)) { //精品推荐 case TYPE_1: wrapper.eq(StoreProductDO::getIsBest, ShopCommonEnum.IS_STATUS_1.getValue()); break; //首发新品 case TYPE_3: wrapper.eq(StoreProductDO::getIsNew, ShopCommonEnum.IS_STATUS_1.getValue()); break; // 猜你喜欢 case TYPE_4: wrapper.eq(StoreProductDO::getIsBenefit, ShopCommonEnum.IS_STATUS_1.getValue()); break; // 热门榜单 case TYPE_2: wrapper.eq(StoreProductDO::getIsHot, ShopCommonEnum.IS_STATUS_1.getValue()); break; default: } Page pageModel = new Page<>(page, limit); IPage pageList = this.baseMapper.selectPage(pageModel, wrapper); return StoreProductConvert.INSTANCE.convertList03(pageList.getRecords()); } /** * 商品列表 * * @param productQueryParam AppStoreProductQueryParam * @return list */ @Override public List getGoodsList(AppStoreProductQueryParam productQueryParam) { List list = categoryService.getEnableCategoryList(productQueryParam.getShopId()); list.sort(Comparator.comparing(ProductCategoryDO::getSort)); List appCategoryRespVOS = ProductCategoryConvert.INSTANCE.convertList03(list); for (AppCategoryRespVO appCategoryRespVO : appCategoryRespVOS) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StoreProductDO::getIsShow, ShopCommonEnum.SHOW_1.getValue()) .eq(StoreProductDO::getCateId,appCategoryRespVO.getId()) .eq(StoreProductDO::getShopId,productQueryParam.getShopId()); List storeProductDOList = this.baseMapper.selectList(wrapper); List appStoreProductRespVoList = ListUtil.list(false); for (StoreProductDO storeProductDO : storeProductDOList) { Map returnMap = appStoreProductAttrService.getProductAttrDetail(storeProductDO.getId()); AppStoreProductRespVo storeProductQueryVo = StoreProductConvert.INSTANCE.convert01(storeProductDO); storeProductQueryVo.setProductAttr((List) returnMap.get("productAttr")); storeProductQueryVo.setProductValue((Map) returnMap.get("productValue")); appStoreProductRespVoList.add(storeProductQueryVo); } // List appStoreProductRespVoList = StoreProductConvert.INSTANCE // .convertList03(this.baseMapper.selectList(wrapper)); appCategoryRespVO.setGoodsList(appStoreProductRespVoList); } return appCategoryRespVOS; } /** * 返回普通商品库存 * * @param productId 商品id * @param unique sku唯一值 * @return int */ @Override public int getProductStock(Long productId, String unique, String type) { StoreProductAttrValueDO storeProductAttrValue = storeProductAttrValueService .getOne(Wrappers.lambdaQuery() .eq(StoreProductAttrValueDO::getSku, unique) .eq(StoreProductAttrValueDO::getProductId, productId)); if (storeProductAttrValue == null) { return 0; } if (ProductTypeEnum.PINK.getValue().equals(type)) { return storeProductAttrValue.getPinkStock(); } else if (ProductTypeEnum.SECKILL.getValue().equals(type)) { return storeProductAttrValue.getSeckillStock(); } return storeProductAttrValue.getStock(); } /** * 获取单个商品 * * @param id 商品id * @return YxStoreProductQueryVo */ @Override public AppStoreProductRespVo getStoreProductById(Long id) { AppStoreProductRespVo storeProductRespVo = StoreProductConvert.INSTANCE.convert01(this.baseMapper.selectById(id)); Map returnMap = appStoreProductAttrService.getProductAttrDetail(id); storeProductRespVo.setProductAttr((List) returnMap.get("productAttr")); storeProductRespVo.setProductValue((Map) returnMap.get("productValue")); return storeProductRespVo; } /** * 减少库存与增加销量 * * @param num 数量 * @param productId 商品id * @param unique sku */ @Override @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public void decProductStock(int num, Long productId, String unique, Long activityId, String type) { int res = 0; res = storeProductAttrValueMapper.decStockIncSales(num,productId,unique); if(res == 0) { throw exception(PRODUCT_STOCK_LESS); } int product = this.baseMapper.decStockIncSales(num, productId); if (product == 0) { throw exception(PRODUCT_STOCK_LESS); } } /** * 增加库存 减少销量 * * @param num 数量 * @param productId 商品id * @param unique sku唯一值 */ @Override public void incProductStock(Integer num, Long productId, String unique, Long activityId, String type) { //处理属性sku if (StrUtil.isNotEmpty(unique)) { storeProductAttrValueMapper.incStockDecSales(num, productId, unique); } //更新商品 this.baseMapper.incStockDecSales(num, productId); } /** * 检测商品库存 库存加锁 * * @param uid 用户ID * @param productId 产品ID * @param cartNum 购买数量 * @param productAttrUnique 商品属性Unique */ @Override public void checkProductStock(Long uid, Long productId, Integer cartNum, String productAttrUnique) { StoreProductDO product = this .lambdaQuery().eq(StoreProductDO::getId, productId) .eq(StoreProductDO::getIsShow, ShopCommonEnum.SHOW_1.getValue()) .one(); if (product == null) { throw exception(STORE_PRODUCT_NOT_EXISTS); } int stock = this.getProductStock(productId, productAttrUnique, ""); if (stock < cartNum) { throw exception(new ErrorCode(1008003010, product.getStoreName() + "库存不足" + cartNum)); } } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproduct/StoreProductService.java ================================================ package co.yixiang.yshop.module.product.service.storeproduct; import java.util.*; import jakarta.validation.*; import co.yixiang.yshop.module.product.controller.admin.storeproduct.vo.*; import co.yixiang.yshop.module.product.dal.dataobject.storeproduct.StoreProductDO; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattrvalue.StoreProductAttrValueDO; import co.yixiang.yshop.module.product.service.storeproduct.dto.StoreProductDto; import com.baomidou.mybatisplus.extension.service.IService; /** * 商品 Service 接口 * * @author yshop */ public interface StoreProductService extends IService { /** * 创建商品 * * @param createReqVO 创建信息 * @return 编号 */ Long createStoreProduct(@Valid StoreProductCreateReqVO createReqVO); /** * 更新商品 * * @param updateReqVO 更新信息 */ void updateStoreProduct(@Valid StoreProductUpdateReqVO updateReqVO); /** * 删除商品 * * @param id 编号 */ void deleteStoreProduct(Long id); /** * 获得商品 * * @param id 编号 * @return 商品 */ StoreProductDO getStoreProduct(Long id); /** * 获得商品列表 * * @param ids 编号 * @return 商品列表 */ List getStoreProductList(Collection ids); /** * 获得商品分页 * * @param pageReqVO 分页查询 * @return 商品分页 */ PageResult getStoreProductPage(StoreProductPageReqVO pageReqVO); /** * 获得商品列表, 用于 Excel 导出 * * @param exportReqVO 查询条件 * @return 商品列表 */ List getStoreProductList(StoreProductExportReqVO exportReqVO); /** * 获取生成的属性 * @param id 商品id * @param jsonStr jsonStr * @return map */ Map getFormatAttr(Long id, String jsonStr,boolean isActivity); /** * 新增/保存商品 * @param storeProductDto 商品 */ void insertAndEditYxStoreProduct(StoreProductDto storeProductDto); Map getProductInfo(Long id); /** * 商品上架下架 * @param id 商品id * @param status ShopCommonEnum */ void onSale(Long id,Integer status); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproduct/StoreProductServiceImpl.java ================================================ package co.yixiang.yshop.module.product.service.storeproduct; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.convert.Convert; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.enums.ShopCommonEnum; import co.yixiang.yshop.framework.common.util.string.StrUtils; import co.yixiang.yshop.module.product.convert.storeproductrule.StoreProductRuleConvert; import co.yixiang.yshop.module.product.dal.dataobject.category.ProductCategoryDO; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattrresult.StoreProductAttrResultDO; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattrvalue.StoreProductAttrValueDO; import co.yixiang.yshop.module.product.dal.dataobject.storeproductrule.StoreProductRuleDO; import co.yixiang.yshop.module.product.enums.product.SpecTypeEnum; import co.yixiang.yshop.module.product.service.category.ProductCategoryService; import co.yixiang.yshop.module.product.service.storeproduct.dto.*; import co.yixiang.yshop.module.product.service.storeproductattr.StoreProductAttrService; import co.yixiang.yshop.module.product.service.storeproductattrresult.StoreProductAttrResultService; import co.yixiang.yshop.module.product.service.storeproductattrvalue.StoreProductAttrValueService; import co.yixiang.yshop.module.product.service.storeproductrule.StoreProductRuleService; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; import co.yixiang.yshop.module.store.dal.mysql.storeshop.StoreShopMapper; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.util.*; import java.util.stream.Collectors; import co.yixiang.yshop.module.product.controller.admin.storeproduct.vo.*; import co.yixiang.yshop.module.product.dal.dataobject.storeproduct.StoreProductDO; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.product.convert.storeproduct.StoreProductConvert; import co.yixiang.yshop.module.product.dal.mysql.storeproduct.StoreProductMapper; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.product.enums.ErrorCodeConstants.*; /** * 商品 Service 实现类 * * @author yshop */ @Service @Validated public class StoreProductServiceImpl extends ServiceImpl implements StoreProductService { @Resource private StoreProductMapper storeProductMapper; @Resource private StoreProductAttrValueService storeProductAttrValueService; @Resource private StoreProductAttrService storeProductAttrService; @Resource private StoreProductRuleService storeProductRuleService; @Resource private StoreProductAttrResultService storeProductAttrResultService; @Resource private ProductCategoryService productCategoryService; @Resource private StoreShopMapper storeShopMapper; @Override public Long createStoreProduct(StoreProductCreateReqVO createReqVO) { // 插入 StoreProductDO storeProduct = StoreProductConvert.INSTANCE.convert(createReqVO); storeProductMapper.insert(storeProduct); // 返回 return storeProduct.getId(); } @Override public void updateStoreProduct(StoreProductUpdateReqVO updateReqVO) { // 校验存在 validateStoreProductExists(updateReqVO.getId()); // 更新 StoreProductDO updateObj = StoreProductConvert.INSTANCE.convert(updateReqVO); storeProductMapper.updateById(updateObj); } @Override public void deleteStoreProduct(Long id) { // 校验存在 validateStoreProductExists(id); // 删除 storeProductMapper.deleteById(id); } private void validateStoreProductExists(Long id) { if (storeProductMapper.selectById(id) == null) { throw exception(STORE_PRODUCT_NOT_EXISTS); } } @Override public StoreProductDO getStoreProduct(Long id) { return storeProductMapper.selectById(id); } @Override public List getStoreProductList(Collection ids) { return storeProductMapper.selectBatchIds(ids); } @Override public PageResult getStoreProductPage(StoreProductPageReqVO pageReqVO) { // if(StrUtil.isNotEmpty(pageReqVO.getCateId())) { // ProductCategoryDO productCategoryDO = productCategoryService.getCategory(Convert.toLong(pageReqVO.getCateId())); // if(productCategoryDO != null) { // List catIds = new ArrayList<>(); // if (Objects.equals(productCategoryDO.getParentId(), ProductCategoryDO.PARENT_ID_NULL)) { // catIds = productCategoryService.list((Wrappers.lambdaQuery() // .eq(ProductCategoryDO::getParentId, productCategoryDO.getId()))) // .stream().map(ProductCategoryDO::getId).collect(Collectors.toList()); // } else { // catIds.add(Convert.toLong(pageReqVO.getCateId())); // } // pageReqVO.setCatIds(catIds); // } // } return storeProductMapper.selectPage(pageReqVO); } @Override public List getStoreProductList(StoreProductExportReqVO exportReqVO) { return storeProductMapper.selectList(exportReqVO); } /** * 获取生成的属性 * * @param id 商品id * @param jsonStr jsonStr * @return map */ @Override public Map getFormatAttr(Long id, String jsonStr, boolean isActivity) { JSONObject jsonObject = JSON.parseObject(jsonStr); Map resultMap = new LinkedHashMap<>(3); if (jsonObject == null || jsonObject.get("attrs") == null || jsonObject.getJSONArray("attrs").isEmpty()) { resultMap.put("attr", new ArrayList<>()); resultMap.put("value", new ArrayList<>()); resultMap.put("header", new ArrayList<>()); return resultMap; } List fromatDetailDTOList = JSON.parseArray(jsonObject.get("attrs").toString(), FromatDetailDto.class); //fromatDetailDTOList DetailDto detailDto = this.attrFormat(fromatDetailDTOList); List> headerMapList = null; List> valueMapList = new ArrayList<>(); String align = "center"; Map headerMap = new LinkedHashMap<>(); for (Map> map : detailDto.getRes()) { Map detail = map.get("detail"); String[] detailArr = detail.values().toArray(new String[]{}); //Arrays.sort(detailArr); // String sku = String.join(",", detailArr); String sku = String.join(",", StrUtils.compareTo(CollUtil.toList(detailArr))); Map valueMap = new LinkedHashMap<>(); List detailKeys = detail.entrySet() .stream() .map(Map.Entry::getKey) .collect(Collectors.toList()); int i = 0; headerMapList = new ArrayList<>(); for (String title : detailKeys) { headerMap.put("title", title); headerMap.put("minWidth", "130"); headerMap.put("align", align); headerMap.put("key", "value" + (i + 1)); headerMap.put("slot", "value" + (i + 1)); headerMapList.add(ObjectUtil.clone(headerMap)); i++; } String[] detailValues = detail.values().toArray(new String[]{}); for (int j = 0; j < detailValues.length; j++) { String key = "value" + (j + 1); valueMap.put(key, detailValues[j]); } // /** 拼团属性对应的金额 */ // private BigDecimal pinkPrice; // // /** 秒杀属性对应的金额 */ // private BigDecimal seckillPrice; // /** 拼团库存属性对应的库存 */ // private Integer pinkStock; // // private Integer seckillStock; valueMap.put("detail", detail); valueMap.put("sku", ""); valueMap.put("pic", ""); valueMap.put("price", 0); valueMap.put("cost", 0); valueMap.put("ot_price", 0); valueMap.put("stock", 0); valueMap.put("bar_code", ""); valueMap.put("weight", 0); valueMap.put("volume", 0); valueMap.put("brokerage", 0); valueMap.put("brokerage_two", 0); valueMap.put("pink_price", 0); valueMap.put("seckill_price", 0); valueMap.put("pink_stock", 0); valueMap.put("seckill_stock", 0); valueMap.put("integral", 0); if (id > 0) { StoreProductAttrValueDO storeProductAttrValue = storeProductAttrValueService .getOne(Wrappers.lambdaQuery() .eq(StoreProductAttrValueDO::getProductId, id) .eq(StoreProductAttrValueDO::getSku, sku)); if (storeProductAttrValue != null) { valueMap.put("sku",storeProductAttrValue.getSku()); valueMap.put("pic", storeProductAttrValue.getImage()); valueMap.put("price", storeProductAttrValue.getPrice()); valueMap.put("cost", storeProductAttrValue.getCost()); valueMap.put("ot_price", storeProductAttrValue.getOtPrice()); valueMap.put("stock", storeProductAttrValue.getStock()); valueMap.put("bar_code", storeProductAttrValue.getBarCode()); valueMap.put("weight", storeProductAttrValue.getWeight()); valueMap.put("volume", storeProductAttrValue.getVolume()); valueMap.put("brokerage", storeProductAttrValue.getBrokerage()); valueMap.put("brokerage_two", storeProductAttrValue.getBrokerageTwo()); valueMap.put("pink_price", storeProductAttrValue.getPinkPrice()); valueMap.put("seckill_price", storeProductAttrValue.getSeckillPrice()); valueMap.put("pink_stock", storeProductAttrValue.getPinkStock()); valueMap.put("seckill_stock", storeProductAttrValue.getSeckillStock()); valueMap.put("integral", storeProductAttrValue.getIntegral()); } } valueMapList.add(ObjectUtil.clone(valueMap)); } this.addMap(headerMap, headerMapList, align, isActivity); resultMap.put("attr", fromatDetailDTOList); resultMap.put("value", valueMapList); resultMap.put("header", headerMapList); return resultMap; } /** * 增加表头 * * @param headerMap headerMap * @param headerMapList headerMapList * @param align align */ private void addMap(Map headerMap, List> headerMapList, String align, boolean isActivity) { headerMap.put("title", "图片"); headerMap.put("slot", "pic"); headerMap.put("align", align); headerMap.put("minWidth", 80); headerMapList.add(ObjectUtil.clone(headerMap)); headerMap.put("title", "售价"); headerMap.put("slot", "price"); headerMap.put("align", align); headerMap.put("minWidth", 120); headerMapList.add(ObjectUtil.clone(headerMap)); headerMap.put("title", "成本价"); headerMap.put("slot", "cost"); headerMap.put("align", align); headerMap.put("minWidth", 140); headerMapList.add(ObjectUtil.clone(headerMap)); headerMap.put("title", "原价"); headerMap.put("slot", "ot_price"); headerMap.put("align", align); headerMap.put("minWidth", 140); headerMapList.add(ObjectUtil.clone(headerMap)); headerMap.put("title", "库存"); headerMap.put("slot", "stock"); headerMap.put("align", align); headerMap.put("minWidth", 140); headerMapList.add(ObjectUtil.clone(headerMap)); headerMap.put("title", "产品编号"); headerMap.put("slot", "bar_code"); headerMap.put("align", align); headerMap.put("minWidth", 140); headerMapList.add(ObjectUtil.clone(headerMap)); headerMap.put("title", "重量(KG)"); headerMap.put("slot", "weight"); headerMap.put("align", align); headerMap.put("minWidth", 140); headerMapList.add(ObjectUtil.clone(headerMap)); headerMap.put("title", "体积(m³)"); headerMap.put("slot", "volume"); headerMap.put("align", align); headerMap.put("minWidth", 140); headerMapList.add(ObjectUtil.clone(headerMap)); if (isActivity) { headerMap.put("title", "拼团价"); headerMap.put("slot", "pink_price"); headerMap.put("align", align); headerMap.put("minWidth", 140); headerMapList.add(ObjectUtil.clone(headerMap)); headerMap.put("title", "拼团活动库存"); headerMap.put("slot", "pink_stock"); headerMap.put("align", align); headerMap.put("minWidth", 140); headerMapList.add(ObjectUtil.clone(headerMap)); headerMap.put("title", "秒杀价"); headerMap.put("slot", "seckill_price"); headerMap.put("align", align); headerMap.put("minWidth", 140); headerMapList.add(ObjectUtil.clone(headerMap)); headerMap.put("title", "秒杀活动库存"); headerMap.put("slot", "seckill_stock"); headerMap.put("align", align); headerMap.put("minWidth", 140); headerMapList.add(ObjectUtil.clone(headerMap)); } headerMap.put("title", "操作"); headerMap.put("slot", "action"); headerMap.put("align", align); headerMap.put("minWidth", 70); headerMapList.add(ObjectUtil.clone(headerMap)); } /** * 组合规则属性算法 * * @param fromatDetailDTOList * @return DetailDto */ private DetailDto attrFormat(List fromatDetailDTOList) { List data = new ArrayList<>(); List>> res = new ArrayList<>(); fromatDetailDTOList.stream() .map(FromatDetailDto::getDetail) .forEach(i -> { if (i == null || i.isEmpty()) { throw exception(STORE_PRODUCT_RULE_NEED); } String str = ArrayUtil.join(i.toArray(), ","); if (str.contains("-")) { throw exception(STORE_PRODUCT_RULE_RE); } }); if (fromatDetailDTOList.size() > 1) { for (int i = 0; i < fromatDetailDTOList.size() - 1; i++) { if (i == 0) { data = fromatDetailDTOList.get(i).getDetail(); } List tmp = new LinkedList<>(); for (String v : data) { for (String g : fromatDetailDTOList.get(i + 1).getDetail()) { String rep2 = ""; if (i == 0) { rep2 = fromatDetailDTOList.get(i).getValue() + "_" + v + "-" + fromatDetailDTOList.get(i + 1).getValue() + "_" + g; } else { rep2 = v + "-" + fromatDetailDTOList.get(i + 1).getValue() + "_" + g; } tmp.add(rep2); if (i == fromatDetailDTOList.size() - 2) { Map> rep4 = new LinkedHashMap<>(); Map reptemp = new LinkedHashMap<>(); for (String h : Arrays.asList(rep2.split("-"))) { List rep3 = Arrays.asList(h.split("_")); if (rep3.size() > 1) { reptemp.put(rep3.get(0), rep3.get(1)); } else { reptemp.put(rep3.get(0), ""); } } rep4.put("detail", reptemp); res.add(rep4); } } } if (!tmp.isEmpty()) { data = tmp; } } } else { List dataArr = new ArrayList<>(); for (FromatDetailDto fromatDetailDTO : fromatDetailDTOList) { for (String str : fromatDetailDTO.getDetail()) { Map> map2 = new LinkedHashMap<>(); dataArr.add(fromatDetailDTO.getValue() + "_" + str); Map map1 = new LinkedHashMap<>(); map1.put(fromatDetailDTO.getValue(), str); map2.put("detail", map1); res.add(map2); } } String s = StrUtil.join("-", dataArr); data.add(s); } DetailDto detailDto = new DetailDto(); detailDto.setData(data); detailDto.setRes(res); return detailDto; } /** * 新增/保存商品 * * @param storeProductDto 商品 */ @Override @Transactional(rollbackFor = Exception.class) public void insertAndEditYxStoreProduct(StoreProductDto storeProductDto) { //storeProductDto.setDescription(RegexUtil.converProductDescription(storeProductDto.getDescription())); ProductResultDto resultDTO = this.computedProduct(storeProductDto.getAttrs()); //添加商品 StoreProductDO yxStoreProduct = new StoreProductDO(); BeanUtil.copyProperties(storeProductDto, yxStoreProduct, "sliderImage"); if (storeProductDto.getSliderImage().isEmpty()) { throw exception(STORE_PRODUCT_SLIDER_ERROR); } StoreShopDO storeShopDO = storeShopMapper.selectById(storeProductDto.getShopId()); yxStoreProduct.setShopName(storeShopDO.getName()); yxStoreProduct.setPrice(BigDecimal.valueOf(resultDTO.getMinPrice())); yxStoreProduct.setOtPrice(BigDecimal.valueOf(resultDTO.getMinOtPrice())); yxStoreProduct.setCost(BigDecimal.valueOf(resultDTO.getMinCost())); yxStoreProduct.setIntegral(resultDTO.getMinIntegral()); yxStoreProduct.setStock(resultDTO.getStock()); yxStoreProduct.setSliderImage(String.join(",", storeProductDto.getSliderImage())); this.saveOrUpdate(yxStoreProduct); //属性处理 //处理单sKu if (SpecTypeEnum.TYPE_0.getValue().equals(storeProductDto.getSpecType())) { FromatDetailDto fromatDetailDto = FromatDetailDto.builder() .value("规格") .detailValue("") .attrHidden("") .detail(ListUtil.toList("默认")) .build(); List attrs = storeProductDto.getAttrs(); ProductFormatDto productFormatDto = attrs.get(0); productFormatDto.setValue1("规格"); Map map = new HashMap<>(); map.put("规格", "默认"); productFormatDto.setDetail(map); storeProductAttrService.insertYxStoreProductAttr(ListUtil.toList(fromatDetailDto), ListUtil.toList(productFormatDto), yxStoreProduct.getId()); } else { storeProductAttrService.insertYxStoreProductAttr(storeProductDto.getItems(), storeProductDto.getAttrs(), yxStoreProduct.getId()); } } @Override public Map getProductInfo(Long id) { Map map = new LinkedHashMap<>(3); ArrayUtil.newArray(String.class, 3); //运费模板 //todo //shippingTemplatesService.list() //商品规格 List list = storeProductRuleService.getStoreProductRuleList(CollUtil.newArrayList()); map.put("ruleList", StoreProductRuleConvert.INSTANCE.convertList(list)); if (id == 0 ) { return map; } //处理商品详情 StoreProductDO storeProduct = storeProductMapper.selectById(id); ProductDto productDto = new ProductDto(); BeanUtil.copyProperties(storeProduct,productDto,"sliderImage"); productDto.setSliderImage(Arrays.asList(storeProduct.getSliderImage().split(","))); StoreProductAttrResultDO storeProductAttrResult = storeProductAttrResultService .getOne(Wrappers.lambdaQuery() .eq(StoreProductAttrResultDO::getProductId,id).last("limit 1")); JSONObject result = JSON.parseObject(storeProductAttrResult.getResult()); List attrValues = storeProductAttrValueService.list(new LambdaQueryWrapper().eq(StoreProductAttrValueDO::getProductId, id)); List productFormatDtos =attrValues.stream().map(i ->{ ProductFormatDto productFormatDto = new ProductFormatDto(); BeanUtils.copyProperties(i,productFormatDto); productFormatDto.setPic(i.getImage()); return productFormatDto; }).collect(Collectors.toList()); if(SpecTypeEnum.TYPE_1.getValue().equals(storeProduct.getSpecType())){ productDto.setAttr(new ProductFormatDto()); productDto.setAttrs(productFormatDtos); productDto.setItems(result.getObject("attr",ArrayList.class)); }else{ // this.productFromat(productDto, result); this.productFromatNew(productDto, attrValues.get(0)); } map.put("productInfo",productDto); return map; } /** * 商品上架下架 * * @param id 商品id * @param status ShopCommonEnum */ @Override public void onSale(Long id, Integer status) { if (ShopCommonEnum.SHOW_1.getValue().equals(status)) { status = ShopCommonEnum.SHOW_0.getValue(); } else { status = ShopCommonEnum.SHOW_1.getValue(); } storeProductMapper.updateOnsale(status, id); } /** * 获取商品属性 * @param productDto * @param result */ private void productFromatNew(ProductDto productDto, StoreProductAttrValueDO result) { //Map mapAttr = (Map) result.getObject("value",ArrayList.class).get(0); ProductFormatDto productFormatDto = ProductFormatDto.builder() .pic(result.getImage()) .price(result.getPrice().doubleValue()) .cost(result.getCost().doubleValue()) .otPrice(result.getOtPrice().doubleValue()) .stock(result.getStock()) .barCode(result.getBarCode()) .weight(result.getWeight().doubleValue()) .volume(result.getVolume().doubleValue()) .value1("规格") .integral(result.getIntegral()) .brokerage(result.getBrokerage().doubleValue()) .brokerageTwo(result.getBrokerageTwo().doubleValue()) .pinkPrice(result.getPinkPrice().doubleValue()) .pinkStock(result.getPinkStock()) .seckillPrice(result.getSeckillPrice().doubleValue()) .seckillStock(result.getSeckillStock()) .build(); productDto.setAttr(productFormatDto); } /** * 获取商品属性 已经废弃 * @param productDto * @param result */ @Deprecated private void productFromat(ProductDto productDto, JSONObject result) { Map mapAttr = (Map) result.getObject("value",ArrayList.class).get(0); ProductFormatDto productFormatDto = ProductFormatDto.builder() .pic(mapAttr.get("pic").toString()) .price(Double.valueOf(mapAttr.get("price").toString())) .cost(Double.valueOf(mapAttr.get("cost").toString())) .otPrice(Double.valueOf(mapAttr.get("otPrice").toString())) .stock(Integer.valueOf(mapAttr.get("stock").toString())) .barCode(mapAttr.get("barCode").toString()) .weight(Double.valueOf(mapAttr.get("weight").toString())) .volume(Double.valueOf(mapAttr.get("volume").toString())) .value1(mapAttr.get("value1").toString()) .integral(mapAttr.get("integral") !=null ? Integer.valueOf(mapAttr.get("integral").toString()) : 0) .brokerage(Double.valueOf(mapAttr.get("brokerage").toString())) .brokerageTwo(Double.valueOf(mapAttr.get("brokerageTwo").toString())) .pinkPrice(Double.valueOf(mapAttr.get("pinkPrice").toString())) .pinkStock(Integer.valueOf(mapAttr.get("pinkStock").toString())) .seckillPrice(Double.valueOf(mapAttr.get("seckillPrice").toString())) .seckillStock(Integer.valueOf(mapAttr.get("seckillStock").toString())) .build(); productDto.setAttr(productFormatDto); } /** * 计算产品数据 * * @param attrs attrs * @return ProductResultDto */ private ProductResultDto computedProduct(List attrs) { //取最小价格 Double minPrice = attrs .stream() .map(ProductFormatDto::getPrice) .min(Comparator.naturalOrder()) .orElse(0d); //取最小积分 Integer minIntegral = attrs .stream() .map(ProductFormatDto::getIntegral) .min(Comparator.naturalOrder()) .orElse(0); Double minOtPrice = attrs .stream() .map(ProductFormatDto::getOtPrice) .min(Comparator.naturalOrder()) .orElse(0d); Double minCost = attrs .stream() .map(ProductFormatDto::getCost) .min(Comparator.naturalOrder()) .orElse(0d); //计算库存 Integer stock = attrs .stream() .map(ProductFormatDto::getStock) .reduce(Integer::sum) .orElse(0); if (stock <= 0) { throw exception(STORE_PRODUCT_STOCK_ERROR); } return ProductResultDto.builder() .minPrice(minPrice) .minOtPrice(minOtPrice) .minCost(minCost) .stock(stock) .minIntegral(minIntegral) .build(); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproduct/dto/AttrValueDto.java ================================================ package co.yixiang.yshop.module.product.service.storeproduct.dto; import lombok.Data; /** * @ClassName AttrValueDTO * @Author hupeng <610796224@qq.com> * @Date 2019/10/23 **/ @Data public class AttrValueDto { private String attr; private Boolean check = false; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproduct/dto/DetailDto.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.product.service.storeproduct.dto; import lombok.Data; import java.util.List; import java.util.Map; /** * @ClassName DetailDTO * @Author hupeng <610796224@qq.com> * @Date 2019/10/12 **/ @Data public class DetailDto { private List data; //private List>>> res; private List>> res; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproduct/dto/FromatDetailDto.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.product.service.storeproduct.dto; import lombok.*; import java.util.List; /** * @ClassName FromatDetailDTO * @Author hupeng <610796224@qq.com> * @Date 2019/10/12 **/ @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder public class FromatDetailDto { private String attrHidden; private String detailValue; private List detail; private String value; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproduct/dto/ProductDto.java ================================================ package co.yixiang.yshop.module.product.service.storeproduct.dto; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import java.util.List; /** * 商品对象VO * * @author hupeng * @date 2020-04-25 */ @NoArgsConstructor @AllArgsConstructor @Getter @Setter public class ProductDto { /** 商品id */ private Long id; /** 商品图片 */ private String image; private String shopName; private Integer shopId; /** 轮播图 */ @JsonProperty("slider_image") private List sliderImage; /** 商品名称 */ @JsonProperty("store_name") private String storeName; /** 商品简介 */ @JsonProperty("store_info") private String storeInfo; /** 关键字 */ private String keyword; /** 商品条码(一维码) */ @JsonProperty("bar_code") private String barCode; /** 分类id */ @JsonProperty("cate_id") private String cateId; /** 商品价格 */ private Double price; /** 市场价 */ @JsonProperty("ot_price") private Double otPrice; /** 邮费 */ private Double postage; /** 单位名 */ @JsonProperty("unit_name") private String unitName; /** 排序 */ private Long sort; /** 销量 */ private Long sales; /** 库存 */ private Long stock; /** 状态(0:未上架,1:上架) */ @JsonProperty("is_show") private Integer isShow; /** 是否热卖 */ @JsonProperty("is_hot") private Integer isHot; /** 是否优惠 */ @JsonProperty("is_benefit") private Integer isBenefit; /** 是否精品 */ @JsonProperty("is_best") private Integer isBest; /** 是否新品 */ @JsonProperty("is_new") private Integer isNew; /** 商品描述 */ private String description; /** 是否包邮 */ @JsonProperty("is_postage") private Integer isPostage; /** 获得积分 */ @JsonProperty("give_integral") private Double giveIntegral; /** 成本价 */ private Double cost; /** 是否优品推荐 */ @JsonProperty("is_good") private Integer isGood; /** 是否单独分佣 */ @JsonProperty("is_sub") private Integer isSub; /** 是否开启啊积分兑换 */ @JsonProperty("is_integral") private Integer isIntegral; /** 虚拟销量 */ private Long ficti; /** 运费模板ID */ @JsonProperty("temp_id") private Long tempId; /** 规格 0单 1多 */ @JsonProperty("spec_type") private Integer specType; private ProductFormatDto attr; private List items; private List attrs; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproduct/dto/ProductFormatDto.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.product.service.storeproduct.dto; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.*; import java.util.Map; /** * @ClassName ProductFormatDTO * @Author hupeng <610796224@qq.com> * @Date 2019/10/12 **/ @Builder @AllArgsConstructor @NoArgsConstructor @Getter @Setter public class ProductFormatDto { private String sku = ""; @JsonProperty("bar_code") private String barCode = ""; private Double brokerage = 0d; @JsonProperty("brokerage_two") private Double brokerageTwo = 0d; private Double price = 0d; @JsonProperty("ot_price") private Double otPrice = 0d; private Double cost = 0d; private Integer stock = 0; private Integer integral = 0; private String pic = ""; private String value1 = ""; private String value2 = ""; private Double volume = 0d; private Double weight = 0d; @JsonProperty("pink_price") private Double pinkPrice = 0d; @JsonProperty("pink_stock") private Integer pinkStock = 0; @JsonProperty("seckill_price") private Double seckillPrice = 0d; @JsonProperty("seckill_stock") private Integer seckillStock = 0; private Map detail; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproduct/dto/ProductResultDto.java ================================================ package co.yixiang.yshop.module.product.service.storeproduct.dto; import lombok.Builder; import lombok.Getter; import lombok.Setter; /** * @ClassName 产品结果DTO * @Author hupeng <610796224@qq.com> * @Date 2020/4/24 **/ @Getter @Setter @Builder public class ProductResultDto { private Double minPrice; private Double minOtPrice; private Double minCost; private Integer stock; private Integer minIntegral; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproduct/dto/StoreProductDto.java ================================================ package co.yixiang.yshop.module.product.service.storeproduct.dto; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; import lombok.Setter; import lombok.ToString; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.util.List; /** * 商品对象DTO * * @author hupeng * @date 2020-04-23 */ @Getter @Setter @ToString public class StoreProductDto { /** 商品id */ private Long id; /** * 店铺id */ private Integer shopId; /** * 店铺名称 */ private String shopName; /** 商品图片 */ @NotBlank(message = "商品图片必传") private String image; /** 轮播图 */ @NotNull(message = "轮播图不为空") @JsonProperty("slider_image") private List sliderImage; /** 商品名称 */ @NotBlank(message = "商品名称不能为空") @JsonProperty("store_name") private String storeName; /** 商品简介 */ @JsonProperty("store_info") private String storeInfo; /** 关键字 */ //@NotBlank(message = "关键字不能为空") private String keyword; /** 商品条码(一维码) */ @JsonProperty("bar_code") private String barCode; /** 分类id */ @NotNull(message = "分类id不能为空") @JsonProperty("cate_id") private String cateId; /** 商品价格 */ private Double price; /** 市场价 */ private Double otPrice; /** 邮费 */ private Double postage; /** 单位名 */ @JsonProperty("unit_name") private String unitName; /** 排序 */ private Long sort; /** 销量 */ private Long sales; /** 库存 */ private Long stock; /** 状态(0:未上架,1:上架) */ @JsonProperty("is_show") private Integer isShow; /** 是否热卖 */ @JsonProperty("is_hot") private Integer isHot; /** 是否优惠 */ @JsonProperty("is_benefit") private Integer isBenefit; /** 是否精品 */ @JsonProperty("is_best") private Integer isBest; /** 是否新品 */ @JsonProperty("is_new") private Integer isNew; /** 商品描述 */ @NotBlank(message = "商品详情不能为空") private String description; /** 是否包邮 */ @JsonProperty("is_postage") private Integer isPostage; /** 获得积分 */ @JsonProperty("give_integral") private Double giveIntegral; /** 成本价 */ private Double cost; /** 是否优品推荐 */ @JsonProperty("is_good") private Integer isGood; /** 是否单独分佣 */ @JsonProperty("is_sub") private Integer isSub; /** 是否开启啊积分兑换 */ @JsonProperty("is_integral") private Integer isIntegral; /** 虚拟销量 */ private Long ficti; /** 运费模板ID */ @JsonProperty("temp_id") private Long tempId; /** 规格 0单 1多 */ @JsonProperty("spec_type") private Integer specType; //属性项目 private List items; //sku结果集 private List attrs; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproduct/dto/YxStoreProductDto.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.product.service.storeproduct.dto; import lombok.Data; import java.io.Serializable; import java.math.BigDecimal; /** * @author hupeng * @date 2020-05-12 */ @Data public class YxStoreProductDto implements Serializable { /** 商品id */ private Integer id; /** 商户Id(0为总后台管理员创建,不为0的时候是商户后台创建) */ private Integer merId; /** 商品图片 */ private String image; /** 轮播图 */ private String sliderImage; /** 商品名称 */ private String storeName; /** 商品简介 */ private String storeInfo; /** 关键字 */ private String keyword; /** 产品条码(一维码) */ private String barCode; /** 分类id */ private String cateId; /** 商品价格 */ private BigDecimal price; /** 会员价格 */ private BigDecimal vipPrice; /** 市场价 */ private BigDecimal otPrice; /** 邮费 */ private BigDecimal postage; /** 单位名 */ private String unitName; /** 排序 */ private Integer sort; /** 销量 */ private Integer sales; /** 库存 */ private Integer stock; /** 状态(0:未上架,1:上架) */ private Integer isShow; /** 是否热卖 */ private Integer isHot; /** 是否优惠 */ private Integer isBenefit; /** 是否精品 */ private Integer isBest; /** 是否新品 */ private Integer isNew; /** 产品描述 */ private String description; /** 添加时间 */ private Integer addTime; /** 是否包邮 */ private Integer isPostage; /** 是否删除 */ private Integer isDel; /** 商户是否代理 0不可代理1可代理 */ private Integer merUse; /** 获得积分 */ private BigDecimal giveIntegral; /** 成本价 */ private BigDecimal cost; /** 秒杀状态 0 未开启 1已开启 */ private Integer isSeckill; /** 砍价状态 0未开启 1开启 */ private Integer isBargain; /** 是否优品推荐 */ private Integer isGood; /** 虚拟销量 */ private Integer ficti; /** 浏览量 */ private Integer browse; /** 产品二维码地址(用户小程序海报) */ private String codePath; /** 淘宝京东1688类型 */ private String soureLink; private Integer isIntegral; // private YxStoreCategorySmallDto storeCategory; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproduct/dto/YxStoreProductRelationDto.java ================================================ ///** // * Copyright (C) 2018-2022 // * All rights reserved, Designed By www.yixiang.co // * 注意: // * 本软件为www.yixiang.co开发研制,未经购买不得使用 // * 购买后可获得全部源代码(禁止转卖、分享、上传到码云、github等开源平台) // * 一经发现盗用、分享等行为,将追究法律责任,后果自负 // */ //package co.yixiang.yshop.module.product.service.storeproduct.dto; // //import co.yixiang.modules.product.domain.YxStoreProduct; //import com.fasterxml.jackson.annotation.JsonFormat; //import lombok.Data; //import org.springframework.format.annotation.DateTimeFormat; // //import java.io.Serializable; //import java.sql.Timestamp; // ///** // * @author hupeng // * @date 2020-09-03 // */ //@Data //public class YxStoreProductRelationDto implements Serializable { // // private Long id; // // /** 用户ID */ // private Long uid; // // private String userName; // // /** 商品ID */ // private Long productId; // // private YxStoreProduct product; // // /** 类型(收藏(collect)、点赞(like)) */ // private String type; // // /** 某种类型的商品(普通商品、秒杀商品) */ // private String category; // // /** 添加时间 */ // @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") // @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") // private Timestamp createTime; // @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") // @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") // private Timestamp updateTime; // // private Integer isDel; //} // ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproduct/dto/YxStoreProductReplyDto.java ================================================ ///** // * Copyright (C) 2018-2022 // * All rights reserved, Designed By www.yixiang.co // // */ //package co.yixiang.yshop.module.product.service.storeproduct.dto; // //import co.yixiang.modules.user.service.dto.YxUserSmallDto; //import lombok.Data; // //import java.io.Serializable; //import java.util.Date; // ///** //* @author hupeng //* @date 2020-05-12 //*/ //@Data //public class YxStoreProductReplyDto implements Serializable { // // // 评论ID // private Long id; // // // 用户ID // private Long uid; // // private YxUserSmallDto user; // // // 订单ID // private Long oid; // // // 唯一id // private String unique; // // // 产品id // private Long productId; // // private YxStoreProductSmallDto storeProduct; // // // // 某种商品类型(普通商品、秒杀商品) // private String replyType; // // // 商品分数 // private Integer productScore; // // // 服务分数 // private Integer serviceScore; // // // 评论内容 // private String comment; // // // 评论图片 // private String pics; // // // 评论时间 // private Date createTime; // // // 管理员回复内容 // private String merchantReplyContent; // // // 管理员回复时间 // private Date merchantReplyTime; // // // 0未回复1已回复 // private Integer isReply; //} ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproduct/dto/YxStoreProductRuleDto.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co * 注意: * 本软件为www.yixiang.co开发研制,未经购买不得使用 * 购买后可获得全部源代码(禁止转卖、分享、上传到码云、github等开源平台) * 一经发现盗用、分享等行为,将追究法律责任,后果自负 */ package co.yixiang.yshop.module.product.service.storeproduct.dto; import com.alibaba.fastjson.JSONArray; import lombok.Data; import java.io.Serializable; import java.sql.Timestamp; /** * @author hupeng * @date 2020-06-28 */ @Data public class YxStoreProductRuleDto implements Serializable { private Integer id; /** 规格名称 */ private String ruleName; /** 规格值 */ private JSONArray ruleValue; private Timestamp createTime; private Timestamp updateTime; private Integer isDel; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproduct/dto/YxStoreProductSmallDto.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.product.service.storeproduct.dto; import lombok.Data; import java.io.Serializable; /** * @author hupeng * @date 2019-10-04 */ @Data public class YxStoreProductSmallDto implements Serializable { // 商品id private Integer id; // 商品图片 private String image; // 商品名称 private String storeName; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproductattr/AppStoreProductAttrService.java ================================================ package co.yixiang.yshop.module.product.service.storeproductattr; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattr.StoreProductAttrDO; import co.yixiang.yshop.module.product.service.storeproduct.dto.FromatDetailDto; import co.yixiang.yshop.module.product.service.storeproduct.dto.ProductFormatDto; import com.baomidou.mybatisplus.extension.service.IService; import java.util.Collection; import java.util.List; import java.util.Map; /** * 商品属性 Service 接口 * * @author yshop */ public interface AppStoreProductAttrService extends IService { /** * 获取商品sku属性 * @param productId 商品id * @return map */ Map getProductAttrDetail(long productId); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproductattr/AppStoreProductAttrServiceImpl.java ================================================ package co.yixiang.yshop.module.product.service.storeproductattr; import co.yixiang.yshop.module.product.controller.app.product.vo.AppStoreProductAttrQueryVo; import co.yixiang.yshop.module.product.convert.storeproductattr.StoreProductAttrConvert; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattr.StoreProductAttrDO; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattrvalue.StoreProductAttrValueDO; import co.yixiang.yshop.module.product.dal.mysql.storeproductattr.StoreProductAttrMapper; import co.yixiang.yshop.module.product.service.storeproduct.dto.AttrValueDto; import co.yixiang.yshop.module.product.service.storeproductattrvalue.StoreProductAttrValueService; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.*; import java.util.stream.Collectors; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; /** * 商品属性 Service 实现类 * * @author yshop */ @Service @Validated public class AppStoreProductAttrServiceImpl extends ServiceImpl implements AppStoreProductAttrService { @Resource private StoreProductAttrValueService storeProductAttrValueService; /** * 获取商品sku属性 * @param productId 商品id * @return map */ @Override public Map getProductAttrDetail(long productId) { List storeProductAttrs = this.baseMapper .selectList(Wrappers.lambdaQuery() .eq(StoreProductAttrDO::getProductId,productId) .orderByAsc(StoreProductAttrDO::getAttrValues)); List productAttrValues = storeProductAttrValueService .list(Wrappers.lambdaQuery() .eq(StoreProductAttrValueDO::getProductId,productId)); Map map = productAttrValues.stream() .collect(Collectors.toMap(StoreProductAttrValueDO::getSku, p -> p)); List yxStoreProductAttrQueryVoList = new ArrayList<>(); for (StoreProductAttrDO attr : storeProductAttrs) { List stringList = Arrays.asList(attr.getAttrValues().split(",")); List attrValueDTOS = new ArrayList<>(); for (String str : stringList) { AttrValueDto attrValueDTO = new AttrValueDto(); attrValueDTO.setAttr(str); attrValueDTOS.add(attrValueDTO); } AppStoreProductAttrQueryVo attrQueryVo = StoreProductAttrConvert.INSTANCE.convert(attr); attrQueryVo.setAttrValue(attrValueDTOS); attrQueryVo.setAttrValueArr(stringList); yxStoreProductAttrQueryVoList.add(attrQueryVo); } Map returnMap = new LinkedHashMap<>(2); returnMap.put("productAttr",yxStoreProductAttrQueryVoList); returnMap.put("productValue",map); return returnMap; } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproductattr/StoreProductAttrService.java ================================================ package co.yixiang.yshop.module.product.service.storeproductattr; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattr.StoreProductAttrDO; import co.yixiang.yshop.module.product.service.storeproduct.dto.FromatDetailDto; import co.yixiang.yshop.module.product.service.storeproduct.dto.ProductFormatDto; import com.baomidou.mybatisplus.extension.service.IService; import java.util.Collection; import java.util.List; /** * 商品属性 Service 接口 * * @author yshop */ public interface StoreProductAttrService extends IService { /** * 删除商品属性 * * @param id 编号 */ void deleteStoreProductAttr(Long id); /** * 获得商品属性 * * @param id 编号 * @return 商品属性 */ StoreProductAttrDO getStoreProductAttr(Long id); /** * 获得商品属性列表 * * @param ids 编号 * @return 商品属性列表 */ List getStoreProductAttrList(Collection ids); /** * 新增商品属性 * @param items attr * @param attrs value * @param productId 商品id */ void insertYxStoreProductAttr(List items, List attrs, Long productId); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproductattr/StoreProductAttrServiceImpl.java ================================================ package co.yixiang.yshop.module.product.service.storeproductattr; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.util.string.StrUtils; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattr.StoreProductAttrDO; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattrvalue.StoreProductAttrValueDO; import co.yixiang.yshop.module.product.dal.mysql.storeproductattr.StoreProductAttrMapper; import co.yixiang.yshop.module.product.dal.mysql.storeproductattrvalue.StoreProductAttrValueMapper; import co.yixiang.yshop.module.product.service.storeproduct.dto.FromatDetailDto; import co.yixiang.yshop.module.product.service.storeproduct.dto.ProductFormatDto; import co.yixiang.yshop.module.product.service.storeproductattrresult.StoreProductAttrResultService; import co.yixiang.yshop.module.product.service.storeproductattrvalue.StoreProductAttrValueService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.math.BigDecimal; import java.util.*; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.product.enums.ErrorCodeConstants.*; /** * 商品属性 Service 实现类 * * @author yshop */ @Service @Validated public class StoreProductAttrServiceImpl extends ServiceImpl implements StoreProductAttrService { @Resource private StoreProductAttrMapper storeProductAttrMapper; @Resource private StoreProductAttrValueService storeProductAttrValueService; @Resource private StoreProductAttrValueMapper storeProductAttrValueMapper; @Resource private StoreProductAttrResultService storeProductAttrResultService; @Override public void deleteStoreProductAttr(Long id) { // 校验存在 validateStoreProductAttrExists(id); // 删除 storeProductAttrMapper.deleteById(id); } private void validateStoreProductAttrExists(Long id) { if (storeProductAttrMapper.selectById(id) == null) { throw exception(STORE_PRODUCT_ATTR_NOT_EXISTS); } } @Override public StoreProductAttrDO getStoreProductAttr(Long id) { return storeProductAttrMapper.selectById(id); } @Override public List getStoreProductAttrList(Collection ids) { return storeProductAttrMapper.selectBatchIds(ids); } /** * 新增商品属性 * @param items attr * @param attrs value * @param productId 商品id */ @Override @Transactional(rollbackFor = Exception.class) public void insertYxStoreProductAttr(List items, List attrs, Long productId) { List attrGroup = new ArrayList<>(); for (FromatDetailDto fromatDetailDto : items) { StoreProductAttrDO storeProductAttr = StoreProductAttrDO.builder() .productId(productId) .attrName(fromatDetailDto.getValue()) .attrValues(StrUtil.join(",",fromatDetailDto.getDetail())) .build(); attrGroup.add(storeProductAttr); } /*int count = storeProductAttrValueService.count(Wrappers.lambdaQuery().eq(YxStoreProductAttrValue::getProductId, productId)); if (count > 0 ) { throw new BadRequestException("该产品已被添加到其他活动,禁止操作!"); }*/ List valueGroup = new ArrayList<>(); for (ProductFormatDto productFormatDto : attrs) { // if(productFormatDto.getPinkStock()>productFormatDto.getStock() || productFormatDto.getSeckillStock()>productFormatDto.getStock()){ // throw new BadRequestException("活动商品库存不能大于原有商品库存"); // } List stringList = new ArrayList<>(productFormatDto.getDetail().values()); stringList = StrUtils.compareTo(stringList); StoreProductAttrValueDO oldAttrValue = storeProductAttrValueService.getOne(new LambdaQueryWrapper() .eq(StoreProductAttrValueDO::getSku, productFormatDto.getSku()) .eq(StoreProductAttrValueDO::getProductId, productId)); String unique = IdUtil.simpleUUID(); if (Objects.nonNull(oldAttrValue)) { unique = oldAttrValue.getUnique(); } StoreProductAttrValueDO yxStoreProductAttrValue = StoreProductAttrValueDO.builder() .id(Objects.isNull(oldAttrValue) ? null : oldAttrValue.getId()) .productId(productId) .sku(StrUtil.join(",",stringList)) .price(BigDecimal.valueOf(productFormatDto.getPrice())) .cost(BigDecimal.valueOf(productFormatDto.getCost())) .otPrice(BigDecimal.valueOf(productFormatDto.getOtPrice())) .unique(unique) .image(productFormatDto.getPic()) .barCode(productFormatDto.getBarCode()) .weight(BigDecimal.valueOf(productFormatDto.getWeight())) .volume(BigDecimal.valueOf(productFormatDto.getVolume())) .brokerage(BigDecimal.valueOf(productFormatDto.getBrokerage())) .brokerageTwo(BigDecimal.valueOf(productFormatDto.getBrokerageTwo())) .stock(productFormatDto.getStock()) .integral(productFormatDto.getIntegral()) .pinkPrice(BigDecimal.valueOf(productFormatDto.getPinkPrice()==null?0:productFormatDto.getPinkPrice())) .seckillPrice(BigDecimal.valueOf(productFormatDto.getSeckillPrice()==null?0:productFormatDto.getSeckillPrice())) .pinkStock(productFormatDto.getPinkStock()==null?0:productFormatDto.getPinkStock()) .seckillStock(productFormatDto.getSeckillStock()==null?0:productFormatDto.getSeckillStock()) .build(); valueGroup.add(yxStoreProductAttrValue); } if(attrGroup.isEmpty() || valueGroup.isEmpty()){ throw exception(STORE_PRODUCT_ATTR_NEED); } //清理属性 this.clearProductAttr(productId); //批量添加 this.saveBatch(attrGroup); storeProductAttrValueService.saveBatch(valueGroup); Map map = new LinkedHashMap<>(); map.put("attr",items); map.put("value",attrs); storeProductAttrResultService.insertYxStoreProductAttrResult(map,productId); } /** * 删除YxStoreProductAttrValue表的属性 * @param productId 商品id */ private void clearProductAttr(Long productId) { if(ObjectUtil.isNull(productId)) { throw exception(STORE_PRODUCT_NOT_EXISTS); } storeProductAttrMapper.delete(Wrappers.lambdaQuery() .eq(StoreProductAttrDO::getProductId,productId)); storeProductAttrValueMapper.delete(Wrappers.lambdaQuery() .eq(StoreProductAttrValueDO::getProductId,productId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproductattrresult/StoreProductAttrResultService.java ================================================ package co.yixiang.yshop.module.product.service.storeproductattrresult; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattrresult.StoreProductAttrResultDO; import com.baomidou.mybatisplus.extension.service.IService; import java.util.Collection; import java.util.List; import java.util.Map; /** * 商品属性详情 Service 接口 * * @author yshop */ public interface StoreProductAttrResultService extends IService { /** * 删除商品属性详情 * * @param id 编号 */ void deleteStoreProductAttrResult(Long id); /** * 获得商品属性详情 * * @param id 编号 * @return 商品属性详情 */ StoreProductAttrResultDO getStoreProductAttrResult(Long id); /** * 获得商品属性详情列表 * * @param ids 编号 * @return 商品属性详情列表 */ List getStoreProductAttrResultList(Collection ids); /** * 新增商品属性详情 * @param map map * @param productId 商品id */ void insertYxStoreProductAttrResult(Map map, Long productId); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproductattrresult/StoreProductAttrResultServiceImpl.java ================================================ package co.yixiang.yshop.module.product.service.storeproductattrresult; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattrresult.StoreProductAttrResultDO; import co.yixiang.yshop.module.product.dal.mysql.storeproductattrresult.StoreProductAttrResultMapper; import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.product.enums.ErrorCodeConstants.STORE_PRODUCT_ATTR_RESULT_NOT_EXISTS; /** * 商品属性详情 Service 实现类 * * @author yshop */ @Service @Validated public class StoreProductAttrResultServiceImpl extends ServiceImpl implements StoreProductAttrResultService { @Resource private StoreProductAttrResultMapper storeProductAttrResultMapper; @Override public void deleteStoreProductAttrResult(Long id) { // 校验存在 validateStoreProductAttrResultExists(id); // 删除 storeProductAttrResultMapper.deleteById(id); } private void validateStoreProductAttrResultExists(Long id) { if (storeProductAttrResultMapper.selectById(id) == null) { throw exception(STORE_PRODUCT_ATTR_RESULT_NOT_EXISTS); } } @Override public StoreProductAttrResultDO getStoreProductAttrResult(Long id) { return storeProductAttrResultMapper.selectById(id); } @Override public List getStoreProductAttrResultList(Collection ids) { return storeProductAttrResultMapper.selectBatchIds(ids); } /** * 新增商品属性详情 * @param map map * @param productId 商品id */ @Override public void insertYxStoreProductAttrResult(Map map, Long productId) { StoreProductAttrResultDO yxStoreProductAttrResult = new StoreProductAttrResultDO(); yxStoreProductAttrResult.setProductId(productId); yxStoreProductAttrResult.setResult(JSON.toJSONString(map)); yxStoreProductAttrResult.setChangeTime(new Date()); long count = this.count(Wrappers.lambdaQuery() .eq(StoreProductAttrResultDO::getProductId,productId)); if(count > 0) { this.remove(Wrappers.lambdaQuery() .eq(StoreProductAttrResultDO::getProductId,productId)); } this.save(yxStoreProductAttrResult); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproductattrvalue/StoreProductAttrValueService.java ================================================ package co.yixiang.yshop.module.product.service.storeproductattrvalue; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattrvalue.StoreProductAttrValueDO; import com.baomidou.mybatisplus.extension.service.IService; import java.util.Collection; import java.util.List; /** * 商品属性值 Service 接口 * * @author yshop */ public interface StoreProductAttrValueService extends IService { /** * 删除商品属性值 * * @param id 编号 */ void deleteStoreProductAttrValue(Long id); /** * 获得商品属性值 * * @param id 编号 * @return 商品属性值 */ StoreProductAttrValueDO getStoreProductAttrValue(Long id); /** * 获得商品属性值列表 * * @param ids 编号 * @return 商品属性值列表 */ List getStoreProductAttrValueList(Collection ids); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproductattrvalue/StoreProductAttrValueServiceImpl.java ================================================ package co.yixiang.yshop.module.product.service.storeproductattrvalue; import co.yixiang.yshop.module.product.dal.dataobject.storeproductattrvalue.StoreProductAttrValueDO; import co.yixiang.yshop.module.product.dal.mysql.storeproductattrvalue.StoreProductAttrValueMapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.Collection; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.product.enums.ErrorCodeConstants.STORE_PRODUCT_ATTR_VALUE_NOT_EXISTS; /** * 商品属性值 Service 实现类 * * @author yshop */ @Service @Validated public class StoreProductAttrValueServiceImpl extends ServiceImpl implements StoreProductAttrValueService { @Resource private StoreProductAttrValueMapper storeProductAttrValueMapper; @Override public void deleteStoreProductAttrValue(Long id) { // 校验存在 validateStoreProductAttrValueExists(id); // 删除 storeProductAttrValueMapper.deleteById(id); } private void validateStoreProductAttrValueExists(Long id) { if (storeProductAttrValueMapper.selectById(id) == null) { throw exception(STORE_PRODUCT_ATTR_VALUE_NOT_EXISTS); } } @Override public StoreProductAttrValueDO getStoreProductAttrValue(Long id) { return storeProductAttrValueMapper.selectById(id); } @Override public List getStoreProductAttrValueList(Collection ids) { return storeProductAttrValueMapper.selectBatchIds(ids); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproductreply/AppStoreProductReplyService.java ================================================ package co.yixiang.yshop.module.product.service.storeproductreply; import co.yixiang.yshop.module.product.controller.app.product.vo.AppReplyCountVo; import co.yixiang.yshop.module.product.controller.app.product.vo.AppStoreProductReplyQueryVo; import co.yixiang.yshop.module.product.dal.dataobject.storeproductreply.StoreProductReplyDO; import com.baomidou.mybatisplus.extension.service.IService; import java.util.List; /** * 评论 Service 接口 * * @author yshop */ public interface AppStoreProductReplyService extends IService { /** * 获取评价数量 * @param productId * @return */ Long productReplyCount(long productId); /** * 获取单条评价 * @param productId 商品di * @return YxStoreProductReplyQueryVo */ AppStoreProductReplyQueryVo getReply(long productId); /** * 好评比例 * @param productId * @return */ String replyPer(long productId); /** * 返回当前商品评价数量 * @param unique * @return */ Long replyCount(String unique); /** * 获取评价列表 * @param productId 商品id * @param type 0-全部 1-好评 2-中评 3-差评 * @param page page * @param limit limit * @return list */ List getReplyList(long productId, int type, int page, int limit); /** * 评价数据 * @param productId 商品id * @return ReplyCountVO */ AppReplyCountVo getReplyCount(long productId); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproductreply/AppStoreProductReplyServiceImpl.java ================================================ package co.yixiang.yshop.module.product.service.storeproductreply; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.constant.ShopConstants; import co.yixiang.yshop.module.product.controller.app.cart.vo.AppStoreCartQueryVo; import co.yixiang.yshop.module.product.controller.app.product.vo.AppReplyCountVo; import co.yixiang.yshop.module.product.controller.app.product.vo.AppStoreProductReplyQueryVo; import co.yixiang.yshop.module.product.dal.dataobject.storeproductreply.StoreProductReplyDO; import co.yixiang.yshop.module.product.dal.mysql.storeproductreply.StoreProductReplyMapper; import co.yixiang.yshop.module.product.enums.ProductConstants; import co.yixiang.yshop.module.product.enums.product.ScoreEnum; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; /** * 评论 Service 实现类 * * @author yshop */ @Service @Validated public class AppStoreProductReplyServiceImpl extends ServiceImpl implements AppStoreProductReplyService { @Resource private StoreProductReplyMapper storeProductReplyMapper; /** * 获取评价数量 * @param productId * @return */ @Override public Long productReplyCount(long productId) { return this.baseMapper.selectCount(Wrappers.lambdaQuery() .eq(StoreProductReplyDO::getProductId,productId)); } /** * 获取单条评价 * @param productId 商品di * @return YxStoreProductReplyQueryVo */ @Override public AppStoreProductReplyQueryVo getReply(long productId) { AppStoreProductReplyQueryVo vo = this.baseMapper.getReply(productId); if(ObjectUtil.isNotNull(vo)){ return handleReply(vo); } return null; } /** * 好评比例 * @param productId 商品id * @return % */ @Override public String replyPer(long productId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StoreProductReplyDO::getProductId,productId) .eq(StoreProductReplyDO::getProductScore, ScoreEnum.DEFAULT_5.getValue()); Long productScoreCount = this.baseMapper.selectCount(wrapper); Long count = productReplyCount(productId); if(count > 0){ return ""+NumberUtil.round(NumberUtil.mul(NumberUtil.div(productScoreCount,count),100),2); } return ShopConstants.YSHOP_ZERO; } /** * 返回当前商品评价数量 * @param unique * @return */ @Override public Long replyCount(String unique) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StoreProductReplyDO::getUnique,unique); return this.baseMapper.selectCount(wrapper); } /** * 获取评价列表 * @param productId 商品id * @param type 0-全部 1-好评 2-中评 3-差评 * @param page page * @param limit limit * @return list */ @Override public List getReplyList(long productId, int type, int page, int limit) { List newList = new ArrayList<>(); Page pageModel = new Page<>(page, limit); List list = this.baseMapper .selectReplyList(pageModel,productId,type); List list1 = list.stream().map(i ->{ AppStoreProductReplyQueryVo vo = new AppStoreProductReplyQueryVo(); BeanUtils.copyProperties(i,vo); if(i.getPictures().contains(",")){ vo.setPics(i.getPictures().split(",")); } return vo; }).collect(Collectors.toList()); for (AppStoreProductReplyQueryVo queryVo : list1) { newList.add(handleReply(queryVo)); } return newList; } /** * 评价数据 * @param productId 商品id * @return ReplyCountVO */ @Override public AppReplyCountVo getReplyCount(long productId) { Long sumCount = productReplyCount(productId); if(sumCount == 0) { return new AppReplyCountVo(); } //好评 Long goodCount = this.baseMapper.selectCount(Wrappers.lambdaQuery() .eq(StoreProductReplyDO::getProductId,productId) .eq(StoreProductReplyDO::getProductScore,ScoreEnum.DEFAULT_5.getValue())); //中评 Long inCount = this.baseMapper.selectCount(Wrappers.lambdaQuery() .eq(StoreProductReplyDO::getProductId,productId) .lt(StoreProductReplyDO::getProductScore,ScoreEnum.DEFAULT_5.getValue()) .gt(StoreProductReplyDO::getProductScore,ScoreEnum.DEFAULT_2.getValue())); //差评 Long poorCount = this.baseMapper.selectCount(Wrappers.lambdaQuery() .eq(StoreProductReplyDO::getProductId,productId) .lt(StoreProductReplyDO::getProductScore,ScoreEnum.DEFAULT_2.getValue())); //好评率 String replyChance = ""+NumberUtil.round(NumberUtil.mul(NumberUtil.div(goodCount,sumCount),100),2); String replyStar = ""+NumberUtil.round(NumberUtil.mul(NumberUtil.div(goodCount,sumCount),5),2); return AppReplyCountVo.builder() .sumCount(sumCount) .goodCount(goodCount) .inCount(inCount) .poorCount(poorCount) .replyChance(replyChance) .replySstar(replyStar) .build(); } /** * 处理评价 * @param replyQueryVo replyQueryVo * @return YxStoreProductReplyQueryVo */ private AppStoreProductReplyQueryVo handleReply(AppStoreProductReplyQueryVo replyQueryVo) { AppStoreCartQueryVo cartInfo = JSONObject.parseObject(replyQueryVo.getCartInfo() ,AppStoreCartQueryVo.class); if(ObjectUtil.isNotNull(cartInfo)){ if(ObjectUtil.isNotNull(cartInfo.getProductInfo())){ if(ObjectUtil.isNotNull(cartInfo.getProductInfo().getAttrInfo())){ replyQueryVo.setSku(cartInfo.getProductInfo().getAttrInfo().getSku()); } } } BigDecimal star = NumberUtil.add(replyQueryVo.getProductScore(), replyQueryVo.getServiceScore()); star = NumberUtil.div(star,2); replyQueryVo.setStar(String.valueOf(star.intValue())); if(StrUtil.isEmpty(replyQueryVo.getComment())){ replyQueryVo.setComment(ProductConstants.NO_COMMENT_CONTENT); } return replyQueryVo; } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproductreply/StoreProductReplyService.java ================================================ package co.yixiang.yshop.module.product.service.storeproductreply; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.product.controller.admin.storeproductreply.vo.StoreProductReplyPageReqVO; import co.yixiang.yshop.module.product.controller.admin.storeproductreply.vo.StoreProductReplyUpdateReqVO; import co.yixiang.yshop.module.product.controller.app.product.vo.AppStoreProductReplyQueryVo; import co.yixiang.yshop.module.product.dal.dataobject.storeproductreply.StoreProductReplyDO; import jakarta.validation.Valid; /** * 评论 Service 接口 * * @author yshop */ public interface StoreProductReplyService { /** * 更新评论 * * @param updateReqVO 更新信息 */ void updateStoreProductReply(@Valid StoreProductReplyUpdateReqVO updateReqVO); /** * 删除评论 * * @param id 编号 */ void deleteStoreProductReply(Long id); /** * 获得评论 * * @param id 编号 * @return 评论 */ StoreProductReplyDO getStoreProductReply(Long id); /** * 获得评论分页 * * @param pageReqVO 分页查询 * @return 评论分页 */ PageResult getStoreProductReplyPage(StoreProductReplyPageReqVO pageReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproductreply/StoreProductReplyServiceImpl.java ================================================ package co.yixiang.yshop.module.product.service.storeproductreply; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.product.controller.app.product.vo.AppStoreProductReplyQueryVo; import co.yixiang.yshop.module.product.dal.dataobject.storeproduct.StoreProductDO; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import java.util.*; import co.yixiang.yshop.module.product.controller.admin.storeproductreply.vo.*; import co.yixiang.yshop.module.product.dal.dataobject.storeproductreply.StoreProductReplyDO; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.product.convert.storeproductreply.StoreProductReplyConvert; import co.yixiang.yshop.module.product.dal.mysql.storeproductreply.StoreProductReplyMapper; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.product.enums.ErrorCodeConstants.*; /** * 评论 Service 实现类 * * @author yshop */ @Service @Validated public class StoreProductReplyServiceImpl implements StoreProductReplyService { @Resource private StoreProductReplyMapper storeProductReplyMapper; @Override public void updateStoreProductReply(StoreProductReplyUpdateReqVO updateReqVO) { // 校验存在 validateStoreProductReplyExists(updateReqVO.getId()); // 更新 StoreProductReplyDO updateObj = StoreProductReplyConvert.INSTANCE.convert(updateReqVO); storeProductReplyMapper.updateById(updateObj); } @Override public void deleteStoreProductReply(Long id) { // 校验存在 validateStoreProductReplyExists(id); // 删除 storeProductReplyMapper.deleteById(id); } private void validateStoreProductReplyExists(Long id) { if (storeProductReplyMapper.selectById(id) == null) { throw exception(STORE_PRODUCT_REPLY_NOT_EXISTS); } } @Override public StoreProductReplyDO getStoreProductReply(Long id) { return storeProductReplyMapper.selectById(id); } @Override public PageResult getStoreProductReplyPage(StoreProductReplyPageReqVO pageReqVO) { Page pageModel = new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize()); List list = storeProductReplyMapper .allReplyList(pageModel,pageReqVO.getNickname()); return new PageResult<>(list, storeProductReplyMapper.allReplyListCount(pageReqVO.getNickname())); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproductrule/StoreProductRuleService.java ================================================ package co.yixiang.yshop.module.product.service.storeproductrule; import java.util.*; import jakarta.validation.*; import co.yixiang.yshop.module.product.controller.admin.storeproductrule.vo.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.product.dal.dataobject.storeproductrule.StoreProductRuleDO; /** * 商品规则值(规格) Service 接口 * * @author yshop */ public interface StoreProductRuleService { /** * 创建商品规则值(规格) * * @param createReqVO 创建信息 * @return 编号 */ Integer createStoreProductRule(@Valid StoreProductRuleCreateReqVO createReqVO); /** * 更新商品规则值(规格) * * @param updateReqVO 更新信息 */ void updateStoreProductRule(@Valid StoreProductRuleUpdateReqVO updateReqVO); /** * 删除商品规则值(规格) * * @param id 编号 */ void deleteStoreProductRule(Integer id); /** * 获得商品规则值(规格) * * @param id 编号 * @return 商品规则值(规格) */ StoreProductRuleDO getStoreProductRule(Integer id); /** * 获得商品规则值(规格)列表 * * @param ids 编号 * @return 商品规则值(规格)列表 */ List getStoreProductRuleList(Collection ids); /** * 获得商品规则值(规格)分页 * * @param pageReqVO 分页查询 * @return 商品规则值(规格)分页 */ PageResult getStoreProductRulePage(StoreProductRulePageReqVO pageReqVO); /** * 获得商品规则值(规格)列表, 用于 Excel 导出 * * @param exportReqVO 查询条件 * @return 商品规则值(规格)列表 */ List getStoreProductRuleList(StoreProductRuleExportReqVO exportReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/java/co/yixiang/yshop/module/product/service/storeproductrule/StoreProductRuleServiceImpl.java ================================================ package co.yixiang.yshop.module.product.service.storeproductrule; import co.yixiang.yshop.module.product.convert.storeproductrule.StoreProductRuleConvert; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import java.util.*; import co.yixiang.yshop.module.product.controller.admin.storeproductrule.vo.*; import co.yixiang.yshop.module.product.dal.dataobject.storeproductrule.StoreProductRuleDO; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.product.dal.mysql.storeproductrule.StoreProductRuleMapper; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.product.enums.ErrorCodeConstants.STORE_PRODUCT_RULE_NOT_EXISTS; /** * 商品规则值(规格) Service 实现类 * * @author yshop */ @Service @Validated public class StoreProductRuleServiceImpl implements StoreProductRuleService { @Resource private StoreProductRuleMapper storeProductRuleMapper; @Override public Integer createStoreProductRule(StoreProductRuleCreateReqVO createReqVO) { // 插入 StoreProductRuleDO storeProductRule = StoreProductRuleConvert.INSTANCE.convert(createReqVO); storeProductRuleMapper.insert(storeProductRule); // 返回 return storeProductRule.getId(); } @Override public void updateStoreProductRule(StoreProductRuleUpdateReqVO updateReqVO) { // 校验存在 validateStoreProductRuleExists(updateReqVO.getId()); // 更新 StoreProductRuleDO updateObj = StoreProductRuleConvert.INSTANCE.convert(updateReqVO); storeProductRuleMapper.updateById(updateObj); } @Override public void deleteStoreProductRule(Integer id) { // 校验存在 validateStoreProductRuleExists(id); // 删除 storeProductRuleMapper.deleteById(id); } private void validateStoreProductRuleExists(Integer id) { if (storeProductRuleMapper.selectById(id) == null) { throw exception(STORE_PRODUCT_RULE_NOT_EXISTS); } } @Override public StoreProductRuleDO getStoreProductRule(Integer id) { return storeProductRuleMapper.selectById(id); } @Override public List getStoreProductRuleList(Collection ids) { if (ids.isEmpty()) { return storeProductRuleMapper.selectList(); } return storeProductRuleMapper.selectBatchIds(ids); } @Override public PageResult getStoreProductRulePage(StoreProductRulePageReqVO pageReqVO) { return storeProductRuleMapper.selectPage(pageReqVO); } @Override public List getStoreProductRuleList(StoreProductRuleExportReqVO exportReqVO) { return storeProductRuleMapper.selectList(exportReqVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/resources/mapper/shippingtemplates/ShippingTemplatesMapper.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/resources/mapper/shippingtemplatesfree/ShippingTemplatesFreeMapper.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/resources/mapper/shippingtemplatesregion/ShippingTemplatesRegionMapper.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/resources/mapper/storeproduct/StoreProductMapper.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/resources/mapper/storeproductattr/StoreProductAttrMapper.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/resources/mapper/storeproductattrresult/StoreProductAttrResultMapper.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/resources/mapper/storeproductattrvalue/StoreProductAttrValueMapper.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/resources/mapper/storeproductrelation/StoreProductRelationMapper.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/resources/mapper/storeproductreply/StoreProductReplyMapper.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-product-biz/src/main/resources/mapper/storeproductrule/StoreProductRuleMapper.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-api/pom.xml ================================================ 4.0.0 co.yixiang.boot yshop-module-mall ${revision} yshop-module-shop-api jar ${project.artifactId} shop 模块 API,暴露给其它模块调用 co.yixiang.boot yshop-common org.springframework.boot spring-boot-starter-validation true ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-api/src/main/java/co/yixiang/yshop/module/shop/enums/ErrorCodeConstants.java ================================================ package co.yixiang.yshop.module.shop.enums; import co.yixiang.yshop.framework.common.exception.ErrorCode; /** * Product 错误码枚举类 * * product 系统,使用 1-008-000-000 段 */ public interface ErrorCodeConstants { // ========== 素材相关 1009001000 ============ ErrorCode MATERIAL_NOT_EXISTS = new ErrorCode(1009001000, "素材库不存在"); ErrorCode MATERIAL_GROUP_NOT_EXISTS = new ErrorCode(1009001001, "素材分组不存在"); ErrorCode ADS_NOT_EXISTS = new ErrorCode(1008017000, "广告图管理不存在"); ErrorCode SERVICE_NOT_EXISTS = new ErrorCode(1008017001, "我的服务不存在"); ErrorCode RECHARGE_NOT_EXISTS = new ErrorCode(1008017002, "充值金额不存在"); // ========== 商品规则值(规格) ========== } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/pom.xml ================================================ co.yixiang.boot yshop-module-mall ${revision} 4.0.0 yshop-module-shop-biz jar ${project.artifactId} shop 模块,主要实现商品相关功能 co.yixiang.boot yshop-module-shop-api ${revision} co.yixiang.boot yshop-module-store-biz ${revision} co.yixiang.boot yshop-spring-boot-starter-web co.yixiang.boot yshop-spring-boot-starter-security co.yixiang.boot yshop-spring-boot-starter-mybatis co.yixiang.boot yshop-spring-boot-starter-test co.yixiang.boot yshop-spring-boot-starter-excel ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/material/MaterialController.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.material; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.shop.controller.admin.material.vo.MaterialCreateReqVO; import co.yixiang.yshop.module.shop.controller.admin.material.vo.MaterialPageReqVO; import co.yixiang.yshop.module.shop.controller.admin.material.vo.MaterialRespVO; import co.yixiang.yshop.module.shop.controller.admin.material.vo.MaterialUpdateReqVO; import co.yixiang.yshop.module.shop.convert.material.MaterialConvert; import co.yixiang.yshop.module.shop.dal.dataobject.material.MaterialDO; import co.yixiang.yshop.module.shop.service.material.MaterialService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.Collection; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 素材库") @RestController @RequestMapping("/shop/material") @Validated public class MaterialController { @Resource private MaterialService materialService; @PostMapping("/create") @Operation(summary = "创建素材库") //@PreAuthorize("@ss.hasPermission('shop:material:create')") public CommonResult createMaterial(@Valid @RequestBody MaterialCreateReqVO createReqVO) { return success(materialService.createMaterial(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新素材库") //@PreAuthorize("@ss.hasPermission('shop:material:update')") public CommonResult updateMaterial(@Valid @RequestBody MaterialUpdateReqVO updateReqVO) { materialService.updateMaterial(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除素材库") @Parameter(name = "id", description = "编号", required = true) //@PreAuthorize("@ss.hasPermission('shop:material:delete')") public CommonResult deleteMaterial(@RequestParam("id") Long id) { materialService.deleteMaterial(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得素材库") @Parameter(name = "id", description = "编号", required = true, example = "1024") //@PreAuthorize("@ss.hasPermission('shop:material:query')") public CommonResult getMaterial(@RequestParam("id") Long id) { MaterialDO material = materialService.getMaterial(id); return success(MaterialConvert.INSTANCE.convert(material)); } @GetMapping("/list") @Operation(summary = "获得素材库列表") @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") //@PreAuthorize("@ss.hasPermission('shop:material:query')") public CommonResult> getMaterialList(@RequestParam("ids") Collection ids) { List list = materialService.getMaterialList(ids); return success(MaterialConvert.INSTANCE.convertList(list)); } @GetMapping("/page") @Operation(summary = "获得素材库分页") //@PreAuthorize("@ss.hasPermission('shop:material:query')") public CommonResult> getMaterialPage(@Valid MaterialPageReqVO pageVO) { PageResult pageResult = materialService.getMaterialPage(pageVO); return success(MaterialConvert.INSTANCE.convertPage(pageResult)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/material/vo/MaterialBaseVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.material.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import jakarta.validation.constraints.*; /** * 素材库 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class MaterialBaseVO { @Schema(description = "类型1、图片;2、视频", required = true, example = "2") @NotNull(message = "类型1、图片;2、视频不能为空") private String type; @Schema(description = "分组ID", example = "21579") private String groupId; @Schema(description = "素材名", required = true, example = "yshop") @NotNull(message = "素材名不能为空") private String name; @Schema(description = "素材链接", example = "https://www.yixiang.co") private String url; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/material/vo/MaterialCreateReqVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.material.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 素材库创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MaterialCreateReqVO extends MaterialBaseVO { } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/material/vo/MaterialExcelVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.material.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.alibaba.excel.annotation.ExcelProperty; /** * 素材库 Excel VO * * @author yshop */ @Data public class MaterialExcelVO { @ExcelProperty("编号") private Long id; @ExcelProperty("创建时间") private LocalDateTime createTime; @ExcelProperty("类型1、图片;2、视频") private String type; @ExcelProperty("分组ID") private String groupId; @ExcelProperty("素材名") private String name; @ExcelProperty("素材链接") private String url; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/material/vo/MaterialExportReqVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.material.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import java.time.LocalDateTime; import org.springframework.format.annotation.DateTimeFormat; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 素材库 Excel 导出 Request VO,参数和 MaterialPageReqVO 是一致的") @Data public class MaterialExportReqVO { @Schema(description = "创建时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; @Schema(description = "类型1、图片;2、视频", example = "2") private String type; @Schema(description = "分组ID", example = "21579") private String groupId; @Schema(description = "素材名", example = "yshop") private String name; @Schema(description = "素材链接", example = "https://www.yixiang.co") private String url; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/material/vo/MaterialPageReqVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.material.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 素材库分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MaterialPageReqVO extends PageParam { @Schema(description = "创建时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; @Schema(description = "类型1、图片;2、视频", example = "2") private String type; @Schema(description = "分组ID", example = "21579") private String groupId; @Schema(description = "素材名", example = "yshop") private String name; @Schema(description = "素材链接", example = "https://www.yixiang.co") private String url; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/material/vo/MaterialRespVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.material.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; @Schema(description = "管理后台 - 素材库 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MaterialRespVO extends MaterialBaseVO { @Schema(description = "编号", required = true, example = "9920") private Long id; @Schema(description = "创建时间", required = true) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/material/vo/MaterialUpdateReqVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.material.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 素材库更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MaterialUpdateReqVO extends MaterialBaseVO { @Schema(description = "编号", required = true, example = "9920") @NotNull(message = "编号不能为空") private Long id; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/materialgroup/MaterialGroupController.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.materialgroup; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.shop.controller.admin.materialgroup.vo.*; import co.yixiang.yshop.module.shop.convert.materialgroup.MaterialGroupConvert; import co.yixiang.yshop.module.shop.dal.dataobject.materialgroup.MaterialGroupDO; import co.yixiang.yshop.module.shop.service.materialgroup.MaterialGroupService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 素材分组") @RestController @RequestMapping("/shop/material-group") @Validated public class MaterialGroupController { @Resource private MaterialGroupService materialGroupService; @PostMapping("/create") @Operation(summary = "创建素材分组") //@PreAuthorize("@ss.hasPermission('shop:material-group:create')") public CommonResult createMaterialGroup(@Valid @RequestBody MaterialGroupCreateReqVO createReqVO) { return success(materialGroupService.createMaterialGroup(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新素材分组") //@PreAuthorize("@ss.hasPermission('shop:material-group:update')") public CommonResult updateMaterialGroup(@Valid @RequestBody MaterialGroupUpdateReqVO updateReqVO) { materialGroupService.updateMaterialGroup(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除素材分组") @Parameter(name = "id", description = "编号", required = true) //@PreAuthorize("@ss.hasPermission('shop:material-group:delete')") public CommonResult deleteMaterialGroup(@RequestParam("id") Long id) { materialGroupService.deleteMaterialGroup(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得素材分组") @Parameter(name = "id", description = "编号", required = true, example = "1024") //@PreAuthorize("@ss.hasPermission('shop:material-group:query')") public CommonResult getMaterialGroup(@RequestParam("id") Long id) { MaterialGroupDO materialGroup = materialGroupService.getMaterialGroup(id); return success(MaterialGroupConvert.INSTANCE.convert(materialGroup)); } @GetMapping("/list") @Operation(summary = "获得素材分组列表") //@Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") //@PreAuthorize("@ss.hasPermission('shop:material-group:query')") public CommonResult> getMaterialGroupList() { MaterialGroupExportReqVO exportReqVO = new MaterialGroupExportReqVO(); List list = materialGroupService.getMaterialGroupList(exportReqVO); return success(MaterialGroupConvert.INSTANCE.convertList(list)); } @GetMapping("/page") @Operation(summary = "获得素材分组分页") //@PreAuthorize("@ss.hasPermission('shop:material-group:query')") public CommonResult> getMaterialGroupPage(@Valid MaterialGroupPageReqVO pageVO) { PageResult pageResult = materialGroupService.getMaterialGroupPage(pageVO); return success(MaterialGroupConvert.INSTANCE.convertPage(pageResult)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/materialgroup/vo/MaterialGroupBaseVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.materialgroup.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import jakarta.validation.constraints.*; /** * 素材分组 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class MaterialGroupBaseVO { @Schema(description = "分组名", required = true, example = "赵六") @NotNull(message = "分组名不能为空") private String name; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/materialgroup/vo/MaterialGroupCreateReqVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.materialgroup.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 素材分组创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MaterialGroupCreateReqVO extends MaterialGroupBaseVO { } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/materialgroup/vo/MaterialGroupExcelVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.materialgroup.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.alibaba.excel.annotation.ExcelProperty; /** * 素材分组 Excel VO * * @author yshop */ @Data public class MaterialGroupExcelVO { @ExcelProperty("编号") private Long id; @ExcelProperty("创建时间") private LocalDateTime createTime; @ExcelProperty("分组名") private String name; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/materialgroup/vo/MaterialGroupExportReqVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.materialgroup.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import java.time.LocalDateTime; import org.springframework.format.annotation.DateTimeFormat; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 素材分组 Excel 导出 Request VO,参数和 MaterialGroupPageReqVO 是一致的") @Data public class MaterialGroupExportReqVO { @Schema(description = "创建时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; @Schema(description = "分组名", example = "赵六") private String name; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/materialgroup/vo/MaterialGroupPageReqVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.materialgroup.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 素材分组分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MaterialGroupPageReqVO extends PageParam { @Schema(description = "创建时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; @Schema(description = "分组名", example = "赵六") private String name; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/materialgroup/vo/MaterialGroupRespVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.materialgroup.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; @Schema(description = "管理后台 - 素材分组 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MaterialGroupRespVO extends MaterialGroupBaseVO { @Schema(description = "编号", required = true, example = "1995") private Long id; @Schema(description = "创建时间", required = true) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/materialgroup/vo/MaterialGroupUpdateReqVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.materialgroup.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 素材分组更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MaterialGroupUpdateReqVO extends MaterialGroupBaseVO { @Schema(description = "编号", required = true, example = "1995") @NotNull(message = "编号不能为空") private Long id; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/service/ServiceController.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.service; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import org.springframework.security.access.prepost.PreAuthorize; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.constraints.*; import jakarta.validation.*; import java.util.*; import java.io.IOException; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.pojo.CommonResult; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.shop.controller.admin.service.vo.*; import co.yixiang.yshop.module.shop.dal.dataobject.service.ServiceDO; import co.yixiang.yshop.module.shop.convert.service.ServiceConvert; import co.yixiang.yshop.module.shop.service.service.ServiceService; @Tag(name = "管理后台 - 我的服务") @RestController @RequestMapping("/shop/service") @Validated public class ServiceController { @Resource private ServiceService serviceService; @PostMapping("/create") @Operation(summary = "创建我的服务") @PreAuthorize("@ss.hasPermission('shop:service:create')") public CommonResult createService(@Valid @RequestBody ServiceCreateReqVO createReqVO) { return success(serviceService.createService(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新我的服务") @PreAuthorize("@ss.hasPermission('shop:service:update')") public CommonResult updateService(@Valid @RequestBody ServiceUpdateReqVO updateReqVO) { serviceService.updateService(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除我的服务") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('shop:service:delete')") public CommonResult deleteService(@RequestParam("id") Long id) { serviceService.deleteService(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得我的服务") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('shop:service:query')") public CommonResult getService(@RequestParam("id") Long id) { ServiceDO service = serviceService.getService(id); return success(ServiceConvert.INSTANCE.convert(service)); } @GetMapping("/list") @Operation(summary = "获得我的服务列表") @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") @PreAuthorize("@ss.hasPermission('shop:service:query')") public CommonResult> getServiceList(@RequestParam("ids") Collection ids) { List list = serviceService.getServiceList(ids); return success(ServiceConvert.INSTANCE.convertList(list)); } @GetMapping("/page") @Operation(summary = "获得我的服务分页") @PreAuthorize("@ss.hasPermission('shop:service:query')") public CommonResult> getServicePage(@Valid ServicePageReqVO pageVO) { PageResult pageResult = serviceService.getServicePage(pageVO); return success(ServiceConvert.INSTANCE.convertPage(pageResult)); } @GetMapping("/export-excel") @Operation(summary = "导出我的服务 Excel") @PreAuthorize("@ss.hasPermission('shop:service:export')") public void exportServiceExcel(@Valid ServiceExportReqVO exportReqVO, HttpServletResponse response) throws IOException { List list = serviceService.getServiceList(exportReqVO); // 导出 Excel List datas = ServiceConvert.INSTANCE.convertList02(list); ExcelUtils.write(response, "我的服务.xls", "数据", ServiceExcelVO.class, datas); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/service/vo/ServiceBaseVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.service.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import jakarta.validation.constraints.*; /** * 我的服务 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class ServiceBaseVO { @Schema(description = "标题", required = true, example = "张三") @NotNull(message = "标题不能为空") private String name; @Schema(description = "图标", required = true) @NotNull(message = "图标不能为空") private String image; @Schema(description = "类型:pages=页面,miniprogram=跳转小程序,menu=菜单,content=内容,call=电话", required = true, example = "1") @NotNull(message = "类型不能为空") private String type; @Schema(description = "详情", required = true) private String content; @Schema(description = "父级id", required = true, example = "25371") private Integer pid; @Schema(description = "小程序app_id", required = true, example = "20728") private String appId; @Schema(description = "页面路径", required = true) private String pages; @Schema(description = "电话", required = true) private String phone; @Schema(description = "权重", required = true) private Integer weigh; @Schema(description = "状态:0=下架,1=上架", required = true, example = "2") private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/service/vo/ServiceCreateReqVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.service.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 我的服务创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ServiceCreateReqVO extends ServiceBaseVO { } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/service/vo/ServiceExcelVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.service.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.alibaba.excel.annotation.ExcelProperty; /** * 我的服务 Excel VO * * @author yshop */ @Data public class ServiceExcelVO { @ExcelProperty("id") private Long id; @ExcelProperty("标题") private String name; @ExcelProperty("图标") private String image; @ExcelProperty("类型:pages=页面,miniprogram=跳转小程序,menu=菜单,content=内容,call=电话") private String type; @ExcelProperty("详情") private String content; @ExcelProperty("父级id") private Integer pid; @ExcelProperty("小程序app_id") private String appId; @ExcelProperty("页面路径") private String pages; @ExcelProperty("电话") private String phone; @ExcelProperty("权重") private Integer weigh; @ExcelProperty("状态:0=下架,1=上架") private Integer status; @ExcelProperty("添加时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/service/vo/ServiceExportReqVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.service.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import java.time.LocalDateTime; import org.springframework.format.annotation.DateTimeFormat; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 我的服务 Excel 导出 Request VO,参数和 ServicePageReqVO 是一致的") @Data public class ServiceExportReqVO { @Schema(description = "标题", example = "张三") private String name; @Schema(description = "图标") private String image; @Schema(description = "类型:pages=页面,miniprogram=跳转小程序,menu=菜单,content=内容,call=电话", example = "1") private String type; @Schema(description = "详情") private String content; @Schema(description = "父级id", example = "25371") private Integer pid; @Schema(description = "小程序app_id", example = "20728") private String appId; @Schema(description = "页面路径") private String pages; @Schema(description = "电话") private String phone; @Schema(description = "权重") private Integer weigh; @Schema(description = "状态:0=下架,1=上架", example = "2") private Boolean status; @Schema(description = "添加时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/service/vo/ServicePageReqVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.service.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 我的服务分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ServicePageReqVO extends PageParam { @Schema(description = "标题", example = "张三") private String name; @Schema(description = "图标") private String image; @Schema(description = "类型:pages=页面,miniprogram=跳转小程序,menu=菜单,content=内容,call=电话", example = "1") private String type; @Schema(description = "详情") private String content; @Schema(description = "父级id", example = "25371") private Integer pid; @Schema(description = "小程序app_id", example = "20728") private String appId; @Schema(description = "页面路径") private String pages; @Schema(description = "电话") private String phone; @Schema(description = "权重") private Integer weigh; @Schema(description = "状态:0=下架,1=上架", example = "2") private Boolean status; @Schema(description = "添加时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/service/vo/ServiceRespVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.service.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; @Schema(description = "管理后台 - 我的服务 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ServiceRespVO extends ServiceBaseVO { @Schema(description = "id", required = true, example = "6335") private Long id; @Schema(description = "添加时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/service/vo/ServiceUpdateReqVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.service.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 我的服务更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ServiceUpdateReqVO extends ServiceBaseVO { @Schema(description = "id", required = true, example = "6335") @NotNull(message = "id不能为空") private Long id; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/shopads/ShopAdsController.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.shopads; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import org.springframework.security.access.prepost.PreAuthorize; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.constraints.*; import jakarta.validation.*; import java.util.*; import java.io.IOException; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.pojo.CommonResult; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.shop.controller.admin.shopads.vo.*; import co.yixiang.yshop.module.shop.dal.dataobject.shopads.ShopAdsDO; import co.yixiang.yshop.module.shop.convert.shopads.ShopAdsConvert; import co.yixiang.yshop.module.shop.service.shopads.ShopAdsService; @Tag(name = "管理后台 - 广告图管理") @RestController @RequestMapping("/shop/ads") @Validated public class ShopAdsController { @Resource private ShopAdsService adsService; @PostMapping("/create") @Operation(summary = "创建广告图管理") @PreAuthorize("@ss.hasPermission('shop:ads:create')") public CommonResult createAds(@Valid @RequestBody ShopAdsCreateReqVO createReqVO) { return success(adsService.createAds(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新广告图管理") @PreAuthorize("@ss.hasPermission('shop:ads:update')") public CommonResult updateAds(@Valid @RequestBody ShopAdsUpdateReqVO updateReqVO) { adsService.updateAds(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除广告图管理") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('shop:ads:delete')") public CommonResult deleteAds(@RequestParam("id") Long id) { adsService.deleteAds(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得广告图管理") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('shop:ads:query')") public CommonResult getAds(@RequestParam("id") Long id) { ShopAdsDO ads = adsService.getAds(id); return success(ShopAdsConvert.INSTANCE.convert(ads)); } @GetMapping("/list") @Operation(summary = "获得广告图管理列表") @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") @PreAuthorize("@ss.hasPermission('shop:ads:query')") public CommonResult> getAdsList(@RequestParam("ids") Collection ids) { List list = adsService.getAdsList(ids); return success(ShopAdsConvert.INSTANCE.convertList(list)); } @GetMapping("/page") @Operation(summary = "获得广告图管理分页") @PreAuthorize("@ss.hasPermission('shop:ads:query')") public CommonResult> getAdsPage(@Valid ShopAdsPageReqVO pageVO) { PageResult pageResult = adsService.getAdsPage(pageVO); return success(ShopAdsConvert.INSTANCE.convertPage(pageResult)); } @GetMapping("/export-excel") @Operation(summary = "导出广告图管理 Excel") @PreAuthorize("@ss.hasPermission('shop:ads:export')") public void exportAdsExcel(@Valid ShopAdsExportReqVO exportReqVO, HttpServletResponse response) throws IOException { List list = adsService.getAdsList(exportReqVO); // 导出 Excel List datas = ShopAdsConvert.INSTANCE.convertList02(list); ExcelUtils.write(response, "广告图管理.xls", "数据", ShopAdsExcelVO.class, datas); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/shopads/vo/ShopAdsBaseVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.shopads.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import jakarta.validation.constraints.*; /** * 广告图管理 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class ShopAdsBaseVO { @Schema(description = "图片") private String image; @Schema(description = "是否显现", required = true) @NotNull(message = "是否显现不能为空") private Integer isSwitch; @Schema(description = "权重", required = true) @NotNull(message = "权重不能为空") private Integer weigh; @Schema(description = "所支持的店铺id用','区分,0代表全部", required = true, example = "2901") @NotNull(message = "所支持的店铺id用','区分,0代表全部不能为空") private String shopId; private String shopName; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/shopads/vo/ShopAdsCreateReqVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.shopads.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 广告图管理创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ShopAdsCreateReqVO extends ShopAdsBaseVO { } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/shopads/vo/ShopAdsExcelVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.shopads.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.alibaba.excel.annotation.ExcelProperty; /** * 广告图管理 Excel VO * * @author yshop */ @Data public class ShopAdsExcelVO { @ExcelProperty("id") private Long id; @ExcelProperty("图片") private String image; @ExcelProperty("是否显现") private Integer isSwitch; @ExcelProperty("权重") private Integer weigh; @ExcelProperty("所支持的店铺id用','区分,0代表全部") private String shopId; @ExcelProperty("添加时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/shopads/vo/ShopAdsExportReqVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.shopads.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import java.time.LocalDateTime; import org.springframework.format.annotation.DateTimeFormat; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 广告图管理 Excel 导出 Request VO,参数和 ShopAdsPageReqVO 是一致的") @Data public class ShopAdsExportReqVO { @Schema(description = "图片") private String image; @Schema(description = "是否显现") private Integer isSwitch; @Schema(description = "权重") private Integer weigh; @Schema(description = "所支持的店铺id用','区分,0代表全部", example = "2901") private String shopId; @Schema(description = "添加时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/shopads/vo/ShopAdsPageReqVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.shopads.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 广告图管理分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ShopAdsPageReqVO extends PageParam { @Schema(description = "图片") private String image; @Schema(description = "是否显现") private Integer isSwitch; @Schema(description = "权重") private Integer weigh; @Schema(description = "所支持的店铺id用','区分,0代表全部", example = "2901") private String shopName; @Schema(description = "添加时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/shopads/vo/ShopAdsRespVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.shopads.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; @Schema(description = "管理后台 - 广告图管理 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ShopAdsRespVO extends ShopAdsBaseVO { @Schema(description = "id", required = true, example = "24753") private Long id; @Schema(description = "添加时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/admin/shopads/vo/ShopAdsUpdateReqVO.java ================================================ package co.yixiang.yshop.module.shop.controller.admin.shopads.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 广告图管理更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ShopAdsUpdateReqVO extends ShopAdsBaseVO { @Schema(description = "id", required = true, example = "24753") @NotNull(message = "id不能为空") private Long id; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/app/ad/AppAdController.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co * 注意: * 本软件为www.yixiang.co开发研制,未经购买不得使用 * 购买后可获得全部源代码(禁止转卖、分享、上传到码云、github等开源平台) * 一经发现盗用、分享等行为,将追究法律责任,后果自负 */ package co.yixiang.yshop.module.shop.controller.app.ad; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.security.core.annotations.PreAuthenticated; import co.yixiang.yshop.module.shop.controller.app.ad.vo.AppShopAdsVO; import co.yixiang.yshop.module.shop.convert.shopads.ShopAdsConvert; import co.yixiang.yshop.module.shop.dal.dataobject.shopads.ShopAdsDO; import co.yixiang.yshop.module.shop.service.shopads.AppShopAdsService; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.List; import java.util.Map; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; /** *

* 首页广告控制器 *

* * @author hupeng * @since 2023-8-10 */ @Slf4j @RestController @Tag(name = "用户 APP - 广告") @RequiredArgsConstructor(onConstructor = @__(@Autowired)) @RequestMapping("/ad") public class AppAdController { private final AppShopAdsService appShopAdsService; @Value("${yshop.info.isActive}") private Boolean isActive; @GetMapping("/list") @Operation(summary = "查询广告列表") @Parameter(name = "shop_id", description = "店铺id", required = true, example = "10 ") public CommonResult> getList(@RequestParam("shop_id") Long shopId) { // List appShopAdsVOS = appShopAdsService.list(new LambdaQueryWrapper() // .eq(ShopAdsDO::getId,0).or().apply(shopId > 0, // "FIND_IN_SET ('" + shopId + "',shop_id)")); List appShopAdsVOS = appShopAdsService.list(new LambdaQueryWrapper() .eq(ShopAdsDO::getShopId,0).or().eq(ShopAdsDO::getShopId,shopId)); Map map = new HashMap<>(); map.put("list",ShopAdsConvert.INSTANCE.convertList03(appShopAdsVOS)); map.put("isActive",isActive); return success(map); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/app/ad/vo/AppShopAdsVO.java ================================================ package co.yixiang.yshop.module.shop.controller.app.ad.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotNull; /** * 广告图VO */ @Data public class AppShopAdsVO { @Schema(description = "id", required = true, example = "24753") private Long id; @Schema(description = "图片") private String image; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/app/service/AppServiceController.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co * 注意: * 本软件为www.yixiang.co开发研制,未经购买不得使用 * 购买后可获得全部源代码(禁止转卖、分享、上传到码云、github等开源平台) * 一经发现盗用、分享等行为,将追究法律责任,后果自负 */ package co.yixiang.yshop.module.shop.controller.app.service; import co.yixiang.yshop.framework.common.enums.ShopCommonEnum; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.module.shop.controller.app.service.vo.AppServiceVO; import co.yixiang.yshop.module.shop.convert.service.ServiceConvert; import co.yixiang.yshop.module.shop.dal.dataobject.service.ServiceDO; import co.yixiang.yshop.module.shop.service.service.AppServiceService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; /** *

* 首页广告控制器 *

* * @author hupeng * @since 2023-8-11 */ @Slf4j @RestController @Tag(name = "用户 APP - 服务菜单") @RequiredArgsConstructor(onConstructor = @__(@Autowired)) @RequestMapping("/service") public class AppServiceController { private final AppServiceService appServiceService; @GetMapping("/list") @Operation(summary = "服务菜单列表") public CommonResult> getList() { List appShopAdsVOS = appServiceService.list(new LambdaQueryWrapper() .eq(ServiceDO::getStatus, ShopCommonEnum.IS_STATUS_1.getValue()).orderByDesc(ServiceDO::getWeigh)); return success(ServiceConvert.INSTANCE.convertList03(appShopAdsVOS)); } @GetMapping("/content") @Operation(summary = "服务菜单列表") public CommonResult getContent(@RequestParam("id") Integer id) { ServiceDO appShopAdsVO = appServiceService.getById(id); return success(ServiceConvert.INSTANCE.convert03(appShopAdsVO)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/controller/app/service/vo/AppServiceVO.java ================================================ package co.yixiang.yshop.module.shop.controller.app.service.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; /** * 我的服务 */ @Data public class AppServiceVO { @Schema(description = "id", required = true, example = "6335") private Long id; @Schema(description = "标题", required = true, example = "张三") private String name; @Schema(description = "图标", required = true) private String image; @Schema(description = "类型:pages=页面,miniprogram=跳转小程序,menu=菜单,content=内容,call=电话", required = true, example = "1") private String type; @Schema(description = "详情", required = true) private String content; @Schema(description = "父级id", required = true, example = "25371") private Integer pid; @Schema(description = "小程序app_id", required = true, example = "20728") private String appId; @Schema(description = "页面路径", required = true) private String pages; @Schema(description = "电话", required = true) private String phone; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/convert/material/MaterialConvert.java ================================================ package co.yixiang.yshop.module.shop.convert.material; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import co.yixiang.yshop.module.shop.controller.admin.material.vo.*; import co.yixiang.yshop.module.shop.dal.dataobject.material.MaterialDO; /** * 素材库 Convert * * @author yshop */ @Mapper public interface MaterialConvert { MaterialConvert INSTANCE = Mappers.getMapper(MaterialConvert.class); MaterialDO convert(MaterialCreateReqVO bean); MaterialDO convert(MaterialUpdateReqVO bean); MaterialRespVO convert(MaterialDO bean); List convertList(List list); PageResult convertPage(PageResult page); List convertList02(List list); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/convert/materialgroup/MaterialGroupConvert.java ================================================ package co.yixiang.yshop.module.shop.convert.materialgroup; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import co.yixiang.yshop.module.shop.controller.admin.materialgroup.vo.*; import co.yixiang.yshop.module.shop.dal.dataobject.materialgroup.MaterialGroupDO; /** * 素材分组 Convert * * @author yshop */ @Mapper public interface MaterialGroupConvert { MaterialGroupConvert INSTANCE = Mappers.getMapper(MaterialGroupConvert.class); MaterialGroupDO convert(MaterialGroupCreateReqVO bean); MaterialGroupDO convert(MaterialGroupUpdateReqVO bean); MaterialGroupRespVO convert(MaterialGroupDO bean); List convertList(List list); PageResult convertPage(PageResult page); List convertList02(List list); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/convert/service/ServiceConvert.java ================================================ package co.yixiang.yshop.module.shop.convert.service; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.shop.controller.app.service.vo.AppServiceVO; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import co.yixiang.yshop.module.shop.controller.admin.service.vo.*; import co.yixiang.yshop.module.shop.dal.dataobject.service.ServiceDO; /** * 我的服务 Convert * * @author yshop */ @Mapper public interface ServiceConvert { ServiceConvert INSTANCE = Mappers.getMapper(ServiceConvert.class); ServiceDO convert(ServiceCreateReqVO bean); ServiceDO convert(ServiceUpdateReqVO bean); ServiceRespVO convert(ServiceDO bean); List convertList(List list); List convertList03(List list); AppServiceVO convert03(ServiceDO bean); PageResult convertPage(PageResult page); List convertList02(List list); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/convert/shopads/ShopAdsConvert.java ================================================ package co.yixiang.yshop.module.shop.convert.shopads; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.shop.controller.app.ad.vo.AppShopAdsVO; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import co.yixiang.yshop.module.shop.controller.admin.shopads.vo.*; import co.yixiang.yshop.module.shop.dal.dataobject.shopads.ShopAdsDO; /** * 广告图管理 Convert * * @author yshop */ @Mapper public interface ShopAdsConvert { ShopAdsConvert INSTANCE = Mappers.getMapper(ShopAdsConvert.class); ShopAdsDO convert(ShopAdsCreateReqVO bean); ShopAdsDO convert(ShopAdsUpdateReqVO bean); ShopAdsRespVO convert(ShopAdsDO bean); List convertList(List list); List convertList03(List list); PageResult convertPage(PageResult page); List convertList02(List list); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/dal/dataobject/material/MaterialDO.java ================================================ package co.yixiang.yshop.module.shop.dal.dataobject.material; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; /** * 素材库 DO * * @author yshop */ @TableName("yshop_material") @KeySequence("yshop_material_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class MaterialDO extends BaseDO { /** * 编号 */ @TableId private Long id; /** * 类型1、图片;2、视频 */ private String type; /** * 分组ID */ private String groupId; /** * 素材名 */ private String name; /** * 素材链接 */ private String url; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/dal/dataobject/materialgroup/MaterialGroupDO.java ================================================ package co.yixiang.yshop.module.shop.dal.dataobject.materialgroup; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; /** * 素材分组 DO * * @author yshop */ @TableName("yshop_material_group") @KeySequence("yshop_material_group_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class MaterialGroupDO extends BaseDO { /** * 编号 */ @TableId private Long id; /** * 分组名 */ private String name; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/dal/dataobject/service/ServiceDO.java ================================================ package co.yixiang.yshop.module.shop.dal.dataobject.service; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; /** * 我的服务 DO * * @author yshop */ @TableName("yshop_service") @KeySequence("yshop_service_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class ServiceDO extends BaseDO { /** * id */ @TableId private Long id; /** * 标题 */ private String name; /** * 图标 */ private String image; /** * 类型:pages=页面,miniprogram=跳转小程序,menu=菜单,content=内容,call=电话 */ private String type; /** * 详情 */ private String content; /** * 父级id */ private Integer pid; /** * 小程序app_id */ private String appId; /** * 页面路径 */ private String pages; /** * 电话 */ private String phone; /** * 权重 */ private Integer weigh; /** * 状态:0=下架,1=上架 */ private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/dal/dataobject/shopads/ShopAdsDO.java ================================================ package co.yixiang.yshop.module.shop.dal.dataobject.shopads; import co.yixiang.yshop.framework.mybatis.core.type.StringListTypeHandler; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; /** * 广告图管理 DO * * @author yshop */ @TableName(value = "yshop_shop_ads") @KeySequence("yshop_shop_ads_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class ShopAdsDO extends BaseDO { /** * id */ @TableId private Long id; /** * 图片 */ private String image; /** * 是否显现 */ private Integer isSwitch; /** * 权重 */ private Integer weigh; /** * 所支持的店铺id用','区分,0代表全部 */ private String shopId; private String shopName; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/dal/mysql/material/MaterialMapper.java ================================================ package co.yixiang.yshop.module.shop.dal.mysql.material; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.shop.dal.dataobject.material.MaterialDO; import org.apache.ibatis.annotations.Mapper; import co.yixiang.yshop.module.shop.controller.admin.material.vo.*; /** * 素材库 Mapper * * @author yshop */ @Mapper public interface MaterialMapper extends BaseMapperX { default PageResult selectPage(MaterialPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .betweenIfPresent(MaterialDO::getCreateTime, reqVO.getCreateTime()) .eqIfPresent(MaterialDO::getType, reqVO.getType()) .eqIfPresent(MaterialDO::getGroupId, reqVO.getGroupId()) .likeIfPresent(MaterialDO::getName, reqVO.getName()) .eqIfPresent(MaterialDO::getUrl, reqVO.getUrl()) .orderByDesc(MaterialDO::getId)); } default List selectList(MaterialExportReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .betweenIfPresent(MaterialDO::getCreateTime, reqVO.getCreateTime()) .eqIfPresent(MaterialDO::getType, reqVO.getType()) .eqIfPresent(MaterialDO::getGroupId, reqVO.getGroupId()) .likeIfPresent(MaterialDO::getName, reqVO.getName()) .eqIfPresent(MaterialDO::getUrl, reqVO.getUrl()) .orderByDesc(MaterialDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/dal/mysql/materialgroup/MaterialGroupMapper.java ================================================ package co.yixiang.yshop.module.shop.dal.mysql.materialgroup; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.shop.dal.dataobject.materialgroup.MaterialGroupDO; import org.apache.ibatis.annotations.Mapper; import co.yixiang.yshop.module.shop.controller.admin.materialgroup.vo.*; /** * 素材分组 Mapper * * @author yshop */ @Mapper public interface MaterialGroupMapper extends BaseMapperX { default PageResult selectPage(MaterialGroupPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .betweenIfPresent(MaterialGroupDO::getCreateTime, reqVO.getCreateTime()) .likeIfPresent(MaterialGroupDO::getName, reqVO.getName()) .orderByDesc(MaterialGroupDO::getId)); } default List selectList(MaterialGroupExportReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .betweenIfPresent(MaterialGroupDO::getCreateTime, reqVO.getCreateTime()) .likeIfPresent(MaterialGroupDO::getName, reqVO.getName()) .orderByDesc(MaterialGroupDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/dal/mysql/service/ServiceMapper.java ================================================ package co.yixiang.yshop.module.shop.dal.mysql.service; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.shop.dal.dataobject.service.ServiceDO; import org.apache.ibatis.annotations.Mapper; import co.yixiang.yshop.module.shop.controller.admin.service.vo.*; /** * 我的服务 Mapper * * @author yshop */ @Mapper public interface ServiceMapper extends BaseMapperX { default PageResult selectPage(ServicePageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(ServiceDO::getName, reqVO.getName()) .eqIfPresent(ServiceDO::getImage, reqVO.getImage()) .eqIfPresent(ServiceDO::getType, reqVO.getType()) .eqIfPresent(ServiceDO::getContent, reqVO.getContent()) .eqIfPresent(ServiceDO::getPid, reqVO.getPid()) .eqIfPresent(ServiceDO::getAppId, reqVO.getAppId()) .eqIfPresent(ServiceDO::getPages, reqVO.getPages()) .eqIfPresent(ServiceDO::getPhone, reqVO.getPhone()) .eqIfPresent(ServiceDO::getWeigh, reqVO.getWeigh()) .eqIfPresent(ServiceDO::getStatus, reqVO.getStatus()) .betweenIfPresent(ServiceDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(ServiceDO::getId)); } default List selectList(ServiceExportReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .likeIfPresent(ServiceDO::getName, reqVO.getName()) .eqIfPresent(ServiceDO::getImage, reqVO.getImage()) .eqIfPresent(ServiceDO::getType, reqVO.getType()) .eqIfPresent(ServiceDO::getContent, reqVO.getContent()) .eqIfPresent(ServiceDO::getPid, reqVO.getPid()) .eqIfPresent(ServiceDO::getAppId, reqVO.getAppId()) .eqIfPresent(ServiceDO::getPages, reqVO.getPages()) .eqIfPresent(ServiceDO::getPhone, reqVO.getPhone()) .eqIfPresent(ServiceDO::getWeigh, reqVO.getWeigh()) .eqIfPresent(ServiceDO::getStatus, reqVO.getStatus()) .betweenIfPresent(ServiceDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(ServiceDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/dal/mysql/shopads/ShopAdsMapper.java ================================================ package co.yixiang.yshop.module.shop.dal.mysql.shopads; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.shop.dal.dataobject.shopads.ShopAdsDO; import org.apache.ibatis.annotations.Mapper; import co.yixiang.yshop.module.shop.controller.admin.shopads.vo.*; /** * 广告图管理 Mapper * * @author yshop */ @Mapper public interface ShopAdsMapper extends BaseMapperX { default PageResult selectPage(ShopAdsPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(ShopAdsDO::getImage, reqVO.getImage()) .eqIfPresent(ShopAdsDO::getIsSwitch, reqVO.getIsSwitch()) .eqIfPresent(ShopAdsDO::getWeigh, reqVO.getWeigh()) .likeIfPresent(ShopAdsDO::getShopName, reqVO.getShopName()) .betweenIfPresent(ShopAdsDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(ShopAdsDO::getId)); } default List selectList(ShopAdsExportReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .eqIfPresent(ShopAdsDO::getImage, reqVO.getImage()) .eqIfPresent(ShopAdsDO::getIsSwitch, reqVO.getIsSwitch()) .eqIfPresent(ShopAdsDO::getWeigh, reqVO.getWeigh()) .eqIfPresent(ShopAdsDO::getShopId, reqVO.getShopId()) .betweenIfPresent(ShopAdsDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(ShopAdsDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/framework/package-info.java ================================================ /** * 属于 promotion 模块的 framework 封装 * * @author yshop */ package co.yixiang.yshop.module.shop.framework; ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/framework/web/config/ShopWebConfiguration.java ================================================ package co.yixiang.yshop.module.shop.framework.web.config; import co.yixiang.yshop.framework.swagger.config.YshopSwaggerAutoConfiguration; import org.springdoc.core.models.GroupedOpenApi; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * shop 模块的 web 组件的 Configuration * * @author yshop */ @Configuration(proxyBeanMethods = false) public class ShopWebConfiguration { /** * promotion 模块的 API 分组 */ @Bean public GroupedOpenApi shopGroupedOpenApi() { return YshopSwaggerAutoConfiguration.buildGroupedOpenApi("shop"); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/framework/web/package-info.java ================================================ /** * promotion 模块的 web 配置 */ package co.yixiang.yshop.module.shop.framework.web; ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/service/material/MaterialService.java ================================================ package co.yixiang.yshop.module.shop.service.material; import java.util.*; import jakarta.validation.*; import co.yixiang.yshop.module.shop.controller.admin.material.vo.*; import co.yixiang.yshop.module.shop.dal.dataobject.material.MaterialDO; import co.yixiang.yshop.framework.common.pojo.PageResult; /** * 素材库 Service 接口 * * @author yshop */ public interface MaterialService { /** * 创建素材库 * * @param createReqVO 创建信息 * @return 编号 */ Long createMaterial(@Valid MaterialCreateReqVO createReqVO); /** * 更新素材库 * * @param updateReqVO 更新信息 */ void updateMaterial(@Valid MaterialUpdateReqVO updateReqVO); /** * 删除素材库 * * @param id 编号 */ void deleteMaterial(Long id); /** * 获得素材库 * * @param id 编号 * @return 素材库 */ MaterialDO getMaterial(Long id); /** * 获得素材库列表 * * @param ids 编号 * @return 素材库列表 */ List getMaterialList(Collection ids); /** * 获得素材库分页 * * @param pageReqVO 分页查询 * @return 素材库分页 */ PageResult getMaterialPage(MaterialPageReqVO pageReqVO); /** * 获得素材库列表, 用于 Excel 导出 * * @param exportReqVO 查询条件 * @return 素材库列表 */ List getMaterialList(MaterialExportReqVO exportReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/service/material/MaterialServiceImpl.java ================================================ package co.yixiang.yshop.module.shop.service.material; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import java.util.*; import co.yixiang.yshop.module.shop.controller.admin.material.vo.*; import co.yixiang.yshop.module.shop.dal.dataobject.material.MaterialDO; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.shop.convert.material.MaterialConvert; import co.yixiang.yshop.module.shop.dal.mysql.material.MaterialMapper; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.shop.enums.ErrorCodeConstants.*; /** * 素材库 Service 实现类 * * @author yshop */ @Service @Validated public class MaterialServiceImpl implements MaterialService { @Resource private MaterialMapper materialMapper; @Override public Long createMaterial(MaterialCreateReqVO createReqVO) { // 插入 MaterialDO material = MaterialConvert.INSTANCE.convert(createReqVO); materialMapper.insert(material); // 返回 return material.getId(); } @Override public void updateMaterial(MaterialUpdateReqVO updateReqVO) { // 校验存在 validateMaterialExists(updateReqVO.getId()); // 更新 MaterialDO updateObj = MaterialConvert.INSTANCE.convert(updateReqVO); materialMapper.updateById(updateObj); } @Override public void deleteMaterial(Long id) { // 校验存在 validateMaterialExists(id); // 删除 materialMapper.deleteById(id); } private void validateMaterialExists(Long id) { if (materialMapper.selectById(id) == null) { throw exception(MATERIAL_NOT_EXISTS); } } @Override public MaterialDO getMaterial(Long id) { return materialMapper.selectById(id); } @Override public List getMaterialList(Collection ids) { return materialMapper.selectBatchIds(ids); } @Override public PageResult getMaterialPage(MaterialPageReqVO pageReqVO) { return materialMapper.selectPage(pageReqVO); } @Override public List getMaterialList(MaterialExportReqVO exportReqVO) { return materialMapper.selectList(exportReqVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/service/materialgroup/MaterialGroupService.java ================================================ package co.yixiang.yshop.module.shop.service.materialgroup; import java.util.*; import jakarta.validation.*; import co.yixiang.yshop.module.shop.controller.admin.materialgroup.vo.*; import co.yixiang.yshop.module.shop.dal.dataobject.materialgroup.MaterialGroupDO; import co.yixiang.yshop.framework.common.pojo.PageResult; /** * 素材分组 Service 接口 * * @author yshop */ public interface MaterialGroupService { /** * 创建素材分组 * * @param createReqVO 创建信息 * @return 编号 */ Long createMaterialGroup(@Valid MaterialGroupCreateReqVO createReqVO); /** * 更新素材分组 * * @param updateReqVO 更新信息 */ void updateMaterialGroup(@Valid MaterialGroupUpdateReqVO updateReqVO); /** * 删除素材分组 * * @param id 编号 */ void deleteMaterialGroup(Long id); /** * 获得素材分组 * * @param id 编号 * @return 素材分组 */ MaterialGroupDO getMaterialGroup(Long id); /** * 获得素材分组列表 * * @param ids 编号 * @return 素材分组列表 */ List getMaterialGroupList(Collection ids); /** * 获得素材分组分页 * * @param pageReqVO 分页查询 * @return 素材分组分页 */ PageResult getMaterialGroupPage(MaterialGroupPageReqVO pageReqVO); /** * 获得素材分组列表, 用于 Excel 导出 * * @param exportReqVO 查询条件 * @return 素材分组列表 */ List getMaterialGroupList(MaterialGroupExportReqVO exportReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/service/materialgroup/MaterialGroupServiceImpl.java ================================================ package co.yixiang.yshop.module.shop.service.materialgroup; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import java.util.*; import co.yixiang.yshop.module.shop.controller.admin.materialgroup.vo.*; import co.yixiang.yshop.module.shop.dal.dataobject.materialgroup.MaterialGroupDO; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.shop.convert.materialgroup.MaterialGroupConvert; import co.yixiang.yshop.module.shop.dal.mysql.materialgroup.MaterialGroupMapper; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.shop.enums.ErrorCodeConstants.*; /** * 素材分组 Service 实现类 * * @author yshop */ @Service @Validated public class MaterialGroupServiceImpl implements MaterialGroupService { @Resource private MaterialGroupMapper materialGroupMapper; @Override public Long createMaterialGroup(MaterialGroupCreateReqVO createReqVO) { // 插入 MaterialGroupDO materialGroup = MaterialGroupConvert.INSTANCE.convert(createReqVO); materialGroupMapper.insert(materialGroup); // 返回 return materialGroup.getId(); } @Override public void updateMaterialGroup(MaterialGroupUpdateReqVO updateReqVO) { // 校验存在 validateMaterialGroupExists(updateReqVO.getId()); // 更新 MaterialGroupDO updateObj = MaterialGroupConvert.INSTANCE.convert(updateReqVO); materialGroupMapper.updateById(updateObj); } @Override public void deleteMaterialGroup(Long id) { // 校验存在 validateMaterialGroupExists(id); // 删除 materialGroupMapper.deleteById(id); } private void validateMaterialGroupExists(Long id) { if (materialGroupMapper.selectById(id) == null) { throw exception(MATERIAL_GROUP_NOT_EXISTS); } } @Override public MaterialGroupDO getMaterialGroup(Long id) { return materialGroupMapper.selectById(id); } @Override public List getMaterialGroupList(Collection ids) { return materialGroupMapper.selectBatchIds(ids); } @Override public PageResult getMaterialGroupPage(MaterialGroupPageReqVO pageReqVO) { return materialGroupMapper.selectPage(pageReqVO); } @Override public List getMaterialGroupList(MaterialGroupExportReqVO exportReqVO) { return materialGroupMapper.selectList(exportReqVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/service/service/AppServiceService.java ================================================ package co.yixiang.yshop.module.shop.service.service; import co.yixiang.yshop.module.shop.dal.dataobject.service.ServiceDO; import com.baomidou.mybatisplus.extension.service.IService; /** * 我的服务 Service 接口 * * @author yshop */ public interface AppServiceService extends IService { } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/service/service/AppServiceServiceImpl.java ================================================ package co.yixiang.yshop.module.shop.service.service; import co.yixiang.yshop.module.shop.dal.dataobject.service.ServiceDO; import co.yixiang.yshop.module.shop.dal.mysql.service.ServiceMapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; /** * 我的服务 Service 实现类 * * @author yshop */ @Service @Validated public class AppServiceServiceImpl extends ServiceImpl implements AppServiceService { @Resource private ServiceMapper serviceMapper; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/service/service/ServiceService.java ================================================ package co.yixiang.yshop.module.shop.service.service; import java.util.*; import jakarta.validation.*; import co.yixiang.yshop.module.shop.controller.admin.service.vo.*; import co.yixiang.yshop.module.shop.dal.dataobject.service.ServiceDO; import co.yixiang.yshop.framework.common.pojo.PageResult; /** * 我的服务 Service 接口 * * @author yshop */ public interface ServiceService { /** * 创建我的服务 * * @param createReqVO 创建信息 * @return 编号 */ Long createService(@Valid ServiceCreateReqVO createReqVO); /** * 更新我的服务 * * @param updateReqVO 更新信息 */ void updateService(@Valid ServiceUpdateReqVO updateReqVO); /** * 删除我的服务 * * @param id 编号 */ void deleteService(Long id); /** * 获得我的服务 * * @param id 编号 * @return 我的服务 */ ServiceDO getService(Long id); /** * 获得我的服务列表 * * @param ids 编号 * @return 我的服务列表 */ List getServiceList(Collection ids); /** * 获得我的服务分页 * * @param pageReqVO 分页查询 * @return 我的服务分页 */ PageResult getServicePage(ServicePageReqVO pageReqVO); /** * 获得我的服务列表, 用于 Excel 导出 * * @param exportReqVO 查询条件 * @return 我的服务列表 */ List getServiceList(ServiceExportReqVO exportReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/service/service/ServiceServiceImpl.java ================================================ package co.yixiang.yshop.module.shop.service.service; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import java.util.*; import co.yixiang.yshop.module.shop.controller.admin.service.vo.*; import co.yixiang.yshop.module.shop.dal.dataobject.service.ServiceDO; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.shop.convert.service.ServiceConvert; import co.yixiang.yshop.module.shop.dal.mysql.service.ServiceMapper; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.shop.enums.ErrorCodeConstants.*; /** * 我的服务 Service 实现类 * * @author yshop */ @Service @Validated public class ServiceServiceImpl implements ServiceService { @Resource private ServiceMapper serviceMapper; @Override public Long createService(ServiceCreateReqVO createReqVO) { // 插入 ServiceDO service = ServiceConvert.INSTANCE.convert(createReqVO); serviceMapper.insert(service); // 返回 return service.getId(); } @Override public void updateService(ServiceUpdateReqVO updateReqVO) { // 校验存在 validateServiceExists(updateReqVO.getId()); // 更新 ServiceDO updateObj = ServiceConvert.INSTANCE.convert(updateReqVO); serviceMapper.updateById(updateObj); } @Override public void deleteService(Long id) { // 校验存在 validateServiceExists(id); // 删除 serviceMapper.deleteById(id); } private void validateServiceExists(Long id) { if (serviceMapper.selectById(id) == null) { throw exception(SERVICE_NOT_EXISTS); } } @Override public ServiceDO getService(Long id) { return serviceMapper.selectById(id); } @Override public List getServiceList(Collection ids) { return serviceMapper.selectBatchIds(ids); } @Override public PageResult getServicePage(ServicePageReqVO pageReqVO) { return serviceMapper.selectPage(pageReqVO); } @Override public List getServiceList(ServiceExportReqVO exportReqVO) { return serviceMapper.selectList(exportReqVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/service/shopads/AppShopAdsService.java ================================================ package co.yixiang.yshop.module.shop.service.shopads; import co.yixiang.yshop.module.shop.dal.dataobject.shopads.ShopAdsDO; import com.baomidou.mybatisplus.extension.service.IService; /** * 广告图管理 Service 接口 * * @author yshop */ public interface AppShopAdsService extends IService { } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/service/shopads/AppShopAdsServiceImpl.java ================================================ package co.yixiang.yshop.module.shop.service.shopads; import co.yixiang.yshop.module.shop.dal.dataobject.shopads.ShopAdsDO; import co.yixiang.yshop.module.shop.dal.mysql.shopads.ShopAdsMapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; /** * 商品 AppShopAds 实现类 * * @author yshop */ @Service @Validated public class AppShopAdsServiceImpl extends ServiceImpl implements AppShopAdsService { } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/service/shopads/ShopAdsService.java ================================================ package co.yixiang.yshop.module.shop.service.shopads; import java.util.*; import jakarta.validation.*; import co.yixiang.yshop.module.shop.controller.admin.shopads.vo.*; import co.yixiang.yshop.module.shop.dal.dataobject.shopads.ShopAdsDO; import co.yixiang.yshop.framework.common.pojo.PageResult; /** * 广告图管理 Service 接口 * * @author yshop */ public interface ShopAdsService { /** * 创建广告图管理 * * @param createReqVO 创建信息 * @return 编号 */ Long createAds(@Valid ShopAdsCreateReqVO createReqVO); /** * 更新广告图管理 * * @param updateReqVO 更新信息 */ void updateAds(@Valid ShopAdsUpdateReqVO updateReqVO); /** * 删除广告图管理 * * @param id 编号 */ void deleteAds(Long id); /** * 获得广告图管理 * * @param id 编号 * @return 广告图管理 */ ShopAdsDO getAds(Long id); /** * 获得广告图管理列表 * * @param ids 编号 * @return 广告图管理列表 */ List getAdsList(Collection ids); /** * 获得广告图管理分页 * * @param pageReqVO 分页查询 * @return 广告图管理分页 */ PageResult getAdsPage(ShopAdsPageReqVO pageReqVO); /** * 获得广告图管理列表, 用于 Excel 导出 * * @param exportReqVO 查询条件 * @return 广告图管理列表 */ List getAdsList(ShopAdsExportReqVO exportReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/java/co/yixiang/yshop/module/shop/service/shopads/ShopAdsServiceImpl.java ================================================ package co.yixiang.yshop.module.shop.service.shopads; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.constant.ShopConstants; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; import co.yixiang.yshop.module.store.dal.mysql.storeshop.StoreShopMapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import java.util.*; import java.util.stream.Collectors; import co.yixiang.yshop.module.shop.controller.admin.shopads.vo.*; import co.yixiang.yshop.module.shop.dal.dataobject.shopads.ShopAdsDO; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.shop.convert.shopads.ShopAdsConvert; import co.yixiang.yshop.module.shop.dal.mysql.shopads.ShopAdsMapper; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.shop.enums.ErrorCodeConstants.*; /** * 广告图管理 Service 实现类 * * @author yshop */ @Service @Validated public class ShopAdsServiceImpl implements ShopAdsService { @Resource private ShopAdsMapper adsMapper; @Resource private StoreShopMapper storeShopMapper; @Override public Long createAds(ShopAdsCreateReqVO createReqVO) { // 插入 ShopAdsDO ads = ShopAdsConvert.INSTANCE.convert(createReqVO); StoreShopDO storeShopDO = storeShopMapper.selectById(createReqVO.getShopId()); if(storeShopDO == null){ ads.setShopName("全部"); }else{ ads.setShopName(storeShopDO.getName()); } adsMapper.insert(ads); // 返回 return ads.getId(); } @Override public void updateAds(ShopAdsUpdateReqVO updateReqVO) { // 校验存在 validateAdsExists(updateReqVO.getId()); ShopAdsDO updateObj = ShopAdsConvert.INSTANCE.convert(updateReqVO); StoreShopDO storeShopDO= storeShopMapper.selectById(updateReqVO.getShopId()); if(storeShopDO == null){ updateObj.setShopName("全部"); }else{ updateObj.setShopName(storeShopDO.getName()); } adsMapper.updateById(updateObj); } @Override public void deleteAds(Long id) { // 校验存在 validateAdsExists(id); // 删除 adsMapper.deleteById(id); } private void validateAdsExists(Long id) { if (adsMapper.selectById(id) == null) { throw exception(ADS_NOT_EXISTS); } } @Override public ShopAdsDO getAds(Long id) { return adsMapper.selectById(id); } @Override public List getAdsList(Collection ids) { return adsMapper.selectBatchIds(ids); } @Override public PageResult getAdsPage(ShopAdsPageReqVO pageReqVO) { return adsMapper.selectPage(pageReqVO); } @Override public List getAdsList(ShopAdsExportReqVO exportReqVO) { return adsMapper.selectList(exportReqVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/resources/mapper/material/MaterialMapper.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/main/resources/mapper/materialgroup/MaterialGroupMapper.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-shop-biz/src/test/java/co/yixiang/yshop/module/shop/service/material/MaterialServiceImplTest.java ================================================ package co.yixiang.yshop.module.shop.service.material; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import jakarta.annotation.Resource; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.shop.controller.admin.material.vo.*; import co.yixiang.yshop.module.shop.dal.dataobject.material.MaterialDO; import co.yixiang.yshop.module.shop.dal.mysql.material.MaterialMapper; import co.yixiang.yshop.framework.common.pojo.PageResult; import jakarta.annotation.Resource; import org.springframework.context.annotation.Import; import java.util.*; import java.time.LocalDateTime; import static cn.hutool.core.util.RandomUtil.*; import static co.yixiang.yshop.module.shop.enums.ErrorCodeConstants.*; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.*; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.*; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.*; import static co.yixiang.yshop.framework.common.util.date.DateUtils.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; /** * {@link MaterialServiceImpl} 的单元测试类 * * @author yshop */ @Import(MaterialServiceImpl.class) public class MaterialServiceImplTest extends BaseDbUnitTest { @Resource private MaterialServiceImpl materialService; @Resource private MaterialMapper materialMapper; @Test public void testCreateMaterial_success() { // 准备参数 MaterialCreateReqVO reqVO = randomPojo(MaterialCreateReqVO.class); // 调用 Long materialId = materialService.createMaterial(reqVO); // 断言 assertNotNull(materialId); // 校验记录的属性是否正确 MaterialDO material = materialMapper.selectById(materialId); assertPojoEquals(reqVO, material); } @Test public void testUpdateMaterial_success() { // mock 数据 MaterialDO dbMaterial = randomPojo(MaterialDO.class); materialMapper.insert(dbMaterial);// @Sql: 先插入出一条存在的数据 // 准备参数 MaterialUpdateReqVO reqVO = randomPojo(MaterialUpdateReqVO.class, o -> { o.setId(dbMaterial.getId()); // 设置更新的 ID }); // 调用 materialService.updateMaterial(reqVO); // 校验是否更新正确 MaterialDO material = materialMapper.selectById(reqVO.getId()); // 获取最新的 assertPojoEquals(reqVO, material); } @Test public void testUpdateMaterial_notExists() { // 准备参数 MaterialUpdateReqVO reqVO = randomPojo(MaterialUpdateReqVO.class); // 调用, 并断言异常 assertServiceException(() -> materialService.updateMaterial(reqVO), MATERIAL_NOT_EXISTS); } @Test public void testDeleteMaterial_success() { // mock 数据 MaterialDO dbMaterial = randomPojo(MaterialDO.class); materialMapper.insert(dbMaterial);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbMaterial.getId(); // 调用 materialService.deleteMaterial(id); // 校验数据不存在了 assertNull(materialMapper.selectById(id)); } @Test public void testDeleteMaterial_notExists() { // 准备参数 Long id = randomLongId(); // 调用, 并断言异常 assertServiceException(() -> materialService.deleteMaterial(id), MATERIAL_NOT_EXISTS); } @Test @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 public void testGetMaterialPage() { // mock 数据 MaterialDO dbMaterial = randomPojo(MaterialDO.class, o -> { // 等会查询到 o.setCreateTime(null); o.setType(null); o.setGroupId(null); o.setName(null); o.setUrl(null); }); materialMapper.insert(dbMaterial); // 测试 createTime 不匹配 materialMapper.insert(cloneIgnoreId(dbMaterial, o -> o.setCreateTime(null))); // 测试 type 不匹配 materialMapper.insert(cloneIgnoreId(dbMaterial, o -> o.setType(null))); // 测试 groupId 不匹配 materialMapper.insert(cloneIgnoreId(dbMaterial, o -> o.setGroupId(null))); // 测试 name 不匹配 materialMapper.insert(cloneIgnoreId(dbMaterial, o -> o.setName(null))); // 测试 url 不匹配 materialMapper.insert(cloneIgnoreId(dbMaterial, o -> o.setUrl(null))); // 准备参数 MaterialPageReqVO reqVO = new MaterialPageReqVO(); reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); reqVO.setType(null); reqVO.setGroupId(null); reqVO.setName(null); reqVO.setUrl(null); // 调用 PageResult pageResult = materialService.getMaterialPage(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbMaterial, pageResult.getList().get(0)); } @Test @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 public void testGetMaterialList() { // mock 数据 MaterialDO dbMaterial = randomPojo(MaterialDO.class, o -> { // 等会查询到 o.setCreateTime(null); o.setType(null); o.setGroupId(null); o.setName(null); o.setUrl(null); }); materialMapper.insert(dbMaterial); // 测试 createTime 不匹配 materialMapper.insert(cloneIgnoreId(dbMaterial, o -> o.setCreateTime(null))); // 测试 type 不匹配 materialMapper.insert(cloneIgnoreId(dbMaterial, o -> o.setType(null))); // 测试 groupId 不匹配 materialMapper.insert(cloneIgnoreId(dbMaterial, o -> o.setGroupId(null))); // 测试 name 不匹配 materialMapper.insert(cloneIgnoreId(dbMaterial, o -> o.setName(null))); // 测试 url 不匹配 materialMapper.insert(cloneIgnoreId(dbMaterial, o -> o.setUrl(null))); // 准备参数 MaterialExportReqVO reqVO = new MaterialExportReqVO(); reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); reqVO.setType(null); reqVO.setGroupId(null); reqVO.setName(null); reqVO.setUrl(null); // 调用 List list = materialService.getMaterialList(reqVO); // 断言 assertEquals(1, list.size()); assertPojoEquals(dbMaterial, list.get(0)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-api/pom.xml ================================================ 4.0.0 co.yixiang.boot yshop-module-mall ${revision} yshop-module-store-api jar ${project.artifactId} store 模块 API,暴露给其它模块调用 co.yixiang.boot yshop-common org.springframework.boot spring-boot-starter-validation true ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-api/src/main/java/co/yixiang/yshop/module/store/enums/ErrorCodeConstants.java ================================================ package co.yixiang.yshop.module.store.enums; import co.yixiang.yshop.framework.common.exception.ErrorCode; public interface ErrorCodeConstants { ErrorCode SHOP_NOT_EXISTS = new ErrorCode(1008016000, "门店管理不存在"); ErrorCode WEB_PRINT_NOT_EXISTS = new ErrorCode(1008016001, "易联云打印机不存在"); ErrorCode STORE_REVENUE_NOT_EXISTS = new ErrorCode(1008016002, "店铺收支明细不存在"); ErrorCode WITHDRAWAL_NOT_EXISTS = new ErrorCode(1008016003, "提现管理不存在"); ErrorCode USER_BANK_NOT_EXISTS = new ErrorCode(1008016004, "提现账户不存在"); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-api/src/main/java/co/yixiang/yshop/module/store/enums/WithdrawalStatusEnum.java ================================================ package co.yixiang.yshop.module.store.enums; import lombok.AllArgsConstructor; import lombok.Getter; /** * @author hupeng * 类型枚举 */ @Getter @AllArgsConstructor public enum WithdrawalStatusEnum { STATUS_0(0,"未审核"), STATUS_1(1,"待到账"), STATUS_2(2,"审核拒绝"), STATUS_3(3,"已到账"); private Integer value; private String desc; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/pom.xml ================================================ co.yixiang.boot yshop-module-mall ${revision} 4.0.0 yshop-module-store-biz jar ${project.artifactId} 门店 模块,主要实现商品购物车相关功能 co.yixiang.boot yshop-module-store-api ${revision} co.yixiang.boot yshop-spring-boot-starter-web co.yixiang.boot yshop-spring-boot-starter-security co.yixiang.boot yshop-spring-boot-starter-mybatis co.yixiang.boot yshop-spring-boot-starter-test co.yixiang.boot yshop-spring-boot-starter-excel co.yixiang.boot yshop-spring-boot-starter-redis co.yixiang.boot yshop-spring-boot-starter-job co.yixiang.boot yshop-spring-boot-starter-biz-tenant ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/admin/storeshop/StoreShopController.java ================================================ package co.yixiang.yshop.module.store.controller.admin.storeshop; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import org.springframework.security.access.prepost.PreAuthorize; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.constraints.*; import jakarta.validation.*; import java.util.*; import java.io.IOException; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.pojo.CommonResult; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.store.controller.admin.storeshop.vo.*; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; import co.yixiang.yshop.module.store.convert.storeshop.StoreShopConvert; import co.yixiang.yshop.module.store.service.storeshop.StoreShopService; @Tag(name = "管理后台 - 门店管理") @RestController @RequestMapping("/store/shop") @Validated public class StoreShopController { @Resource private StoreShopService shopService; @PostMapping("/create") @Operation(summary = "创建门店管理") @PreAuthorize("@ss.hasPermission('store:shop:create')") public CommonResult createShop(@Valid @RequestBody StoreShopCreateReqVO createReqVO) { return success(shopService.createShop(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新门店管理") @PreAuthorize("@ss.hasPermission('store:shop:update')") public CommonResult updateShop(@Valid @RequestBody StoreShopUpdateReqVO updateReqVO) { shopService.updateShop(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除门店管理") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('store:shop:delete')") public CommonResult deleteShop(@RequestParam("id") Long id) { shopService.deleteShop(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得门店管理") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('store:shop:query')") public CommonResult getShop(@RequestParam("id") Long id) { StoreShopDO shop = shopService.getShop(id); return success(StoreShopConvert.INSTANCE.convert(shop)); } @GetMapping("/list") @Operation(summary = "获得门店管理列表") @PreAuthorize("@ss.hasPermission('store:shop:query')") public CommonResult> getShopList() { List list = shopService.getShopList(); return success(StoreShopConvert.INSTANCE.convertList(list)); } @GetMapping("/page") @Operation(summary = "获得门店管理分页") @PreAuthorize("@ss.hasPermission('store:shop:query')") public CommonResult> getShopPage(@Valid StoreShopPageReqVO pageVO) { PageResult pageResult = shopService.getShopPage(pageVO); return success(StoreShopConvert.INSTANCE.convertPage(pageResult)); } @GetMapping("/export-excel") @Operation(summary = "导出门店管理 Excel") @PreAuthorize("@ss.hasPermission('store:shop:export')") public void exportShopExcel(@Valid StoreShopExportReqVO exportReqVO, HttpServletResponse response) throws IOException { List list = shopService.getShopList(exportReqVO); // 导出 Excel List datas = StoreShopConvert.INSTANCE.convertList02(list); ExcelUtils.write(response, "门店管理.xls", "数据", StoreShopExcelVO.class, datas); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/admin/storeshop/vo/StoreShopBaseVO.java ================================================ package co.yixiang.yshop.module.store.controller.admin.storeshop.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.math.BigDecimal; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.LocalDateTime; import java.time.LocalDateTime; import java.time.LocalDateTime; import jakarta.validation.constraints.*; import org.springframework.format.annotation.DateTimeFormat; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; /** * 门店管理 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class StoreShopBaseVO { @Schema(description = "店铺名称", required = true, example = "李四") @NotNull(message = "店铺名称不能为空") private String name; @Schema(description = "店铺电话", required = true) @NotNull(message = "店铺电话不能为空") private String mobile; @Schema(description = "图片", required = true) @NotNull(message = "图片不能为空") private String image; @Schema(description = "多张图片", required = true) @NotNull(message = "多张图片不能为空") private List images; @Schema(description = "详细地址", required = true) @NotNull(message = "详细地址不能为空") private String address; @Schema(description = "地图定位地址", required = true) @NotNull(message = "地图定位地址不能为空") private String addressMap; @Schema(description = "经度", required = true) @NotNull(message = "经度不能为空") private String lng; @Schema(description = "纬度", required = true) @NotNull(message = "纬度不能为空") private String lat; @Schema(description = "外卖配送距离,单位为千米。0表示不送外卖", required = true) @NotNull(message = "外卖配送距离,单位为千米。0表示不送外卖不能为空") private Integer distance; @Schema(description = "起送价钱", required = true, example = "15157") private BigDecimal minPrice; @Schema(description = "配送价格", required = true, example = "11771") private BigDecimal deliveryPrice; @Schema(description = "公告", required = true) @NotNull(message = "公告不能为空") private String notice; @Schema(description = "是否营业:0=否,1=是", required = true, example = "2") @NotNull(message = "是否营业:0=否,1=是不能为空") private Integer status; @Schema(description = "管理员id", required = true, example = "22251") @NotNull(message = "管理员id不能为空") private List adminId; @Schema(description = "打印机id", required = true, example = "5596") //@NotNull(message = "打印机id不能为空") private String uniprintId; @Schema(description = "营业开始时间", required = true) @NotNull(message = "营业开始时间不能为空") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private Date startTime; @Schema(description = "营业结束时间", required = true) @NotNull(message = "营业结束时间不能为空") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private Date endTime; @Schema(description = "余额", required = true, example = "5596") private BigDecimal balance; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/admin/storeshop/vo/StoreShopCreateReqVO.java ================================================ package co.yixiang.yshop.module.store.controller.admin.storeshop.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @Schema(description = "管理后台 - 门店管理创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StoreShopCreateReqVO extends StoreShopBaseVO { } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/admin/storeshop/vo/StoreShopExcelVO.java ================================================ package co.yixiang.yshop.module.store.controller.admin.storeshop.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.math.BigDecimal; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.LocalDateTime; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.alibaba.excel.annotation.ExcelProperty; /** * 门店管理 Excel VO * * @author yshop */ @Data public class StoreShopExcelVO { @ExcelProperty("id") private Long id; @ExcelProperty("店铺名称") private String name; @ExcelProperty("店铺电话") private String mobile; @ExcelProperty("图片") private String image; @ExcelProperty("多张图片") private List images; @ExcelProperty("详细地址") private String address; @ExcelProperty("地图定位地址") private String addressMap; @ExcelProperty("经度") private String lng; @ExcelProperty("纬度") private String lat; @ExcelProperty("外卖配送距离,单位为千米。0表示不送外卖") private Integer distance; @ExcelProperty("起送价钱") private BigDecimal minPrice; @ExcelProperty("配送价格") private BigDecimal deliveryPrice; @ExcelProperty("公告") private String notice; @ExcelProperty("是否营业:0=否,1=是") private Integer status; @ExcelProperty("管理员id") private List adminId; @ExcelProperty("打印机id") private String uniprintId; @ExcelProperty("添加时间") private LocalDateTime createTime; @ExcelProperty("营业开始时间") private Date startTime; @ExcelProperty("营业结束时间") private Date endTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/admin/storeshop/vo/StoreShopExportReqVO.java ================================================ package co.yixiang.yshop.module.store.controller.admin.storeshop.vo; import lombok.*; import java.math.BigDecimal; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import java.time.LocalDateTime; import org.springframework.format.annotation.DateTimeFormat; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 门店管理 Excel 导出 Request VO,参数和 StoreShopPageReqVO 是一致的") @Data public class StoreShopExportReqVO { @Schema(description = "店铺名称", example = "李四") private String name; @Schema(description = "店铺电话") private String mobile; @Schema(description = "图片") private String image; @Schema(description = "多张图片") private String images; @Schema(description = "详细地址") private String address; @Schema(description = "地图定位地址") private String addressMap; @Schema(description = "经度") private String lng; @Schema(description = "纬度") private String lat; @Schema(description = "外卖配送距离,单位为千米。0表示不送外卖") private Integer distance; @Schema(description = "起送价钱", example = "15157") private BigDecimal minPrice; @Schema(description = "配送价格", example = "11771") private BigDecimal deliveryPrice; @Schema(description = "公告") private String notice; @Schema(description = "是否营业:0=否,1=是", example = "2") private Boolean status; @Schema(description = "管理员id", example = "22251") private String adminId; @Schema(description = "打印机id", example = "5596") private String uniprintId; @Schema(description = "添加时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; @Schema(description = "营业开始时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] startTime; @Schema(description = "营业结束时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] endTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/admin/storeshop/vo/StoreShopPageReqVO.java ================================================ package co.yixiang.yshop.module.store.controller.admin.storeshop.vo; import lombok.*; import java.math.BigDecimal; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 门店管理分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StoreShopPageReqVO extends PageParam { @Schema(description = "店铺名称", example = "李四") private String name; @Schema(description = "店铺电话") private String mobile; @Schema(description = "图片") private String image; @Schema(description = "多张图片") private String images; @Schema(description = "详细地址") private String address; @Schema(description = "地图定位地址") private String addressMap; @Schema(description = "经度") private String lng; @Schema(description = "纬度") private String lat; @Schema(description = "外卖配送距离,单位为千米。0表示不送外卖") private Integer distance; @Schema(description = "起送价钱", example = "15157") private BigDecimal minPrice; @Schema(description = "配送价格", example = "11771") private BigDecimal deliveryPrice; @Schema(description = "公告") private String notice; @Schema(description = "是否营业:0=否,1=是", example = "2") private Integer status; @Schema(description = "管理员id", example = "22251") private String adminId; @Schema(description = "打印机id", example = "5596") private String uniprintId; @Schema(description = "添加时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; @Schema(description = "营业开始时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] startTime; @Schema(description = "营业结束时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] endTime; private Long shopId; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/admin/storeshop/vo/StoreShopRespVO.java ================================================ package co.yixiang.yshop.module.store.controller.admin.storeshop.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; @Schema(description = "管理后台 - 门店管理 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StoreShopRespVO extends StoreShopBaseVO { @Schema(description = "id", required = true, example = "25450") private Long id; @Schema(description = "添加时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/admin/storeshop/vo/StoreShopUpdateReqVO.java ================================================ package co.yixiang.yshop.module.store.controller.admin.storeshop.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 门店管理更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StoreShopUpdateReqVO extends StoreShopBaseVO { @Schema(description = "id", required = true, example = "25450") @NotNull(message = "id不能为空") private Long id; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/app/storeshop/AppStoreController.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co * 注意: * 本软件为www.yixiang.co开发研制,未经购买不得使用 * 购买后可获得全部源代码(禁止转卖、分享、上传到码云、github等开源平台) * 一经发现盗用、分享等行为,将追究法律责任,后果自负 */ package co.yixiang.yshop.module.store.controller.app.storeshop; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.module.store.controller.app.storeshop.vo.AppStoreShopVO; import co.yixiang.yshop.module.store.convert.storeshop.StoreShopConvert; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; import co.yixiang.yshop.module.store.service.storeshop.AppStoreShopService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; /** *

* 门店控制器 *

* * @author hupeng * @since 2023-8-14 */ @Slf4j @RestController @Tag(name = "用户 APP - 门店") @RequiredArgsConstructor(onConstructor = @__(@Autowired)) @RequestMapping("/store") public class AppStoreController { private final AppStoreShopService appStoreShopService; @GetMapping("/nearby") @Operation(summary = "获取最近的店铺") public CommonResult getNearby(@RequestParam("lng") double lon, @RequestParam("lat") double lat, @RequestParam("kw") String name, @RequestParam("shop_id") Integer shopId) { List list = appStoreShopService.getStoreList(lon,lat,name,shopId); if(list != null ){ return success(list.get(0)); } return success(new AppStoreShopVO()); } @GetMapping("/list") @Operation(summary = "服务菜单列表") public CommonResult> getList(@RequestParam("lng") double lon, @RequestParam("lat") double lat, @RequestParam("kw") String name, @RequestParam("shop_id") Integer shopId) { return success(appStoreShopService.getStoreList(lon,lat,name,shopId)); } @GetMapping("/getShop") @Operation(summary = "获取最近的店铺") public CommonResult getShop(@RequestParam("shop_id") Integer shopId) { StoreShopDO storeShopDO = appStoreShopService.getById(shopId); return success(StoreShopConvert.INSTANCE.convert02(storeShopDO)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/app/storeshop/vo/AppStoreShopVO.java ================================================ package co.yixiang.yshop.module.store.controller.app.storeshop.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.springframework.format.annotation.DateTimeFormat; import java.math.BigDecimal; import java.util.Date; import java.util.List; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; /** * 门店管理VO */ @Data public class AppStoreShopVO { private Long id; @Schema(description = "店铺名称", required = true, example = "李四") private String name; @Schema(description = "店铺电话", required = true) private String mobile; @Schema(description = "图片", required = true) private String image; @Schema(description = "多张图片", required = true) private List images; @Schema(description = "详细地址", required = true) private String address; @Schema(description = "地图定位地址", required = true) private String addressMap; @Schema(description = "经度", required = true) private String lng; @Schema(description = "纬度", required = true) private String lat; @Schema(description = "外卖配送距离,单位为千米。0表示不送外卖", required = true) private Integer distance; @Schema(description = "计算出来的距离", required = true) private Long dis; @Schema(description = "起送价钱", required = true, example = "15157") private BigDecimal minPrice; @Schema(description = "配送价格", required = true, example = "11771") private BigDecimal deliveryPrice; @Schema(description = "公告", required = true) private String notice; @Schema(description = "营业开始时间", required = true) @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private Date startTime; @Schema(description = "营业结束时间", required = true) @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private Date endTime; @Schema(description = "桌面是否空闲", required = true) private Boolean isEmpty; @Schema(description = "桌面订单", required = true) private String deskOrderId; @Schema(description = "是否营业:0=否,1=是", required = true) private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/convert/storeshop/StoreShopConvert.java ================================================ package co.yixiang.yshop.module.store.convert.storeshop; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.store.controller.app.storeshop.vo.AppStoreShopVO; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import co.yixiang.yshop.module.store.controller.admin.storeshop.vo.*; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; /** * 门店管理 Convert * * @author yshop */ @Mapper public interface StoreShopConvert { StoreShopConvert INSTANCE = Mappers.getMapper(StoreShopConvert.class); StoreShopDO convert(StoreShopCreateReqVO bean); StoreShopDO convert(StoreShopUpdateReqVO bean); StoreShopRespVO convert(StoreShopDO bean); AppStoreShopVO convert02(StoreShopDO bean); List convertList(List list); PageResult convertPage(PageResult page); List convertList02(List list); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/dal/dataobject/storeshop/StoreShopDO.java ================================================ package co.yixiang.yshop.module.store.dal.dataobject.storeshop; import co.yixiang.yshop.framework.mybatis.core.type.StringListTypeHandler; import lombok.*; import java.util.*; import java.math.BigDecimal; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.LocalDateTime; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; /** * 门店管理 DO * * @author yshop */ @TableName(value = "yshop_store_shop",autoResultMap = true) @KeySequence("yshop_store_shop_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class StoreShopDO extends BaseDO { /** * id */ @TableId private Long id; /** * 店铺名称 */ private String name; /** * 店铺电话 */ private String mobile; /** * 图片 */ private String image; /** * 多张图片 */ @TableField(typeHandler = StringListTypeHandler.class) private List images; /** * 详细地址 */ private String address; /** * 地图定位地址 */ private String addressMap; /** * 经度 */ private String lng; /** * 纬度 */ private String lat; /** * 外卖配送距离,单位为千米。0表示不送外卖 */ private Integer distance; /** * 起送价钱 */ private BigDecimal minPrice; /** * 配送价格 */ private BigDecimal deliveryPrice; /** * 公告 */ private String notice; /** * 是否营业:0=否,1=是 */ private Integer status; /** * 管理员id */ @TableField(typeHandler = StringListTypeHandler.class) private List adminId; /** * 打印机id */ private String uniprintId; /** * 营业开始时间 */ private Date startTime; /** * 营业结束时间 */ private Date endTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/dal/mysql/storeshop/StoreShopMapper.java ================================================ package co.yixiang.yshop.module.store.dal.mysql.storeshop; import java.math.BigDecimal; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import co.yixiang.yshop.module.store.controller.app.storeshop.vo.AppStoreShopVO; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.apache.ibatis.annotations.Mapper; import co.yixiang.yshop.module.store.controller.admin.storeshop.vo.*; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; /** * 门店管理 Mapper * * @author yshop */ @Mapper public interface StoreShopMapper extends BaseMapperX { default PageResult selectPage(StoreShopPageReqVO reqVO) { Long shopId = SecurityFrameworkUtils.getLoginUser().getShopId(); System.out.println("shopId2:"+shopId); if(shopId == 0) { reqVO.setShopId(null); }else { reqVO.setShopId(shopId); } return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(StoreShopDO::getName, reqVO.getName()) .eqIfPresent(StoreShopDO::getMobile, reqVO.getMobile()) .eqIfPresent(StoreShopDO::getId, reqVO.getShopId()) .eqIfPresent(StoreShopDO::getDistance, reqVO.getDistance()) .eqIfPresent(StoreShopDO::getMinPrice, reqVO.getMinPrice()) .eqIfPresent(StoreShopDO::getDeliveryPrice, reqVO.getDeliveryPrice()) .eqIfPresent(StoreShopDO::getNotice, reqVO.getNotice()) .eqIfPresent(StoreShopDO::getStatus, reqVO.getStatus()) .betweenIfPresent(StoreShopDO::getCreateTime, reqVO.getCreateTime()) .betweenIfPresent(StoreShopDO::getStartTime, reqVO.getStartTime()) .betweenIfPresent(StoreShopDO::getEndTime, reqVO.getEndTime()) .orderByDesc(StoreShopDO::getId)); } default List selectList(StoreShopExportReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .likeIfPresent(StoreShopDO::getName, reqVO.getName()) .eqIfPresent(StoreShopDO::getMobile, reqVO.getMobile()) .eqIfPresent(StoreShopDO::getImage, reqVO.getImage()) .eqIfPresent(StoreShopDO::getImages, reqVO.getImages()) .eqIfPresent(StoreShopDO::getAddress, reqVO.getAddress()) .eqIfPresent(StoreShopDO::getAddressMap, reqVO.getAddressMap()) .eqIfPresent(StoreShopDO::getLng, reqVO.getLng()) .eqIfPresent(StoreShopDO::getLat, reqVO.getLat()) .eqIfPresent(StoreShopDO::getDistance, reqVO.getDistance()) .eqIfPresent(StoreShopDO::getMinPrice, reqVO.getMinPrice()) .eqIfPresent(StoreShopDO::getDeliveryPrice, reqVO.getDeliveryPrice()) .eqIfPresent(StoreShopDO::getNotice, reqVO.getNotice()) .eqIfPresent(StoreShopDO::getStatus, reqVO.getStatus()) .eqIfPresent(StoreShopDO::getAdminId, reqVO.getAdminId()) .eqIfPresent(StoreShopDO::getUniprintId, reqVO.getUniprintId()) .betweenIfPresent(StoreShopDO::getCreateTime, reqVO.getCreateTime()) .betweenIfPresent(StoreShopDO::getStartTime, reqVO.getStartTime()) .betweenIfPresent(StoreShopDO::getEndTime, reqVO.getEndTime()) .orderByDesc(StoreShopDO::getId)); } @Select("" ) List getStoreList(@Param("lon") double lon, @Param("lat") double lat, @Param("name") String name,@Param("shopId") Integer shopId); @Update("update yshop_store_shop set balance=balance+#{price}" + " where id=#{shopId}") int incMoney(@Param("shopId") Long shopId,@Param("price") BigDecimal price); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/dal/redis/PrintTokenRedisDAO.java ================================================ package co.yixiang.yshop.module.store.dal.redis; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Repository; import jakarta.annotation.Resource; import java.util.concurrent.TimeUnit; import static co.yixiang.yshop.module.store.dal.redis.RedisKeyConstants.YSHOP_WEB_PRINT_TOKEN_KEY; /** * * @author yshop */ @Repository public class PrintTokenRedisDAO { @Resource private StringRedisTemplate stringRedisTemplate; public String get() { String redisKey = YSHOP_WEB_PRINT_TOKEN_KEY.getKeyTemplate(); return stringRedisTemplate.opsForValue().get(redisKey); } public void set(String o) { String redisKey = YSHOP_WEB_PRINT_TOKEN_KEY.getKeyTemplate(); stringRedisTemplate.opsForValue().set(redisKey, o,2, TimeUnit.HOURS); } public void delete() { String redisKey = YSHOP_WEB_PRINT_TOKEN_KEY.getKeyTemplate(); stringRedisTemplate.delete(redisKey); } // // private static String formatKey) { // return String.format(YSHOP_ORDER_SALE_STATUS_KEY.getKeyTemplate(), key); // } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/dal/redis/RedisKeyConstants.java ================================================ package co.yixiang.yshop.module.store.dal.redis; import co.yixiang.yshop.framework.redis.core.RedisKeyDefine; import static co.yixiang.yshop.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING; /** * System Redis Key 枚举类 * * @author yshop */ public interface RedisKeyConstants { // // RedisKeyDefine YSHOP_ORDER_CACHE_KEY = new RedisKeyDefine("确认订单数据缓存", // "yshop_order_cache:%s", // 参数为访问uid+key // STRING, CacheDto.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); RedisKeyDefine YSHOP_WEB_PRINT_TOKEN_KEY = new RedisKeyDefine("打印机token", "yshop_web_print_token_cache:", // 参数为访问uid+key STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); // RedisKeyDefine YSHOP_ORDER_COUNT_CACHE_KEY = new RedisKeyDefine("统计订单数据缓存", // "yshop_order_count_cache:%s", // 参数为访问uid // STRING, CacheDto.class, RedisKeyDefine.TimeoutTypeEnum.FOREVER); // // RedisKeyDefine YSHOP_ADMIN_ORDER_COUNT_CACHE_KEY = new RedisKeyDefine("后台统计订单数据缓存", // "yshop_admin_order_count_cache:", // 参数为访问uid // STRING, CacheDto.class, RedisKeyDefine.TimeoutTypeEnum.FOREVER); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/service/storeshop/AppStoreShopService.java ================================================ package co.yixiang.yshop.module.store.service.storeshop; import co.yixiang.yshop.module.store.controller.app.storeshop.vo.AppStoreShopVO; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; import com.baomidou.mybatisplus.extension.service.IService; import java.util.List; /** * 门店管理 Service 接口 * * @author yshop */ public interface AppStoreShopService extends IService { /** * 获取门店 * @param lon 经度 * @param lat 纬度 * @param name 店铺名称 * @return */ List getStoreList(double lon,double lat,String name,Integer shopId); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/service/storeshop/AppStoreShopServiceImpl.java ================================================ package co.yixiang.yshop.module.store.service.storeshop; import co.yixiang.yshop.module.store.controller.app.storeshop.vo.AppStoreShopVO; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; import co.yixiang.yshop.module.store.dal.mysql.storeshop.StoreShopMapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.List; /** * 门店管理 Service 实现类 * * @author yshop */ @Service @Validated public class AppStoreShopServiceImpl extends ServiceImpl implements AppStoreShopService { @Resource private StoreShopMapper shopMapper; /** * 获取门店 * @param lon 经度 * @param lat 纬度 * @param name 店铺名称 * @return */ @Override public List getStoreList(double lon, double lat,String name,Integer shopId) { return shopMapper.getStoreList(lon,lat,name,shopId); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/service/storeshop/StoreShopService.java ================================================ package co.yixiang.yshop.module.store.service.storeshop; import java.util.*; import jakarta.validation.*; import co.yixiang.yshop.module.store.controller.admin.storeshop.vo.*; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; import co.yixiang.yshop.framework.common.pojo.PageResult; /** * 门店管理 Service 接口 * * @author yshop */ public interface StoreShopService { /** * 创建门店管理 * * @param createReqVO 创建信息 * @return 编号 */ Long createShop(@Valid StoreShopCreateReqVO createReqVO); /** * 更新门店管理 * * @param updateReqVO 更新信息 */ void updateShop(@Valid StoreShopUpdateReqVO updateReqVO); /** * 删除门店管理 * * @param id 编号 */ void deleteShop(Long id); /** * 获得门店管理 * * @param id 编号 * @return 门店管理 */ StoreShopDO getShop(Long id); /** * 获得门店管理列表 * * @return 门店管理列表 */ List getShopList(); /** * 获得门店管理分页 * * @param pageReqVO 分页查询 * @return 门店管理分页 */ PageResult getShopPage(StoreShopPageReqVO pageReqVO); /** * 获得门店管理列表, 用于 Excel 导出 * * @param exportReqVO 查询条件 * @return 门店管理列表 */ List getShopList(StoreShopExportReqVO exportReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/service/storeshop/StoreShopServiceImpl.java ================================================ package co.yixiang.yshop.module.store.service.storeshop; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.date.DateUtil; import co.yixiang.yshop.framework.common.exception.ErrorCode; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import java.util.*; import co.yixiang.yshop.module.store.controller.admin.storeshop.vo.*; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.store.convert.storeshop.StoreShopConvert; import co.yixiang.yshop.module.store.dal.mysql.storeshop.StoreShopMapper; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.store.enums.ErrorCodeConstants.*; /** * 门店管理 Service 实现类 * * @author yshop */ @Service @Validated public class StoreShopServiceImpl implements StoreShopService { @Resource private StoreShopMapper shopMapper; @Override public Long createShop(StoreShopCreateReqVO createReqVO) { //管理员只能绑定一个门店 createReqVO.getAdminId().forEach(val -> { Long count = shopMapper.selectCount(new LambdaQueryWrapper() .apply( "FIND_IN_SET ('" + val + "',admin_id)")); if(count > 0){ throw exception(new ErrorCode(1000,"管理员ID:"+val+"已经绑定过其他门店,不能再次绑定")); } }); // 插入 StoreShopDO shop = StoreShopConvert.INSTANCE.convert(createReqVO); Integer status = doShopStatus(shop.getStartTime(),shop.getEndTime()); shop.setStatus(status); shopMapper.insert(shop); // 返回 return shop.getId(); } @Override public void updateShop(StoreShopUpdateReqVO updateReqVO) { //管理员只能绑定一个门店 updateReqVO.getAdminId().forEach(val -> { Long count = shopMapper.selectCount(new LambdaQueryWrapper() .ne(StoreShopDO::getId,updateReqVO.getId()) .apply( "FIND_IN_SET ('" + val + "',admin_id)")); if(count > 0){ throw exception(new ErrorCode(1000,"管理员ID:"+val+"已经绑定过其他门店,不能再次绑定")); } }); // 校验存在 validateShopExists(updateReqVO.getId()); // 更新 StoreShopDO updateObj = StoreShopConvert.INSTANCE.convert(updateReqVO); Integer status = doShopStatus(updateObj.getStartTime(),updateObj.getEndTime()); updateObj.setStatus(status); shopMapper.updateById(updateObj); } /** * 处理营业时间 * @param startTime * @param endTime * @return */ private Integer doShopStatus(Date startTime,Date endTime){ Date now = new Date(); Integer sH = DateUtil.hour(startTime,true); Integer sM = DateUtil.minute(startTime); Integer eH = DateUtil.hour(endTime,true); Integer eM = DateUtil.minute(endTime); Integer nH = DateUtil.hour(now,true); Integer nM = DateUtil.minute(now); if(nH < sH){ return 0; }else if(eH < nH){ return 0; }else if(nH == sH && sM > nM){ return 0; }else if(eH == nH && eM < nM){ return 0; } else{ return 1; } } @Override public void deleteShop(Long id) { // 校验存在 validateShopExists(id); // 删除 shopMapper.deleteById(id); } private void validateShopExists(Long id) { if (shopMapper.selectById(id) == null) { throw exception(SHOP_NOT_EXISTS); } } @Override public StoreShopDO getShop(Long id) { return shopMapper.selectById(id); } @Override public List getShopList() { Long shopId = SecurityFrameworkUtils.getLoginUser().getShopId(); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); if(shopId == 0) { wrapper.ge(StoreShopDO::getId,shopId); }else { wrapper.eq(StoreShopDO::getId,shopId); } return shopMapper.selectList(wrapper); } @Override public PageResult getShopPage(StoreShopPageReqVO pageReqVO) { return shopMapper.selectPage(pageReqVO); } @Override public List getShopList(StoreShopExportReqVO exportReqVO) { return shopMapper.selectList(exportReqVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/resources/mapper/storeshop/StoreShopMapper.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/pom.xml ================================================ yshop co.yixiang.boot ${revision} 4.0.0 yshop-module-marketing pom ${project.artifactId} 营销模块 优惠券 会员卡 分销 拼团 yshop-module-coupon-api yshop-module-coupon-biz ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-api/pom.xml ================================================ 4.0.0 co.yixiang.boot yshop-module-marketing ${revision} yshop-module-coupon-api jar ${project.artifactId} coupon模块 API,暴露给其它模块调用 co.yixiang.boot yshop-common org.springframework.boot spring-boot-starter-validation true ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-api/src/main/java/co/yixiang/yshop/module/coupon/enums/CouponStatusEnum.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.coupon.enums; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.stream.Stream; /** * @author hupeng * 优惠券相关枚举 */ @Getter @AllArgsConstructor public enum CouponStatusEnum { STATUS_4(4,"所有"), STATUS_0(0,"未使用"), STATUS_1(1,"已使用"), STATUS_2(2,"已失效"); private Integer value; private String desc; public static CouponStatusEnum toType(int value) { return Stream.of(CouponStatusEnum.values()) .filter(p -> p.value == value) .findAny() .orElse(null); } } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-api/src/main/java/co/yixiang/yshop/module/coupon/enums/ErrorCodeConstants.java ================================================ package co.yixiang.yshop.module.coupon.enums; import co.yixiang.yshop.framework.common.exception.ErrorCode; public interface ErrorCodeConstants { ErrorCode COUPON_NOT_EXISTS = new ErrorCode(1008018000, "优惠券不存在"); ErrorCode COUPON_USER_NOT_EXISTS = new ErrorCode(1008018001, "用户领的优惠券不存在"); ErrorCode COUPON_RECEIVED = new ErrorCode(1008018001, "优惠券已经领取过"); ErrorCode COUPON_RECEIVE_ZERO = new ErrorCode(1008018001, "优惠券已经被领完"); } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/pom.xml ================================================ co.yixiang.boot yshop-module-marketing ${revision} 4.0.0 yshop-module-coupon-biz jar ${project.artifactId} 优惠券 模块,主要实现商品购物车相关功能 co.yixiang.boot yshop-module-coupon-api ${revision} co.yixiang.boot yshop-module-store-biz ${revision} co.yixiang.boot yshop-spring-boot-starter-job co.yixiang.boot yshop-spring-boot-starter-web co.yixiang.boot yshop-spring-boot-starter-security co.yixiang.boot yshop-spring-boot-starter-mybatis co.yixiang.boot yshop-spring-boot-starter-test co.yixiang.boot yshop-spring-boot-starter-excel ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/controller/admin/coupon/CouponController.java ================================================ package co.yixiang.yshop.module.coupon.controller.admin.coupon; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import org.springframework.security.access.prepost.PreAuthorize; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.constraints.*; import jakarta.validation.*; import java.util.*; import java.io.IOException; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.pojo.CommonResult; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.coupon.controller.admin.coupon.vo.*; import co.yixiang.yshop.module.coupon.dal.dataobject.coupon.CouponDO; import co.yixiang.yshop.module.coupon.convert.coupon.CouponConvert; import co.yixiang.yshop.module.coupon.service.coupon.CouponService; @Tag(name = "管理后台 - 优惠券") @RestController @RequestMapping("/coupon/") @Validated public class CouponController { @Resource private CouponService Service; @PostMapping("/create") @Operation(summary = "创建优惠券") @PreAuthorize("@ss.hasPermission('coupon::create')") public CommonResult create(@Valid @RequestBody CouponCreateReqVO createReqVO) { return success(Service.create(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新优惠券") @PreAuthorize("@ss.hasPermission('coupon::update')") public CommonResult update(@Valid @RequestBody CouponUpdateReqVO updateReqVO) { Service.update(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除优惠券") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('coupon::delete')") public CommonResult delete(@RequestParam("id") Long id) { Service.delete(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得优惠券") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('coupon::query')") public CommonResult get(@RequestParam("id") Long id) { CouponDO couponDO = Service.get(id); return success(CouponConvert.INSTANCE.convert(couponDO)); } @GetMapping("/list") @Operation(summary = "获得优惠券列表") @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") @PreAuthorize("@ss.hasPermission('coupon::query')") public CommonResult> getList() { List list = Service.getList(); return success(CouponConvert.INSTANCE.convertList(list)); } @GetMapping("/page") @Operation(summary = "获得优惠券分页") @PreAuthorize("@ss.hasPermission('coupon::query')") public CommonResult> getPage(@Valid CouponPageReqVO pageVO) { PageResult pageResult = Service.getPage(pageVO); return success(CouponConvert.INSTANCE.convertPage(pageResult)); } @GetMapping("/export-excel") @Operation(summary = "导出优惠券 Excel") @PreAuthorize("@ss.hasPermission('coupon::export')") public void exportExcel(@Valid CouponExportReqVO exportReqVO, HttpServletResponse response) throws IOException { List list = Service.getList(exportReqVO); // 导出 Excel List datas = CouponConvert.INSTANCE.convertList02(list); ExcelUtils.write(response, "优惠券.xls", "数据", CouponExcelVO.class, datas); } } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/controller/admin/coupon/vo/CouponBaseVO.java ================================================ package co.yixiang.yshop.module.coupon.controller.admin.coupon.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.math.BigDecimal; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.LocalDateTime; import java.time.LocalDateTime; import java.time.LocalDateTime; import jakarta.validation.constraints.*; import org.springframework.format.annotation.DateTimeFormat; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; /** * 优惠券 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class CouponBaseVO { @Schema(description = "店铺id,0表示通用", required = true, example = "8915") private String shopId; private String shopName; @Schema(description = "优惠券名称", required = true) @NotNull(message = "优惠券名称不能为空") private String title; @Schema(description = "是否上架", required = true) @NotNull(message = "是否上架不能为空") private Integer isSwitch; @Schema(description = "消费多少可用", required = true) @NotNull(message = "消费多少可用不能为空") private BigDecimal least; @Schema(description = "优惠券金额", required = true) @NotNull(message = "优惠券金额不能为空") private BigDecimal value; @Schema(description = "开始时间", required = true) @NotNull(message = "开始时间不能为空") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime startTime; @Schema(description = "结束时间", required = true) @NotNull(message = "结束时间不能为空") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime endTime; @Schema(description = "权重", required = true) private Integer weigh; @Schema(description = "可用类型:0=通用,1=自取,2=外卖", required = true, example = "2") @NotNull(message = "可用类型不能为空") private Integer type; @Schema(description = "兑换码") private String exchangeCode; @Schema(description = "已领取", required = true) private Integer receive; @Schema(description = "发行数量", required = true) @NotNull(message = "发行数量不能为空") private Integer distribute; @Schema(description = "所需积分", required = true) private Integer score; @Schema(description = "使用说明", required = true) private String instructions; @Schema(description = "图片", required = true) private String image; @Schema(description = "限领数量") @NotNull(message = "限领数量不能为空") private Integer limit; } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/controller/admin/coupon/vo/CouponCreateReqVO.java ================================================ package co.yixiang.yshop.module.coupon.controller.admin.coupon.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 优惠券创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class CouponCreateReqVO extends CouponBaseVO { } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/controller/admin/coupon/vo/CouponExcelVO.java ================================================ package co.yixiang.yshop.module.coupon.controller.admin.coupon.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.math.BigDecimal; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.LocalDateTime; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.alibaba.excel.annotation.ExcelProperty; /** * 优惠券 Excel VO * * @author yshop */ @Data public class CouponExcelVO { @ExcelProperty("id") private Long id; @ExcelProperty("店铺id,0表示通用") private String shopId; @ExcelProperty("店铺名称逗号隔开") private String shopName; @ExcelProperty("优惠券名称") private String title; @ExcelProperty("是否上架") private Integer isSwitch; @ExcelProperty("消费多少可用") private BigDecimal least; @ExcelProperty("优惠券金额") private BigDecimal value; @ExcelProperty("开始时间") private LocalDateTime startTime; @ExcelProperty("结束时间") private LocalDateTime endTime; @ExcelProperty("创建时间") private LocalDateTime createTime; @ExcelProperty("权重") private Integer weigh; @ExcelProperty("可用类型:0=通用,1=自取,2=外卖") private Integer type; @ExcelProperty("兑换码") private String exchangeCode; @ExcelProperty("已领取") private Integer receive; @ExcelProperty("发行数量") private Integer distribute; @ExcelProperty("所需积分") private Integer score; @ExcelProperty("使用说明") private String instructions; @ExcelProperty("图片") private String image; @ExcelProperty("限领数量") private Integer limit; } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/controller/admin/coupon/vo/CouponExportReqVO.java ================================================ package co.yixiang.yshop.module.coupon.controller.admin.coupon.vo; import lombok.*; import java.math.BigDecimal; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import java.time.LocalDateTime; import org.springframework.format.annotation.DateTimeFormat; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 优惠券 Excel 导出 Request VO,参数和 CouponPageReqVO 是一致的") @Data public class CouponExportReqVO { @Schema(description = "店铺id,0表示通用", example = "8915") private String shopId; @Schema(description = "店铺名称逗号隔开", example = "王五") private String shopName; @Schema(description = "优惠券名称") private String title; @Schema(description = "是否上架") private Integer isSwitch; @Schema(description = "消费多少可用") private BigDecimal least; @Schema(description = "优惠券金额") private BigDecimal value; @Schema(description = "开始时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] startTime; @Schema(description = "结束时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] endTime; @Schema(description = "创建时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; @Schema(description = "权重") private Integer weigh; @Schema(description = "可用类型:0=通用,1=自取,2=外卖", example = "2") private Integer type; @Schema(description = "兑换码") private String exchangeCode; @Schema(description = "已领取") private Integer receive; @Schema(description = "发行数量") private Integer distribute; @Schema(description = "所需积分") private Integer score; @Schema(description = "使用说明") private String instructions; @Schema(description = "图片") private String image; @Schema(description = "限领数量") private Integer limit; } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/controller/admin/coupon/vo/CouponPageReqVO.java ================================================ package co.yixiang.yshop.module.coupon.controller.admin.coupon.vo; import lombok.*; import java.math.BigDecimal; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 优惠券分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class CouponPageReqVO extends PageParam { @Schema(description = "店铺id,0表示通用", example = "8915") private String shopId; @Schema(description = "店铺名称逗号隔开", example = "王五") private String shopName; @Schema(description = "优惠券名称") private String title; @Schema(description = "是否上架") private Integer isSwitch; @Schema(description = "消费多少可用") private BigDecimal least; @Schema(description = "优惠券金额") private BigDecimal value; @Schema(description = "开始时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] startTime; @Schema(description = "结束时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] endTime; @Schema(description = "创建时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; @Schema(description = "权重") private Integer weigh; @Schema(description = "可用类型:0=通用,1=自取,2=外卖", example = "2") private Integer type; @Schema(description = "兑换码") private String exchangeCode; @Schema(description = "已领取") private Integer receive; @Schema(description = "发行数量") private Integer distribute; @Schema(description = "所需积分") private Integer score; @Schema(description = "使用说明") private String instructions; @Schema(description = "图片") private String image; @Schema(description = "限领数量") private Integer limit; } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/controller/admin/coupon/vo/CouponRespVO.java ================================================ package co.yixiang.yshop.module.coupon.controller.admin.coupon.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; @Schema(description = "管理后台 - 优惠券 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class CouponRespVO extends CouponBaseVO { @Schema(description = "id", required = true, example = "1582") private Long id; @Schema(description = "创建时间", required = true) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/controller/admin/coupon/vo/CouponUpdateReqVO.java ================================================ package co.yixiang.yshop.module.coupon.controller.admin.coupon.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 优惠券更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class CouponUpdateReqVO extends CouponBaseVO { @Schema(description = "id", required = true, example = "1582") @NotNull(message = "id不能为空") private Long id; } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/controller/admin/couponuser/CouponUserController.java ================================================ package co.yixiang.yshop.module.coupon.controller.admin.couponuser; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import org.springframework.security.access.prepost.PreAuthorize; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.constraints.*; import jakarta.validation.*; import java.util.*; import java.io.IOException; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.pojo.CommonResult; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.coupon.controller.admin.couponuser.vo.*; import co.yixiang.yshop.module.coupon.dal.dataobject.couponuser.CouponUserDO; import co.yixiang.yshop.module.coupon.convert.couponuser.CouponUserConvert; import co.yixiang.yshop.module.coupon.service.couponuser.CouponUserService; @Tag(name = "管理后台 - 用户领的优惠券") @RestController @RequestMapping("/coupon/user") @Validated public class CouponUserController { @Resource private CouponUserService userService; @PostMapping("/create") @Operation(summary = "创建用户领的优惠券") @PreAuthorize("@ss.hasPermission('coupon:user:create')") public CommonResult createUser(@Valid @RequestBody CouponUserCreateReqVO createReqVO) { return success(userService.createUser(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新用户领的优惠券") @PreAuthorize("@ss.hasPermission('coupon:user:update')") public CommonResult updateUser(@Valid @RequestBody CouponUserUpdateReqVO updateReqVO) { userService.updateUser(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除用户领的优惠券") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('coupon:user:delete')") public CommonResult deleteUser(@RequestParam("id") Integer id) { userService.deleteUser(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得用户领的优惠券") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('coupon:user:query')") public CommonResult getUser(@RequestParam("id") Integer id) { CouponUserDO user = userService.getUser(id); return success(CouponUserConvert.INSTANCE.convert(user)); } @GetMapping("/list") @Operation(summary = "获得用户领的优惠券列表") @Parameter(name = "id", description = "编号列表", required = true, example = "1024") // @PreAuthorize("@ss.hasPermission('coupon:user:query')") public CommonResult> getUserList(@RequestParam("couponId") Integer couponId) { List list = userService.getUserList(couponId); return success(CouponUserConvert.INSTANCE.convertList(list)); } @GetMapping("/page") @Operation(summary = "获得用户领的优惠券分页") //@PreAuthorize("@ss.hasPermission('coupon:user:query')") public CommonResult> getUserPage(@Valid CouponUserPageReqVO pageVO) { PageResult pageResult = userService.getUserPage(pageVO); return success(CouponUserConvert.INSTANCE.convertPage(pageResult)); } @GetMapping("/export-excel") @Operation(summary = "导出用户领的优惠券 Excel") @PreAuthorize("@ss.hasPermission('coupon:user:export')") public void exportUserExcel(@Valid CouponUserExportReqVO exportReqVO, HttpServletResponse response) throws IOException { List list = userService.getUserList(exportReqVO); // 导出 Excel List datas = CouponUserConvert.INSTANCE.convertList02(list); ExcelUtils.write(response, "用户领的优惠券.xls", "数据", CouponUserExcelVO.class, datas); } } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/controller/admin/couponuser/vo/CouponUserBaseVO.java ================================================ package co.yixiang.yshop.module.coupon.controller.admin.couponuser.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; import java.util.*; import java.math.BigDecimal; import java.math.BigDecimal; import jakarta.validation.constraints.*; /** * 用户领的优惠券 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class CouponUserBaseVO { @Schema(description = "店铺id,0表示通用", required = true, example = "32582") @NotNull(message = "店铺id,0表示通用不能为空") private String shopId; @Schema(description = "店铺名称逗号隔开", example = "yshop") private String shopName; @Schema(description = "优惠券名称", required = true) @NotNull(message = "优惠券名称不能为空") private String title; @Schema(description = "消费多少可用", required = true) @NotNull(message = "消费多少可用不能为空") private BigDecimal least; @Schema(description = "优惠券金额", required = true) @NotNull(message = "优惠券金额不能为空") private BigDecimal value; @Schema(description = "开始时间", required = true) @NotNull(message = "开始时间不能为空") private LocalDateTime startTime; @Schema(description = "结束时间", required = true) @NotNull(message = "结束时间不能为空") private LocalDateTime endTime; @Schema(description = "创建时间", required = true) @NotNull(message = "创建时间不能为空") private LocalDateTime createTime; @Schema(description = "更新时间", required = true) @NotNull(message = "更新时间不能为空") private LocalDateTime updateTime; @Schema(description = "可用类型:0=通用,1=自取,2=外卖", required = true, example = "2") @NotNull(message = "可用类型:0=通用,1=自取,2=外卖不能为空") private Integer type; @Schema(description = "消耗积分", required = true) @NotNull(message = "消耗积分不能为空") private Integer score; @Schema(description = "使用说明", required = true) @NotNull(message = "使用说明不能为空") private String instructions; @Schema(description = "图片", required = true) @NotNull(message = "图片不能为空") private String image; @Schema(description = "用户id", required = true, example = "20961") @NotNull(message = "用户id不能为空") private Integer userId; @Schema(description = "已使用:0=否,1=是", required = true, example = "1") @NotNull(message = "已使用:0=否,1=是不能为空") private Integer status; @Schema(description = "优惠券id", required = true, example = "23870") @NotNull(message = "优惠券id不能为空") private Integer couponId; @Schema(description = "兑换码") private String exchangeCode; } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/controller/admin/couponuser/vo/CouponUserCreateReqVO.java ================================================ package co.yixiang.yshop.module.coupon.controller.admin.couponuser.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 用户领的优惠券创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class CouponUserCreateReqVO extends CouponUserBaseVO { } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/controller/admin/couponuser/vo/CouponUserExcelVO.java ================================================ package co.yixiang.yshop.module.coupon.controller.admin.couponuser.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.math.BigDecimal; import java.math.BigDecimal; import com.alibaba.excel.annotation.ExcelProperty; /** * 用户领的优惠券 Excel VO * * @author yshop */ @Data public class CouponUserExcelVO { @ExcelProperty("id") private Integer id; @ExcelProperty("店铺id,0表示通用") private String shopId; @ExcelProperty("店铺名称逗号隔开") private String shopName; @ExcelProperty("优惠券名称") private String title; @ExcelProperty("消费多少可用") private BigDecimal least; @ExcelProperty("优惠券金额") private BigDecimal value; @ExcelProperty("开始时间") private Integer starttime; @ExcelProperty("结束时间") private Integer endtime; @ExcelProperty("创建时间") private Integer createtime; @ExcelProperty("更新时间") private Integer updatetime; @ExcelProperty("可用类型:0=通用,1=自取,2=外卖") private Integer type; @ExcelProperty("消耗积分") private Integer score; @ExcelProperty("使用说明") private String instructions; @ExcelProperty("图片") private String image; @ExcelProperty("用户id") private Integer userId; @ExcelProperty("已使用:0=否,1=是") private Integer status; @ExcelProperty("优惠券id") private Integer couponId; @ExcelProperty("兑换码") private String exchangeCode; } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/controller/admin/couponuser/vo/CouponUserExportReqVO.java ================================================ package co.yixiang.yshop.module.coupon.controller.admin.couponuser.vo; import lombok.*; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import org.springframework.format.annotation.DateTimeFormat; import jakarta.validation.constraints.NotNull; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 用户领的优惠券 Excel 导出 Request VO,参数和 CouponUserPageReqVO 是一致的") @Data public class CouponUserExportReqVO { @Schema(description = "店铺id,0表示通用", example = "32582") private String shopId; @Schema(description = "店铺名称逗号隔开", example = "yshop") private String shopName; @Schema(description = "优惠券名称") private String title; @Schema(description = "消费多少可用") private BigDecimal least; @Schema(description = "优惠券金额") private BigDecimal value; @Schema(description = "开始时间", required = true) @NotNull(message = "开始时间不能为空") private LocalDateTime startTime; @Schema(description = "结束时间", required = true) @NotNull(message = "结束时间不能为空") private LocalDateTime endTime; @Schema(description = "创建时间", required = true) @NotNull(message = "创建时间不能为空") private LocalDateTime createTime; @Schema(description = "更新时间", required = true) @NotNull(message = "更新时间不能为空") private LocalDateTime updateTime; @Schema(description = "可用类型:0=通用,1=自取,2=外卖", example = "2") private Boolean type; @Schema(description = "消耗积分") private Integer score; @Schema(description = "使用说明") private String instructions; @Schema(description = "图片") private String image; @Schema(description = "用户id", example = "20961") private Integer userId; @Schema(description = "已使用:0=否,1=是", example = "1") private Boolean status; @Schema(description = "优惠券id", example = "23870") private Integer couponId; @Schema(description = "兑换码") private String exchangeCode; } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/controller/admin/couponuser/vo/CouponUserPageReqVO.java ================================================ package co.yixiang.yshop.module.coupon.controller.admin.couponuser.vo; import lombok.*; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import org.springframework.format.annotation.DateTimeFormat; import jakarta.validation.constraints.NotNull; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 用户领的优惠券分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class CouponUserPageReqVO extends PageParam { @Schema(description = "店铺id,0表示通用", example = "32582") private String shopId; @Schema(description = "店铺名称逗号隔开", example = "yshop") private String shopName; @Schema(description = "优惠券名称") private String title; @Schema(description = "消费多少可用") private BigDecimal least; @Schema(description = "优惠券金额") private BigDecimal value; @Schema(description = "开始时间", required = true) @NotNull(message = "开始时间不能为空") private LocalDateTime startTime; @Schema(description = "结束时间", required = true) @NotNull(message = "结束时间不能为空") private LocalDateTime endTime; @Schema(description = "创建时间", required = true) @NotNull(message = "创建时间不能为空") private LocalDateTime createTime; @Schema(description = "更新时间", required = true) @NotNull(message = "更新时间不能为空") private LocalDateTime updateTime; @Schema(description = "可用类型:0=通用,1=自取,2=外卖", example = "2") private Boolean type; @Schema(description = "消耗积分") private Integer score; @Schema(description = "使用说明") private String instructions; @Schema(description = "图片") private String image; @Schema(description = "用户id", example = "20961") private Integer userId; @Schema(description = "已使用:0=否,1=是", example = "1") private Boolean status; @Schema(description = "优惠券id", example = "23870") private Integer couponId; @Schema(description = "兑换码") private String exchangeCode; } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/controller/admin/couponuser/vo/CouponUserRespVO.java ================================================ package co.yixiang.yshop.module.coupon.controller.admin.couponuser.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; @Schema(description = "管理后台 - 用户领的优惠券 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class CouponUserRespVO extends CouponUserBaseVO { @Schema(description = "id", required = true, example = "5159") private Integer id; } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/controller/admin/couponuser/vo/CouponUserUpdateReqVO.java ================================================ package co.yixiang.yshop.module.coupon.controller.admin.couponuser.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 用户领的优惠券更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class CouponUserUpdateReqVO extends CouponUserBaseVO { @Schema(description = "id", required = true, example = "5159") @NotNull(message = "id不能为空") private Integer id; } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/controller/app/coupon/AppCouponController.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co * 注意: * 本软件为www.yixiang.co开发研制,未经购买不得使用 * 购买后可获得全部源代码(禁止转卖、分享、上传到码云、github等开源平台) * 一经发现盗用、分享等行为,将追究法律责任,后果自负 */ package co.yixiang.yshop.module.coupon.controller.app.coupon; import co.yixiang.yshop.framework.common.enums.ShopCommonEnum; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.security.core.annotations.PreAuthenticated; import co.yixiang.yshop.module.coupon.controller.app.coupon.vo.AppCouponVO; import co.yixiang.yshop.module.coupon.controller.app.coupon.vo.AppMyCouponVO; import co.yixiang.yshop.module.coupon.controller.app.coupon.vo.AppReceVO; import co.yixiang.yshop.module.coupon.dal.dataobject.couponuser.CouponUserDO; import co.yixiang.yshop.module.coupon.service.coupon.AppCouponService; import co.yixiang.yshop.module.coupon.service.couponuser.AppCouponUserService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; /** *

* 优惠券控制器 *

* * @author hupeng * @since 2023-8-20 */ @Slf4j @RestController @Tag(name = "用户 APP - 优惠券") @RequiredArgsConstructor(onConstructor = @__(@Autowired)) @RequestMapping("/coupon") public class AppCouponController { private final AppCouponUserService appCouponUserService; private final AppCouponService appCouponService; @PreAuthenticated @GetMapping("/count") @Operation(summary = "获取可用优惠券数量") public CommonResult getCount(@RequestParam("shop_id") Integer shopId, @RequestParam("type") Integer type) { Long uid = getLoginUserId(); LocalDateTime nowTime = LocalDateTime.now(); Long count = appCouponUserService.count(new LambdaQueryWrapper() .eq(CouponUserDO::getUserId,uid) .eq(CouponUserDO::getShopId,shopId) .lt(CouponUserDO::getStartTime,nowTime) .gt(CouponUserDO::getEndTime,nowTime) .and(i->i.eq(CouponUserDO::getType,type).or().eq(CouponUserDO::getType,0)) .eq(CouponUserDO::getStatus, ShopCommonEnum.IS_STATUS_0)); return success(count); } /** * 获取我的优惠券 */ @PreAuthenticated @GetMapping("/my") @Parameters({ @Parameter(name = "shopId", description = "店铺ID", example = "1"), @Parameter(name = "type", description = "0-未使用 1-已使用 2-已过期", example = "1"), @Parameter(name = "page", description = "页码,默认为1", required = true, example = "1"), @Parameter(name = "pagesize", description = "页大小,默认为10", required = true, example = "10 ") }) @Operation(summary = "获取我的优惠券") public CommonResult> myList(@RequestParam(value = "shopId",required = false) Long shopId, @RequestParam(value = "type",defaultValue = "0") int type, @RequestParam(value = "page",defaultValue = "1") int page, @RequestParam(value = "pagesize",defaultValue = "10") int pagesize){ Long uid = getLoginUserId(); return success(appCouponUserService.getList(uid,shopId,type,page,pagesize)); } /** * 获取未被领取优惠券 */ @PreAuthenticated @GetMapping("/not") @Parameters({ @Parameter(name = "shopId", description = "店铺ID", example = "1"), @Parameter(name = "page", description = "页码,默认为1", required = true, example = "1"), @Parameter(name = "pagesize", description = "页大小,默认为10", required = true, example = "10 ") }) @Operation(summary = "获取未被领取优惠券") public CommonResult> myNotList(@RequestParam(value = "id",required = false) Long shopId, @RequestParam(value = "page",defaultValue = "1") int page, @RequestParam(value = "pagesize",defaultValue = "10") int pagesize){ Long uid = getLoginUserId(); return success(appCouponService.getNotList(uid,shopId,page,pagesize)); } /** * 领取优惠券 */ @PreAuthenticated @PostMapping("/receive") @Parameters({ @Parameter(name = "id", description = "优惠券ID", example = "1"), @Parameter(name = "code", description = "优惠券兑换码", example = "1") }) @Operation(summary = "获取未被领取优惠券") public CommonResult receive(@RequestBody AppReceVO appReceVO){ Long uid = getLoginUserId(); appCouponService.receive(uid,appReceVO.getId(),appReceVO.getCode()); return success(true); } } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/controller/app/coupon/vo/AppCouponVO.java ================================================ package co.yixiang.yshop.module.coupon.controller.app.coupon.vo; import lombok.*; import java.math.BigDecimal; import java.time.LocalDateTime; /** * 优惠券 vo * * @author yshop */ @Data @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class AppCouponVO { /** * id */ private Long id; /** * 店铺id,0表示通用 */ private String shopId; /** * 店铺名称逗号隔开 */ private String shopName; /** * 优惠券名称 */ private String title; /** * 是否上架 */ private Integer isSwitch; /** * 消费多少可用 */ private BigDecimal least; /** * 优惠券金额 */ private BigDecimal value; /** * 开始时间 */ private LocalDateTime startTime; /** * 结束时间 */ private LocalDateTime endTime; /** * 权重 */ private Integer weigh; /** * 可用类型:0=通用,1=自取,2=外卖 */ private Integer type; /** * 兑换码 */ private String exchangeCode; /** * 已领取 */ private Integer receive; /** * 发行数量 */ private Integer distribute; /** * 所需积分 */ private Integer score; /** * 使用说明 */ private String instructions; /** * 图片 */ private String image; /** * 限领数量 */ private Integer limit; private LocalDateTime createTime; /** * 是否已领取 */ private Integer isReceive; } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/controller/app/coupon/vo/AppMyCouponVO.java ================================================ package co.yixiang.yshop.module.coupon.controller.app.coupon.vo; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; import java.math.BigDecimal; import java.time.LocalDateTime; /** * 用户领的优惠券vo * * @author yshop */ @Data @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class AppMyCouponVO { /** * id */ private Integer id; /** * 店铺id,0表示通用 */ private String shopId; /** * 店铺名称逗号隔开 */ private String shopName; /** * 优惠券名称 */ private String title; /** * 消费多少可用 */ private BigDecimal least; /** * 优惠券金额 */ private BigDecimal value; /** * 开始时间 */ private LocalDateTime startTime; /** * 结束时间 */ private LocalDateTime endTime; /** * 可用类型:0=通用,1=自取,2=外卖 */ private Integer type; /** * 消耗积分 */ private Integer score; /** * 使用说明 */ private String instructions; /** * 图片 */ private String image; /** * 用户id */ private Integer userId; /** * 已使用:0=否,1=是 */ private Integer status; /** * 优惠券id */ private Integer couponId; /** * 兑换码 */ private String exchangeCode; private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/controller/app/coupon/vo/AppReceVO.java ================================================ package co.yixiang.yshop.module.coupon.controller.app.coupon.vo; import lombok.*; import java.math.BigDecimal; import java.time.LocalDateTime; /** * 领取优惠券 vo * * @author yshop */ @Data public class AppReceVO { /** * id */ private Long id; /** * 优惠券兑换码 */ private String code; } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/convert/coupon/CouponConvert.java ================================================ package co.yixiang.yshop.module.coupon.convert.coupon; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.coupon.controller.app.coupon.vo.AppCouponVO; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import co.yixiang.yshop.module.coupon.controller.admin.coupon.vo.*; import co.yixiang.yshop.module.coupon.dal.dataobject.coupon.CouponDO; /** * 优惠券 Convert * * @author yshop */ @Mapper public interface CouponConvert { CouponConvert INSTANCE = Mappers.getMapper(CouponConvert.class); CouponDO convert(CouponCreateReqVO bean); CouponDO convert(CouponUpdateReqVO bean); CouponRespVO convert(CouponDO bean); AppCouponVO convert01(CouponDO bean); List convertList(List list); List convertList03(List list); PageResult convertPage(PageResult page); List convertList02(List list); } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/convert/couponuser/CouponUserConvert.java ================================================ package co.yixiang.yshop.module.coupon.convert.couponuser; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.coupon.controller.app.coupon.vo.AppMyCouponVO; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import co.yixiang.yshop.module.coupon.controller.admin.couponuser.vo.*; import co.yixiang.yshop.module.coupon.dal.dataobject.couponuser.CouponUserDO; /** * 用户领的优惠券 Convert * * @author yshop */ @Mapper public interface CouponUserConvert { CouponUserConvert INSTANCE = Mappers.getMapper(CouponUserConvert.class); CouponUserDO convert(CouponUserCreateReqVO bean); CouponUserDO convert(CouponUserUpdateReqVO bean); CouponUserRespVO convert(CouponUserDO bean); List convertList03(List list); List convertList(List list); PageResult convertPage(PageResult page); List convertList02(List list); } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/dal/dataobject/coupon/CouponDO.java ================================================ package co.yixiang.yshop.module.coupon.dal.dataobject.coupon; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; import java.math.BigDecimal; import java.time.LocalDateTime; /** * 优惠券 DO * * @author yshop */ @TableName(value = "yshop_coupon") @KeySequence("yshop_coupon_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class CouponDO extends BaseDO { /** * id */ @TableId private Long id; /** * 店铺id,0表示通用 */ private String shopId; /** * 店铺名称逗号隔开 */ private String shopName; /** * 优惠券名称 */ private String title; /** * 是否上架 */ private Integer isSwitch; /** * 消费多少可用 */ private BigDecimal least; /** * 优惠券金额 */ private BigDecimal value; /** * 开始时间 */ private LocalDateTime startTime; /** * 结束时间 */ private LocalDateTime endTime; /** * 权重 */ private Integer weigh; /** * 可用类型:0=通用,1=自取,2=外卖 */ private Integer type; /** * 兑换码 */ private String exchangeCode; /** * 已领取 */ private Integer receive; /** * 发行数量 */ private Integer distribute; /** * 所需积分 */ private Integer score; /** * 使用说明 */ private String instructions; /** * 图片 */ private String image; /** * 限领数量 */ @TableField(value = "`limit`") private Integer limit; } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/dal/dataobject/couponuser/CouponUserDO.java ================================================ package co.yixiang.yshop.module.coupon.dal.dataobject.couponuser; import lombok.*; import java.time.LocalDateTime; import java.util.*; import java.math.BigDecimal; import java.math.BigDecimal; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; /** * 用户领的优惠券 DO * * @author yshop */ @TableName("yshop_coupon_user") @KeySequence("yshop_coupon_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class CouponUserDO extends BaseDO { /** * id */ @TableId private Integer id; /** * 店铺id,0表示通用 */ private String shopId; /** * 店铺名称逗号隔开 */ private String shopName; /** * 优惠券名称 */ private String title; /** * 消费多少可用 */ private BigDecimal least; /** * 优惠券金额 */ private BigDecimal value; /** * 开始时间 */ private LocalDateTime startTime; /** * 结束时间 */ private LocalDateTime endTime; /** * 可用类型:0=通用,1=自取,2=外卖 */ private Integer type; /** * 消耗积分 */ private Integer score; /** * 使用说明 */ private String instructions; /** * 图片 */ private String image; /** * 用户id */ private Integer userId; /** * 已使用:0=否,1=是 */ private Integer status; /** * 优惠券id */ private Integer couponId; /** * 兑换码 */ private String exchangeCode; } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/dal/mysql/coupon/CouponMapper.java ================================================ package co.yixiang.yshop.module.coupon.dal.mysql.coupon; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import co.yixiang.yshop.module.coupon.dal.dataobject.coupon.CouponDO; import org.apache.ibatis.annotations.Mapper; import co.yixiang.yshop.module.coupon.controller.admin.coupon.vo.*; /** * 优惠券 Mapper * * @author yshop */ @Mapper public interface CouponMapper extends BaseMapperX { default PageResult selectPage(CouponPageReqVO reqVO) { Long shopId = SecurityFrameworkUtils.getLoginUser().getShopId(); if(shopId == 0) { reqVO.setShopId(null); }else { reqVO.setShopId(shopId.toString()); } return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(CouponDO::getShopId, reqVO.getShopId()) .likeIfPresent(CouponDO::getShopName, reqVO.getShopName()) .eqIfPresent(CouponDO::getTitle, reqVO.getTitle()) .orderByDesc(CouponDO::getId)); } default List selectList(CouponExportReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .eqIfPresent(CouponDO::getShopId, reqVO.getShopId()) .likeIfPresent(CouponDO::getShopName, reqVO.getShopName()) .eqIfPresent(CouponDO::getTitle, reqVO.getTitle()) .eqIfPresent(CouponDO::getIsSwitch, reqVO.getIsSwitch()) .eqIfPresent(CouponDO::getLeast, reqVO.getLeast()) .eqIfPresent(CouponDO::getValue, reqVO.getValue()) .betweenIfPresent(CouponDO::getStartTime, reqVO.getStartTime()) .betweenIfPresent(CouponDO::getCreateTime, reqVO.getCreateTime()) .eqIfPresent(CouponDO::getWeigh, reqVO.getWeigh()) .eqIfPresent(CouponDO::getType, reqVO.getType()) .eqIfPresent(CouponDO::getExchangeCode, reqVO.getExchangeCode()) .eqIfPresent(CouponDO::getReceive, reqVO.getReceive()) .eqIfPresent(CouponDO::getDistribute, reqVO.getDistribute()) .eqIfPresent(CouponDO::getScore, reqVO.getScore()) .eqIfPresent(CouponDO::getInstructions, reqVO.getInstructions()) .eqIfPresent(CouponDO::getImage, reqVO.getImage()) .eqIfPresent(CouponDO::getLimit, reqVO.getLimit()) .orderByDesc(CouponDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/dal/mysql/couponuser/CouponUserMapper.java ================================================ package co.yixiang.yshop.module.coupon.dal.mysql.couponuser; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.coupon.dal.dataobject.couponuser.CouponUserDO; import org.apache.ibatis.annotations.Mapper; import co.yixiang.yshop.module.coupon.controller.admin.couponuser.vo.*; /** * 用户领的优惠券 Mapper * * @author yshop */ @Mapper public interface CouponUserMapper extends BaseMapperX { default PageResult selectPage(CouponUserPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(CouponUserDO::getShopId, reqVO.getShopId()) .likeIfPresent(CouponUserDO::getShopName, reqVO.getShopName()) .eqIfPresent(CouponUserDO::getTitle, reqVO.getTitle()) .eqIfPresent(CouponUserDO::getLeast, reqVO.getLeast()) .eqIfPresent(CouponUserDO::getValue, reqVO.getValue()) .eqIfPresent(CouponUserDO::getType, reqVO.getType()) .eqIfPresent(CouponUserDO::getScore, reqVO.getScore()) .eqIfPresent(CouponUserDO::getInstructions, reqVO.getInstructions()) .eqIfPresent(CouponUserDO::getImage, reqVO.getImage()) .eqIfPresent(CouponUserDO::getUserId, reqVO.getUserId()) .eqIfPresent(CouponUserDO::getStatus, reqVO.getStatus()) .eqIfPresent(CouponUserDO::getCouponId, reqVO.getCouponId()) .eqIfPresent(CouponUserDO::getExchangeCode, reqVO.getExchangeCode()) .orderByDesc(CouponUserDO::getId)); } default List selectList(CouponUserExportReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .eqIfPresent(CouponUserDO::getShopId, reqVO.getShopId()) .likeIfPresent(CouponUserDO::getShopName, reqVO.getShopName()) .eqIfPresent(CouponUserDO::getTitle, reqVO.getTitle()) .eqIfPresent(CouponUserDO::getLeast, reqVO.getLeast()) .eqIfPresent(CouponUserDO::getValue, reqVO.getValue()) .eqIfPresent(CouponUserDO::getType, reqVO.getType()) .eqIfPresent(CouponUserDO::getScore, reqVO.getScore()) .eqIfPresent(CouponUserDO::getInstructions, reqVO.getInstructions()) .eqIfPresent(CouponUserDO::getImage, reqVO.getImage()) .eqIfPresent(CouponUserDO::getUserId, reqVO.getUserId()) .eqIfPresent(CouponUserDO::getStatus, reqVO.getStatus()) .eqIfPresent(CouponUserDO::getCouponId, reqVO.getCouponId()) .eqIfPresent(CouponUserDO::getExchangeCode, reqVO.getExchangeCode()) .orderByDesc(CouponUserDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/service/coupon/AppCouponService.java ================================================ package co.yixiang.yshop.module.coupon.service.coupon; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.coupon.controller.admin.coupon.vo.CouponCreateReqVO; import co.yixiang.yshop.module.coupon.controller.admin.coupon.vo.CouponExportReqVO; import co.yixiang.yshop.module.coupon.controller.admin.coupon.vo.CouponPageReqVO; import co.yixiang.yshop.module.coupon.controller.admin.coupon.vo.CouponUpdateReqVO; import co.yixiang.yshop.module.coupon.controller.app.coupon.vo.AppCouponVO; import co.yixiang.yshop.module.coupon.controller.app.coupon.vo.AppMyCouponVO; import co.yixiang.yshop.module.coupon.dal.dataobject.coupon.CouponDO; import com.baomidou.mybatisplus.extension.service.IService; import jakarta.validation.Valid; import java.util.Collection; import java.util.List; /** * 优惠券 Service 接口 * * @author yshop */ public interface AppCouponService extends IService { /** * 获取未被领取优惠券 * @param shopId 店铺id * @param page * @param pagesize * @return */ List getNotList(Long uid, Long shopId, int page, int pagesize); /** * 领取优惠券 * @param uid 用户ID * @param id 优惠券ID * @param code 兑换码 */ void receive(Long uid,Long id,String code); } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/service/coupon/AppCouponServiceImpl.java ================================================ package co.yixiang.yshop.module.coupon.service.coupon; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.util.ObjectUtil; import co.yixiang.yshop.framework.common.enums.ShopCommonEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.ObjectUtils; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.coupon.controller.admin.coupon.vo.CouponCreateReqVO; import co.yixiang.yshop.module.coupon.controller.admin.coupon.vo.CouponExportReqVO; import co.yixiang.yshop.module.coupon.controller.admin.coupon.vo.CouponPageReqVO; import co.yixiang.yshop.module.coupon.controller.admin.coupon.vo.CouponUpdateReqVO; import co.yixiang.yshop.module.coupon.controller.app.coupon.vo.AppCouponVO; import co.yixiang.yshop.module.coupon.controller.app.coupon.vo.AppMyCouponVO; import co.yixiang.yshop.module.coupon.convert.coupon.CouponConvert; import co.yixiang.yshop.module.coupon.convert.couponuser.CouponUserConvert; import co.yixiang.yshop.module.coupon.dal.dataobject.coupon.CouponDO; import co.yixiang.yshop.module.coupon.dal.dataobject.couponuser.CouponUserDO; import co.yixiang.yshop.module.coupon.dal.mysql.coupon.CouponMapper; import co.yixiang.yshop.module.coupon.dal.mysql.couponuser.CouponUserMapper; import co.yixiang.yshop.module.coupon.service.couponuser.AppCouponUserService; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; import co.yixiang.yshop.module.store.dal.mysql.storeshop.StoreShopMapper; import co.yixiang.yshop.module.system.api.logger.dto.OperateLogCreateReqDTO; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.coupon.enums.ErrorCodeConstants.*; /** * 优惠券 Service 实现类 * * @author yshop */ @Service @Validated public class AppCouponServiceImpl extends ServiceImpl implements AppCouponService { @Resource private CouponMapper couponMapper; @Resource private AppCouponUserService appCouponUserService; /** * 获取未被领取优惠券 * @param shopId 店铺id * @param page * @param pagesize * @return */ @Override public List getNotList(Long uid, Long shopId, int page, int pagesize) { LocalDateTime nowTime = LocalDateTime.now(); Page pageModel = new Page<>(page, pagesize); LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX<>(); wrapper.eqIfPresent(CouponDO::getShopId, shopId) .gt(CouponDO::getEndTime,nowTime) .orderByDesc(CouponDO::getWeigh); IPage pageList = this.baseMapper.selectPage(pageModel, wrapper); List list = new ArrayList<>(); for (CouponDO couponDO : pageList.getRecords()) { AppCouponVO appCouponVO = CouponConvert.INSTANCE.convert01(couponDO); long count = appCouponUserService.count(new LambdaQueryWrapper() .eq(CouponUserDO::getUserId,uid).eq(CouponUserDO::getCouponId,couponDO.getId())); if(count > 0){ appCouponVO.setIsReceive(ShopCommonEnum.DEFAULT_1.getValue()); }else { appCouponVO.setIsReceive(ShopCommonEnum.DEFAULT_0.getValue()); } list.add(appCouponVO); } return list; } /** * 领取优惠券 * @param uid 用户ID * @param id 优惠券ID * @param code 兑换码 */ @Override @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public void receive(Long uid, Long id, String code) { CouponDO couponDO = null; if(id != null){ couponDO = this.baseMapper.selectById(id); }else { couponDO = couponMapper.selectOne(CouponDO::getExchangeCode,code); } if(couponDO == null){ throw exception(COUPON_NOT_EXISTS); } if(couponDO.getReceive() >= couponDO.getDistribute()){ throw exception(COUPON_RECEIVE_ZERO); } Long couponCount = appCouponUserService.count(new LambdaQueryWrapper() .eq(CouponUserDO::getUserId,uid).eq(CouponUserDO::getCouponId,couponDO.getId())); if(couponCount > 0){ throw exception(COUPON_RECEIVED); } CouponUserDO couponUserDO = BeanUtil.copyProperties(couponDO,CouponUserDO.class,"id"); couponUserDO.setUserId(uid.intValue()); couponUserDO.setCouponId(couponDO.getId().intValue()); couponUserDO.setEndTime(couponDO.getEndTime()); couponUserDO.setExchangeCode(code); appCouponUserService.save(couponUserDO); int newRecive = couponDO.getReceive() + 1; couponDO.setReceive(newRecive); couponMapper.updateById(couponDO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/service/coupon/CouponService.java ================================================ package co.yixiang.yshop.module.coupon.service.coupon; import java.util.*; import jakarta.validation.*; import co.yixiang.yshop.module.coupon.controller.admin.coupon.vo.*; import co.yixiang.yshop.module.coupon.dal.dataobject.coupon.CouponDO; import co.yixiang.yshop.framework.common.pojo.PageResult; /** * 优惠券 Service 接口 * * @author yshop */ public interface CouponService { /** * 创建优惠券 * * @param createReqVO 创建信息 * @return 编号 */ Long create(@Valid CouponCreateReqVO createReqVO); /** * 更新优惠券 * * @param updateReqVO 更新信息 */ void update(@Valid CouponUpdateReqVO updateReqVO); /** * 删除优惠券 * * @param id 编号 */ void delete(Long id); /** * 获得优惠券 * * @param id 编号 * @return 优惠券 */ CouponDO get(Long id); /** * 获得优惠券列表全店铺通用 * * @return 优惠券列表 */ List getList(); /** * 获得优惠券分页 * * @param pageReqVO 分页查询 * @return 优惠券分页 */ PageResult getPage(CouponPageReqVO pageReqVO); /** * 获得优惠券列表, 用于 Excel 导出 * * @param exportReqVO 查询条件 * @return 优惠券列表 */ List getList(CouponExportReqVO exportReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/service/coupon/CouponServiceImpl.java ================================================ package co.yixiang.yshop.module.coupon.service.coupon; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.coupon.controller.admin.coupon.vo.CouponCreateReqVO; import co.yixiang.yshop.module.coupon.controller.admin.coupon.vo.CouponExportReqVO; import co.yixiang.yshop.module.coupon.controller.admin.coupon.vo.CouponPageReqVO; import co.yixiang.yshop.module.coupon.controller.admin.coupon.vo.CouponUpdateReqVO; import co.yixiang.yshop.module.coupon.convert.coupon.CouponConvert; import co.yixiang.yshop.module.coupon.dal.dataobject.coupon.CouponDO; import co.yixiang.yshop.module.coupon.dal.mysql.coupon.CouponMapper; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; import co.yixiang.yshop.module.store.dal.mysql.storeshop.StoreShopMapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.Collection; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.coupon.enums.ErrorCodeConstants.COUPON_NOT_EXISTS; /** * 优惠券 Service 实现类 * * @author yshop */ @Service @Validated public class CouponServiceImpl implements CouponService { @Resource private CouponMapper Mapper; @Resource private StoreShopMapper storeShopMapper; @Override public Long create(CouponCreateReqVO createReqVO) { // 插入 CouponDO couponDO = CouponConvert.INSTANCE.convert(createReqVO); StoreShopDO storeShopDO = storeShopMapper.selectById(createReqVO.getShopId()); couponDO.setShopName(storeShopDO.getName()); Mapper.insert(couponDO); // 返回 return couponDO.getId(); } @Override public void update(CouponUpdateReqVO updateReqVO) { // 校验存在 validateExists(updateReqVO.getId()); // 更新 CouponDO updateObj = CouponConvert.INSTANCE.convert(updateReqVO); StoreShopDO storeShopDO = storeShopMapper.selectById(updateReqVO.getShopId()); updateObj.setShopName(storeShopDO.getName()); Mapper.updateById(updateObj); } @Override public void delete(Long id) { // 校验存在 validateExists(id); // 删除 Mapper.deleteById(id); } private void validateExists(Long id) { if (Mapper.selectById(id) == null) { throw exception(COUPON_NOT_EXISTS); } } @Override public CouponDO get(Long id) { return Mapper.selectById(id); } @Override public List getList() { return Mapper.selectList(new LambdaQueryWrapper().eq(CouponDO::getShopId,0)); } @Override public PageResult getPage(CouponPageReqVO pageReqVO) { return Mapper.selectPage(pageReqVO); } @Override public List getList(CouponExportReqVO exportReqVO) { return Mapper.selectList(exportReqVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/service/couponuser/AppCouponUserService.java ================================================ package co.yixiang.yshop.module.coupon.service.couponuser; import co.yixiang.yshop.module.coupon.controller.app.coupon.vo.AppMyCouponVO; import co.yixiang.yshop.module.coupon.dal.dataobject.couponuser.CouponUserDO; import com.baomidou.mybatisplus.extension.service.IService; import java.util.List; /** * 用户领的优惠券 Service 接口 * * @author yshop */ public interface AppCouponUserService extends IService { /** * 获取我的优惠券列表 * @param shopId 店铺id * @param type * @param page * @param pagesize * @return */ List getList(Long uid, Long shopId,int type, int page, int pagesize); } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/service/couponuser/AppCouponUserServiceImpl.java ================================================ package co.yixiang.yshop.module.coupon.service.couponuser; import co.yixiang.yshop.framework.common.enums.ShopCommonEnum; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.coupon.controller.app.coupon.vo.AppMyCouponVO; import co.yixiang.yshop.module.coupon.convert.couponuser.CouponUserConvert; import co.yixiang.yshop.module.coupon.dal.dataobject.couponuser.CouponUserDO; import co.yixiang.yshop.module.coupon.dal.mysql.couponuser.CouponUserMapper; import co.yixiang.yshop.module.coupon.enums.CouponStatusEnum; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.time.LocalDateTime; import java.util.List; /** * 用户领的优惠券 Service 实现类 * * @author yshop */ @Service @Validated public class AppCouponUserServiceImpl extends ServiceImpl implements AppCouponUserService { @Resource private CouponUserMapper userMapper; /** * 获取我的优惠券列表 * @param shopId 店铺id * @param page * @param pagesize * @return */ @Override public List getList(Long uid, Long shopId, int type, int page, int pagesize) { LocalDateTime nowTime = LocalDateTime.now(); Page pageModel = new Page<>(page, pagesize); LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX<>(); switch (CouponStatusEnum.toType(type)) { case STATUS_0: wrapper.eq(CouponUserDO::getStatus,CouponStatusEnum.STATUS_0.getValue()) .lt(CouponUserDO::getStartTime,nowTime) .gt(CouponUserDO::getEndTime,nowTime); break; case STATUS_1: wrapper.eq(CouponUserDO::getStatus,CouponStatusEnum.STATUS_1.getValue()) .lt(CouponUserDO::getStartTime,nowTime) .gt(CouponUserDO::getEndTime,nowTime); break; case STATUS_2: wrapper.lt(CouponUserDO::getEndTime,nowTime); break; default: log.warn("为了遵循阿里巴巴规范"); } wrapper.eqIfPresent(CouponUserDO::getShopId, shopId) .eq(CouponUserDO::getUserId,uid); IPage pageList = this.baseMapper.selectPage(pageModel, wrapper); return CouponUserConvert.INSTANCE.convertList03(pageList.getRecords()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/service/couponuser/CouponUserService.java ================================================ package co.yixiang.yshop.module.coupon.service.couponuser; import java.util.*; import jakarta.validation.*; import co.yixiang.yshop.module.coupon.controller.admin.couponuser.vo.*; import co.yixiang.yshop.module.coupon.dal.dataobject.couponuser.CouponUserDO; import co.yixiang.yshop.framework.common.pojo.PageResult; /** * 用户领的优惠券 Service 接口 * * @author yshop */ public interface CouponUserService { /** * 创建用户领的优惠券 * * @param createReqVO 创建信息 * @return 编号 */ Integer createUser(@Valid CouponUserCreateReqVO createReqVO); /** * 更新用户领的优惠券 * * @param updateReqVO 更新信息 */ void updateUser(@Valid CouponUserUpdateReqVO updateReqVO); /** * 删除用户领的优惠券 * * @param id 编号 */ void deleteUser(Integer id); /** * 获得用户领的优惠券 * * @param id 编号 * @return 用户领的优惠券 */ CouponUserDO getUser(Integer id); /** * 获得用户领的优惠券列表 * * @param id 编号 * @return 用户领的优惠券列表 */ List getUserList(Integer id); /** * 获得用户领的优惠券分页 * * @param pageReqVO 分页查询 * @return 用户领的优惠券分页 */ PageResult getUserPage(CouponUserPageReqVO pageReqVO); /** * 获得用户领的优惠券列表, 用于 Excel 导出 * * @param exportReqVO 查询条件 * @return 用户领的优惠券列表 */ List getUserList(CouponUserExportReqVO exportReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/java/co/yixiang/yshop/module/coupon/service/couponuser/CouponUserServiceImpl.java ================================================ package co.yixiang.yshop.module.coupon.service.couponuser; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import java.util.*; import co.yixiang.yshop.module.coupon.controller.admin.couponuser.vo.*; import co.yixiang.yshop.module.coupon.dal.dataobject.couponuser.CouponUserDO; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.coupon.convert.couponuser.CouponUserConvert; import co.yixiang.yshop.module.coupon.dal.mysql.couponuser.CouponUserMapper; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.coupon.enums.ErrorCodeConstants.*; /** * 用户领的优惠券 Service 实现类 * * @author yshop */ @Service @Validated public class CouponUserServiceImpl implements CouponUserService { @Resource private CouponUserMapper userMapper; @Override public Integer createUser(CouponUserCreateReqVO createReqVO) { // 插入 CouponUserDO user = CouponUserConvert.INSTANCE.convert(createReqVO); userMapper.insert(user); // 返回 return user.getId(); } @Override public void updateUser(CouponUserUpdateReqVO updateReqVO) { // 校验存在 validateUserExists(updateReqVO.getId()); // 更新 CouponUserDO updateObj = CouponUserConvert.INSTANCE.convert(updateReqVO); userMapper.updateById(updateObj); } @Override public void deleteUser(Integer id) { // 校验存在 validateUserExists(id); // 删除 userMapper.deleteById(id); } private void validateUserExists(Integer id) { if (userMapper.selectById(id) == null) { throw exception(COUPON_USER_NOT_EXISTS); } } @Override public CouponUserDO getUser(Integer id) { return userMapper.selectById(id); } @Override public List getUserList(Integer id) { CouponUserExportReqVO exportReqVO = new CouponUserExportReqVO(); exportReqVO.setCouponId(id); return userMapper.selectList(exportReqVO); } @Override public PageResult getUserPage(CouponUserPageReqVO pageReqVO) { return userMapper.selectPage(pageReqVO); } @Override public List getUserList(CouponUserExportReqVO exportReqVO) { return userMapper.selectList(exportReqVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-marketing/yshop-module-coupon-biz/src/main/resources/mapper/coupon/CouponMapper.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-member/pom.xml ================================================ co.yixiang.boot yshop ${revision} 4.0.0 yshop-module-member-api yshop-module-member-biz yshop-module-member pom ${project.artifactId} member 模块,我们放会员业务。 例如说:会员中心等等 ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-api/pom.xml ================================================ co.yixiang.boot yshop-module-member ${revision} 4.0.0 yshop-module-member-api jar ${project.artifactId} member 模块 API,暴露给其它模块调用 co.yixiang.boot yshop-common ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-api/src/main/java/co/yixiang/yshop/module/member/api/package-info.java ================================================ /** * member API 包,定义暴露给其它模块的 API */ package co.yixiang.yshop.module.member.api; ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-api/src/main/java/co/yixiang/yshop/module/member/api/user/MemberUserApi.java ================================================ package co.yixiang.yshop.module.member.api.user; import co.yixiang.yshop.module.member.api.user.dto.MemberUserRespDTO; import co.yixiang.yshop.module.member.api.user.dto.WechatUserDto; import java.util.Collection; import java.util.List; import java.util.Map; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertMap; /** * 会员用户的 API 接口 * * @author yshop */ public interface MemberUserApi { void saveWechatMember(WechatUserDto wechatUserDto); /** * 获得会员用户信息 * * @param id 用户编号 * @return 用户信息 */ MemberUserRespDTO getUser(Long id); /** * 获得会员用户信息们 * * @param ids 用户编号的数组 * @return 用户信息们 */ List getUsers(Collection ids); /** * 获得会员用户 Map * * @param ids 用户编号的数组 * @return 会员用户 Map */ default Map getUserMap(Collection ids) { return convertMap(getUsers(ids), MemberUserRespDTO::getId); } /** * 基于用户昵称,模糊匹配用户列表 * * @param nickname 用户昵称,模糊匹配 * @return 用户信息的列表 */ List getUserListByNickname(String nickname); /** * 基于手机号,精准匹配用户 * * @param mobile 手机号 * @return 用户信息 */ MemberUserRespDTO getUserByMobile(String mobile); } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-api/src/main/java/co/yixiang/yshop/module/member/api/user/dto/MemberUserRespDTO.java ================================================ package co.yixiang.yshop.module.member.api.user.dto; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import lombok.Data; /** * 用户信息 Response DTO * * @author yshop */ @Data public class MemberUserRespDTO { /** * 用户ID */ private Long id; /** * 用户昵称 */ private String nickname; /** * 帐号状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; /** * 手机 */ private String mobile; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-api/src/main/java/co/yixiang/yshop/module/member/api/user/dto/WechatUserDto.java ================================================ package co.yixiang.yshop.module.member.api.user.dto; import lombok.*; /** * @ClassName WechatUserDTO * @Author hupeng <610796224@qq.com> * @Date 2023/7/18 **/ @Getter @Setter @Builder @AllArgsConstructor @NoArgsConstructor public class WechatUserDto { private String openid; private String unionId; private String routineOpenid; private String nickname; private String headimgurl; private String sex; private String city; private String language; private String province; private String country; private Boolean subscribe; private Long subscribeTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-api/src/main/java/co/yixiang/yshop/module/member/enums/BillDetailEnum.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.member.enums; import lombok.AllArgsConstructor; import lombok.Getter; /** * @author hupeng * 账单明细相关枚举 */ @Getter @AllArgsConstructor public enum BillDetailEnum { TYPE_1("recharge","充值"), TYPE_2("brokerage","返佣"), TYPE_3("pay_product","消费"), TYPE_4("extract","提现"), TYPE_5("pay_product_refund","退款"), TYPE_6("system_add","系统添加"), TYPE_7("system_sub","系统减少"), TYPE_8("deduction","减去"), TYPE_9("gain","奖励"), TYPE_10("sign","签到"), CATEGORY_1("now_money","金额"), CATEGORY_2("integral","积分"); private String value; private String desc; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-api/src/main/java/co/yixiang/yshop/module/member/enums/BillEnum.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.member.enums; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.stream.Stream; /** * @author hupeng * 账单相关枚举 */ @Getter @AllArgsConstructor public enum BillEnum { PM_0(0,"支出"), PM_1(1,"获得"), STATUS_0(0,"默认"), STATUS_1(1,"有效"), STATUS_2(2,"无效"); private Integer value; private String desc; public static BillEnum toType(int value) { return Stream.of(BillEnum.values()) .filter(p -> p.value == value) .findAny() .orElse(null); } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-api/src/main/java/co/yixiang/yshop/module/member/enums/ErrorCodeConstants.java ================================================ package co.yixiang.yshop.module.member.enums; import co.yixiang.yshop.framework.common.exception.ErrorCode; /** * Member 错误码枚举类 * * member 系统,使用 1-004-000-000 段 */ public interface ErrorCodeConstants { // ========== 用户相关 1004001000============ ErrorCode USER_NOT_EXISTS = new ErrorCode(1004001000, "用户不存在"); ErrorCode USER_PASSWORD_FAILED = new ErrorCode(1004001001, "密码校验失败"); // ========== AUTH 模块 1004003000 ========== ErrorCode AUTH_LOGIN_BAD_CREDENTIALS = new ErrorCode(1004003000, "登录失败,账号密码不正确"); ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1004003001, "登录失败,账号被禁用"); ErrorCode AUTH_TOKEN_EXPIRED = new ErrorCode(1004003004, "Token 已经过期"); ErrorCode AUTH_THIRD_LOGIN_NOT_BIND = new ErrorCode(1004003005, "未绑定账号,需要进行绑定"); ErrorCode AUTH_WEIXIN_MINI_APP_PHONE_CODE_ERROR = new ErrorCode(1004003006, "获得手机号失败"); // ========== 用户收件地址 1004004000 ========== ErrorCode USER_ADDRESS_NOT_EXISTS = new ErrorCode(1004004000, "用户收件地址不存在"); ErrorCode USER_ADDRESS_PARAM_NOT_EXISTS = new ErrorCode(1004004001, "用户收件地址参数错误"); ErrorCode USER_BILL_NOT_EXISTS = new ErrorCode(1004004001, "用户账单不存在"); ErrorCode MINI_AUTH_LOGIN_BAD = new ErrorCode(1004004002, "登录失败,请联系管理员"); ErrorCode COUPON_NOT_CONDITION = new ErrorCode(1004004003, "此优惠券不满足使用提交"); ErrorCode MINI_AUTH_LOGIN_BAD2 = new ErrorCode(1004004002, "登录失败,请返回首页刷新重新登录"); } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-api/src/main/java/co/yixiang/yshop/module/member/enums/LoginTypeEnum.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co */ package co.yixiang.yshop.module.member.enums; import lombok.AllArgsConstructor; import lombok.Getter; /** * @author hupeng * 账单明细相关枚举 */ @Getter @AllArgsConstructor public enum LoginTypeEnum { WEIXIN_H5("weixinh5","weixinh5"), H5("h5","H5"), WECHAT("wechat","公众号"), APP("app","APP"), PC("pc","PC"), ROUNTINE("routine","小程序"), UNIAPPH5("uniappH5","uniappH5"); // // WXAPP("wxapp","微信小程序"), // ALIAPP("aliapp","支付宝小程序"), // WECHAT("wechat","微信公众号"), // H5("h5","h5"), // PC("pc","pc"), // APP("app","APP"); private String value; private String desc; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/pom.xml ================================================ co.yixiang.boot yshop-module-member ${revision} 4.0.0 yshop-module-member-biz jar ${project.artifactId} member 模块,我们放会员业务。 例如说:会员中心等等 co.yixiang.boot yshop-module-member-api ${revision} co.yixiang.boot yshop-module-system-api ${revision} co.yixiang.boot yshop-module-infra-api ${revision} co.yixiang.boot yshop-module-coupon-biz ${revision} co.yixiang.boot yshop-module-shop-biz ${revision} com.github.binarywang wx-java-mp-spring-boot-starter com.github.binarywang wx-java-miniapp-spring-boot-starter co.yixiang.boot yshop-spring-boot-starter-biz-tenant co.yixiang.boot yshop-spring-boot-starter-security org.springframework.boot spring-boot-starter-validation co.yixiang.boot yshop-spring-boot-starter-mybatis co.yixiang.boot yshop-spring-boot-starter-redis co.yixiang.boot yshop-spring-boot-starter-mq co.yixiang.boot yshop-spring-boot-starter-test test co.yixiang.boot yshop-spring-boot-starter-biz-ip ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/api/package-info.java ================================================ package co.yixiang.yshop.module.member.api; ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/api/user/MemberUserApiImpl.java ================================================ package co.yixiang.yshop.module.member.api.user; import cn.hutool.core.util.IdUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.enums.ShopCommonEnum; import co.yixiang.yshop.module.member.api.user.dto.MemberUserRespDTO; import co.yixiang.yshop.module.member.api.user.dto.WechatUserDto; import co.yixiang.yshop.module.member.convert.user.UserConvert; import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO; import co.yixiang.yshop.module.member.service.user.MemberUserService; import jakarta.annotation.Resource; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.util.Collection; import java.util.List; /** * 会员用户的 API 实现类 * * @author yshop */ @Service @Validated public class MemberUserApiImpl implements MemberUserApi { @Resource private MemberUserService userService; @Resource private PasswordEncoder passwordEncoder; @Override public void saveWechatMember(WechatUserDto wechatUserDto) { MemberUserDO user = new MemberUserDO(); user.setNickname(wechatUserDto.getNickname()); user.setAvatar(wechatUserDto.getHeadimgurl()); // 生成密码 String password = IdUtil.fastSimpleUUID(); user.setPassword(encodePassword(password)); user.setUsername(wechatUserDto.getOpenid()); user.setLoginType("wechat"); user.setWxProfile(wechatUserDto); userService.save(user); } @Override public MemberUserRespDTO getUser(Long id) { MemberUserDO user = userService.getUser(id); return UserConvert.INSTANCE.convert2(user); } @Override public List getUsers(Collection ids) { return UserConvert.INSTANCE.convertList2(userService.getUserList(ids)); } @Override public List getUserListByNickname(String nickname) { return UserConvert.INSTANCE.convertList2(userService.getUserListByNickname(nickname)); } @Override public MemberUserRespDTO getUserByMobile(String mobile) { return UserConvert.INSTANCE.convert2(userService.getUserByMobile(mobile)); } /** * 对密码进行加密 * * @param password 密码 * @return 加密后的密码 */ private String encodePassword(String password) { return passwordEncoder.encode(password); } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/user/MemberUserController.java ================================================ package co.yixiang.yshop.module.member.controller.admin.user; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.member.controller.admin.user.vo.*; import co.yixiang.yshop.module.member.convert.user.UserConvert; import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO; import co.yixiang.yshop.module.member.service.user.UserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.Collection; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 用户") @RestController @RequestMapping("/member/user") @Validated public class MemberUserController { @Resource private UserService userService; @PostMapping("/create") @Operation(summary = "创建用户") @PreAuthorize("@ss.hasPermission('member:user:create')") public CommonResult createUser(@Valid @RequestBody UserCreateReqVO createReqVO) { return success(userService.createUser(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新用户") @PreAuthorize("@ss.hasPermission('member:user:update')") public CommonResult updateUser(@Valid @RequestBody UserUpdateReqVO updateReqVO) { userService.updateUser(updateReqVO); return success(true); } @PutMapping("/updateMony") @Operation(summary = "更新用户余额与积分") @PreAuthorize("@ss.hasPermission('member:user:update')") public CommonResult updateMony(@Valid @RequestBody UserUpdateMoneyReqVO updateReqVO) { userService.updateMony(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除用户") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('member:user:delete')") public CommonResult deleteUser(@RequestParam("id") Long id) { userService.deleteUser(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得用户") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('member:user:query')") public CommonResult getUser(@RequestParam("id") Long id) { MemberUserDO user = userService.getUser(id); return success(UserConvert.INSTANCE.convert(user,true)); } @GetMapping("/list") @Operation(summary = "获得用户列表") @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") @PreAuthorize("@ss.hasPermission('member:user:query')") public CommonResult> getUserList(@RequestParam("ids") Collection ids) { List list = userService.getUserList(ids); return success(UserConvert.INSTANCE.convertList(list)); } @GetMapping("/page") @Operation(summary = "获得用户分页") @PreAuthorize("@ss.hasPermission('member:user:query')") public CommonResult> getUserPage(@Valid UserPageReqVO pageVO) { PageResult pageResult = userService.getUserPage(pageVO); return success(UserConvert.INSTANCE.convertPage(pageResult)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/user/vo/UserBaseVO.java ================================================ package co.yixiang.yshop.module.member.controller.admin.user.vo; import co.yixiang.yshop.module.member.api.user.dto.WechatUserDto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.math.BigDecimal; /** * 用户 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class UserBaseVO { @Schema(description = "用户账户(跟accout一样)", example = "王五") private String username; @Schema(description = "真实姓名", example = "王五") private String realName; @Schema(description = "用户昵称", example = "李四") private String nickname; @Schema(description = "用户头像") private String avatar; //@MobileDesensitize @Schema(description = "手机号码") private String mobile; @Schema(description = "添加ip") private String addIp; @Schema(description = "用户余额", required = true) //@NotNull(message = "用户余额不能为空") private BigDecimal nowMoney; @Schema(description = "佣金金额", required = true, example = "14395") //@NotNull(message = "佣金金额不能为空") private BigDecimal brokeragePrice; @Schema(description = "用户剩余积分", required = true) // @NotNull(message = "用户剩余积分不能为空") private BigDecimal integral; @Schema(description = "1为正常,0为禁止", required = true, example = "2") // @NotNull(message = "1为正常,0为禁止不能为空") private Integer status; @Schema(description = "是否为推广员", required = true) // @NotNull(message = "是否为推广员不能为空") private Integer isPromoter; @Schema(description = "用户购买次数", example = "16061") private Integer payCount; @Schema(description = "下级人数", example = "4960") private Integer spreadCount; @Schema(description = "详细地址", required = true) // @NotNull(message = "详细地址不能为空") private String addres; @Schema(description = "管理员编号 ", example = "29490") private Integer adminid; @Schema(description = "用户登陆类型,h5,wechat,routine", required = true, example = "2") // @NotNull(message = "用户登陆类型,h5,wechat,routine不能为空") private String loginType; @Schema(description = "微信用户json信息") private WechatUserDto wxProfile; @Schema(description = "生日") private String birthday; /** * 最后一次登录ip */ private String loginIp; /** * 最后一次登录ip */ private String lastIp; /** * 等级 */ private Integer level; /** * 推广元id */ private Long spreadUid; /** * 身份证号码 */ private String cardId; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/user/vo/UserCreateReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.admin.user.vo; import jakarta.validation.constraints.NotNull; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 用户创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class UserCreateReqVO extends UserBaseVO { @Schema(description = "用户密码(跟pwd)") private String password; @Schema(description = "生日") private String birthday; @Schema(description = "身份证号码", example = "29961") private String cardId; @Schema(description = "用户备注") private String mark; @Schema(description = "合伙人id", example = "4234") private Integer partnerId; @Schema(description = "用户分组id", example = "12625") private Integer groupId; @Schema(description = "最后一次登录ip") private String lastIp; @Schema(description = "连续签到天数", required = true) @NotNull(message = "连续签到天数不能为空") private Integer signNum; @Schema(description = "等级", required = true) @NotNull(message = "等级不能为空") private Integer level; @Schema(description = "推广元id", example = "5747") private Long spreadUid; @Schema(description = "推广员关联时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime spreadTime; @Schema(description = "用户类型", required = true, example = "1") @NotNull(message = "用户类型不能为空") private String userType; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/user/vo/UserExportReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.admin.user.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import java.time.LocalDateTime; import org.springframework.format.annotation.DateTimeFormat; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 用户 Excel 导出 Request VO,参数和 UserPageReqVO 是一致的") @Data public class UserExportReqVO { @Schema(description = "用户账户(跟accout一样)", example = "王五") private String username; @Schema(description = "真实姓名", example = "王五") private String realName; @Schema(description = "用户昵称", example = "李四") private String nickname; @Schema(description = "手机号码") private String phone; @Schema(description = "添加时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/user/vo/UserPageReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.admin.user.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 用户分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class UserPageReqVO extends PageParam { @Schema(description = "用户账户(跟accout一样)", example = "王五") private String username; @Schema(description = "真实姓名", example = "王五") private String realName; @Schema(description = "用户昵称", example = "李四") private String nickname; @Schema(description = "手机号码") private String mobile; private String phone; @Schema(description = "添加时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; @Schema(description = "登录类型") private String loginType; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/user/vo/UserRespVO.java ================================================ package co.yixiang.yshop.module.member.controller.admin.user.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; @Schema(description = "管理后台 - 用户 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class UserRespVO extends UserBaseVO { @Schema(description = "用户id", required = true, example = "16370") private Long id; @Schema(description = "添加时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/user/vo/UserUpdateMoneyReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.admin.user.vo; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.ToString; @Schema(description = "管理后台 - 用户更新 Request VO") @Data @ToString(callSuper = true) public class UserUpdateMoneyReqVO { @Schema(description = "用户id", required = true, example = "16370") @NotNull(message = "用户id不能为空") private Long id; @Schema(description = "修改金额类型") private Integer ptype; @Schema(description = "金额") @NotNull(message = "金额不能为空") private String money; @Schema(description = "修改积分类型") private Integer itype; @Schema(description = "积分") @NotNull(message = "积分不能为空") private String integral; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/user/vo/UserUpdateReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.admin.user.vo; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.*; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 用户更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class UserUpdateReqVO extends UserBaseVO { @Schema(description = "用户id", required = true, example = "16370") @NotNull(message = "用户id不能为空") private Long id; @Schema(description = "用户密码(跟pwd)") private String password; @Schema(description = "生日") private String birthday; @Schema(description = "身份证号码", example = "29961") private String cardId; @Schema(description = "用户备注") private String mark; @Schema(description = "合伙人id", example = "4234") private Integer partnerId; @Schema(description = "用户分组id", example = "12625") private Integer groupId; @Schema(description = "最后一次登录ip") private String lastIp; @Schema(description = "连续签到天数", required = true) //@NotNull(message = "连续签到天数不能为空") private Integer signNum; @Schema(description = "等级", required = true) //@NotNull(message = "等级不能为空") private Integer level; @Schema(description = "推广元id", example = "5747") private Long spreadUid; @Schema(description = "推广员关联时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime spreadTime; @Schema(description = "用户类型", required = true, example = "1") // @NotNull(message = "用户类型不能为空") private String userType; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/useraddress/UserAddressController.java ================================================ package co.yixiang.yshop.module.member.controller.admin.useraddress; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.member.controller.admin.useraddress.vo.UserAddressCreateReqVO; import co.yixiang.yshop.module.member.controller.admin.useraddress.vo.UserAddressPageReqVO; import co.yixiang.yshop.module.member.controller.admin.useraddress.vo.UserAddressRespVO; import co.yixiang.yshop.module.member.controller.admin.useraddress.vo.UserAddressUpdateReqVO; import co.yixiang.yshop.module.member.convert.useraddress.UserAddressConvert; import co.yixiang.yshop.module.member.dal.dataobject.useraddress.UserAddressDO; import co.yixiang.yshop.module.member.service.useraddress.UserAddressService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.Collection; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 用户地址") @RestController @RequestMapping("/member/user-address") @Validated public class UserAddressController { @Resource private UserAddressService userAddressService; @PostMapping("/create") @Operation(summary = "创建用户地址") @PreAuthorize("@ss.hasPermission('member:user-address:create')") public CommonResult createUserAddress(@Valid @RequestBody UserAddressCreateReqVO createReqVO) { return success(userAddressService.createUserAddress(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新用户地址") @PreAuthorize("@ss.hasPermission('member:user-address:update')") public CommonResult updateUserAddress(@Valid @RequestBody UserAddressUpdateReqVO updateReqVO) { userAddressService.updateUserAddress(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除用户地址") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('member:user-address:delete')") public CommonResult deleteUserAddress(@RequestParam("id") Long id) { userAddressService.deleteUserAddress(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得用户地址") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('member:user-address:query')") public CommonResult getUserAddress(@RequestParam("id") Long id) { UserAddressDO userAddress = userAddressService.getUserAddress(id); return success(UserAddressConvert.INSTANCE.convert(userAddress)); } @GetMapping("/list") @Operation(summary = "获得用户地址列表") @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") @PreAuthorize("@ss.hasPermission('member:user-address:query')") public CommonResult> getUserAddressList(@RequestParam("ids") Collection ids) { List list = userAddressService.getUserAddressList(ids); return success(UserAddressConvert.INSTANCE.convertList(list)); } @GetMapping("/page") @Operation(summary = "获得用户地址分页") @PreAuthorize("@ss.hasPermission('member:user-address:query')") public CommonResult> getUserAddressPage(@Valid UserAddressPageReqVO pageVO) { PageResult pageResult = userAddressService.getUserAddressPage(pageVO); return success(UserAddressConvert.INSTANCE.convertPage(pageResult)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/useraddress/vo/UserAddressBaseVO.java ================================================ package co.yixiang.yshop.module.member.controller.admin.useraddress.vo; import co.yixiang.yshop.framework.desensitize.core.slider.annotation.MobileDesensitize; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; /** * 用户地址 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class UserAddressBaseVO { @Schema(description = "用户id", required = true, example = "25124") @NotNull(message = "用户id不能为空") private Long uid; @Schema(description = "收货人姓名", required = true, example = "李四") @NotNull(message = "收货人姓名不能为空") private String realName; @MobileDesensitize @Schema(description = "收货人电话", required = true) @NotNull(message = "收货人电话不能为空") private String phone; @Schema(description = "收货人所在省", required = true) @NotNull(message = "收货人所在省不能为空") private String province; @Schema(description = "收货人所在市", required = true) @NotNull(message = "收货人所在市不能为空") private String city; @Schema(description = "城市id", example = "15595") private Integer cityId; @Schema(description = "收货人所在区", required = true) @NotNull(message = "收货人所在区不能为空") private String district; @Schema(description = "收货人详细地址", required = true) @NotNull(message = "收货人详细地址不能为空") private String detail; @Schema(description = "邮编", required = true) @NotNull(message = "邮编不能为空") private String postCode; @Schema(description = "经度", required = true) @NotNull(message = "经度不能为空") private String longitude; @Schema(description = "纬度", required = true) @NotNull(message = "纬度不能为空") private String latitude; @Schema(description = "是否默认", required = true) @NotNull(message = "是否默认不能为空") private Integer isDefault; private String address; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/useraddress/vo/UserAddressCreateReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.admin.useraddress.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "管理后台 - 用户地址创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class UserAddressCreateReqVO extends UserAddressBaseVO { } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/useraddress/vo/UserAddressExportReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.admin.useraddress.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import java.time.LocalDateTime; import org.springframework.format.annotation.DateTimeFormat; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 用户地址 Excel 导出 Request VO,参数和 UserAddressPageReqVO 是一致的") @Data public class UserAddressExportReqVO { @Schema(description = "用户id", example = "25124") private Long uid; @Schema(description = "收货人姓名", example = "李四") private String realName; @Schema(description = "收货人电话") private String phone; @Schema(description = "收货人所在省") private String province; @Schema(description = "收货人所在市") private String city; @Schema(description = "城市id", example = "15595") private Integer cityId; @Schema(description = "收货人所在区") private String district; @Schema(description = "收货人详细地址") private String detail; @Schema(description = "邮编") private String postCode; @Schema(description = "经度") private String longitude; @Schema(description = "纬度") private String latitude; @Schema(description = "是否默认") private Byte isDefault; @Schema(description = "添加时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/useraddress/vo/UserAddressPageReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.admin.useraddress.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 用户地址分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class UserAddressPageReqVO extends PageParam { @Schema(description = "用户id", example = "25124") private Long uid; @Schema(description = "收货人姓名", example = "李四") private String realName; @Schema(description = "收货人电话") private String phone; @Schema(description = "收货人所在省") private String province; @Schema(description = "收货人所在市") private String city; @Schema(description = "城市id", example = "15595") private Integer cityId; @Schema(description = "收货人所在区") private String district; @Schema(description = "收货人详细地址") private String detail; @Schema(description = "邮编") private String postCode; @Schema(description = "经度") private String longitude; @Schema(description = "纬度") private String latitude; @Schema(description = "是否默认") private Byte isDefault; @Schema(description = "添加时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/useraddress/vo/UserAddressRespVO.java ================================================ package co.yixiang.yshop.module.member.controller.admin.useraddress.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; @Schema(description = "管理后台 - 用户地址 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class UserAddressRespVO extends UserAddressBaseVO { @Schema(description = "用户地址id", required = true, example = "24169") private Long id; @Schema(description = "添加时间", required = true) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/useraddress/vo/UserAddressUpdateReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.admin.useraddress.vo; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.*; import java.util.*; @Schema(description = "管理后台 - 用户地址更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class UserAddressUpdateReqVO extends UserAddressBaseVO { @Schema(description = "用户地址id", required = true, example = "24169") @NotNull(message = "用户地址id不能为空") private Long id; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/userbill/UserBillController.java ================================================ package co.yixiang.yshop.module.member.controller.admin.userbill; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.member.controller.admin.userbill.vo.UserBillPageReqVO; import co.yixiang.yshop.module.member.controller.admin.userbill.vo.UserBillRespVO; import co.yixiang.yshop.module.member.convert.userbill.UserBillConvert; import co.yixiang.yshop.module.member.dal.dataobject.userbill.UserBillDO; import co.yixiang.yshop.module.member.service.userbill.UserBillService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; 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 static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 用户账单") @RestController @RequestMapping("/member/user-bill") @Validated public class UserBillController { @Resource private UserBillService userBillService; @GetMapping("/page") @Operation(summary = "获得用户账单分页") @PreAuthorize("@ss.hasPermission('member:user-bill:query')") public CommonResult> getUserBillPage(@Valid UserBillPageReqVO pageVO) { PageResult pageResult = userBillService.getUserBillPage(pageVO); return success(UserBillConvert.INSTANCE.convertPage(pageResult)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/userbill/vo/UserBillBaseVO.java ================================================ package co.yixiang.yshop.module.member.controller.admin.userbill.vo; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.*; import java.util.*; import java.math.BigDecimal; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.LocalDateTime; /** * 用户账单 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class UserBillBaseVO { @Schema(description = "用户uid", required = true, example = "9419") @NotNull(message = "用户uid不能为空") private Long uid; @Schema(description = "关联id", required = true, example = "18439") @NotNull(message = "关联id不能为空") private String linkId; @Schema(description = "0 = 支出 1 = 获得", required = true) @NotNull(message = "0 = 支出 1 = 获得不能为空") private Byte pm; @Schema(description = "账单标题", required = true) @NotNull(message = "账单标题不能为空") private String title; @Schema(description = "明细种类", required = true) @NotNull(message = "明细种类不能为空") private String category; @Schema(description = "明细类型", required = true, example = "2") @NotNull(message = "明细类型不能为空") private String type; @Schema(description = "明细数字", required = true) @NotNull(message = "明细数字不能为空") private BigDecimal number; @Schema(description = "剩余", required = true) @NotNull(message = "剩余不能为空") private BigDecimal balance; @Schema(description = "备注", required = true) @NotNull(message = "备注不能为空") private String mark; @Schema(description = "0 = 带确定 1 = 有效 -1 = 无效", required = true, example = "1") @NotNull(message = "0 = 带确定 1 = 有效 -1 = 无效不能为空") private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/userbill/vo/UserBillCreateReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.admin.userbill.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "管理后台 - 用户账单创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class UserBillCreateReqVO extends UserBillBaseVO { } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/userbill/vo/UserBillExportReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.admin.userbill.vo; import lombok.*; import java.math.BigDecimal; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import java.time.LocalDateTime; import org.springframework.format.annotation.DateTimeFormat; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 用户账单 Excel 导出 Request VO,参数和 UserBillPageReqVO 是一致的") @Data public class UserBillExportReqVO { @Schema(description = "用户uid", example = "9419") private Long uid; @Schema(description = "关联id", example = "18439") private String linkId; @Schema(description = "0 = 支出 1 = 获得") private Byte pm; @Schema(description = "账单标题") private String title; @Schema(description = "明细种类") private String category; @Schema(description = "明细类型", example = "2") private String type; @Schema(description = "明细数字") private BigDecimal number; @Schema(description = "剩余") private BigDecimal balance; @Schema(description = "备注") private String mark; @Schema(description = "添加时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; @Schema(description = "0 = 带确定 1 = 有效 -1 = 无效", example = "1") private Boolean status; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/userbill/vo/UserBillPageReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.admin.userbill.vo; import lombok.*; import java.math.BigDecimal; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 用户账单分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class UserBillPageReqVO extends PageParam { @Schema(description = "用户uid", example = "9419") private Long uid; @Schema(description = "关联id", example = "18439") private String linkId; @Schema(description = "0 = 支出 1 = 获得") private Byte pm; @Schema(description = "账单标题") private String title; @Schema(description = "明细种类") private String category; @Schema(description = "明细类型", example = "2") private String type; @Schema(description = "明细数字") private BigDecimal number; @Schema(description = "剩余") private BigDecimal balance; @Schema(description = "备注") private String mark; @Schema(description = "添加时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; @Schema(description = "0 = 带确定 1 = 有效 -1 = 无效", example = "1") private Boolean status; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/userbill/vo/UserBillRespVO.java ================================================ package co.yixiang.yshop.module.member.controller.admin.userbill.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; @Schema(description = "管理后台 - 用户账单 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class UserBillRespVO extends UserBillBaseVO { @Schema(description = "用户账单id", required = true, example = "22559") private Long id; @Schema(description = "添加时间", required = true) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/admin/userbill/vo/UserBillUpdateReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.admin.userbill.vo; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.*; @Schema(description = "管理后台 - 用户账单更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class UserBillUpdateReqVO extends UserBillBaseVO { @Schema(description = "用户账单id", required = true, example = "22559") @NotNull(message = "用户账单id不能为空") private Long id; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/address/AppUserAddressController.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co * 注意: * 本软件为www.yixiang.co开发研制,未经购买不得使用 * 购买后可获得全部源代码(禁止转卖、分享、上传到码云、github等开源平台) * 一经发现盗用、分享等行为,将追究法律责任,后果自负 */ package co.yixiang.yshop.module.member.controller.app.address; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.util.yshop.LocationUtils; import co.yixiang.yshop.framework.ip.core.Area; import co.yixiang.yshop.framework.ip.core.utils.AreaUtils; import co.yixiang.yshop.framework.security.core.annotations.PreAuthenticated; import co.yixiang.yshop.module.member.controller.app.address.param.AppAddressParam; import co.yixiang.yshop.module.member.controller.app.address.vo.AppUserAddressLocationVo; import co.yixiang.yshop.module.member.controller.app.address.vo.AppUserAddressQueryVo; import co.yixiang.yshop.module.member.controller.app.address.vo.AreaNodeRespVO; import co.yixiang.yshop.module.member.convert.useraddress.AreaConvert; import co.yixiang.yshop.module.member.service.useraddress.AppUserAddressService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; import static co.yixiang.yshop.module.member.enums.ErrorCodeConstants.USER_ADDRESS_PARAM_NOT_EXISTS; /** *

* 用户地前端控制器 *

* * @author hupeng * @since 2023-6-28 */ @Slf4j @RestController @RequiredArgsConstructor(onConstructor = @__(@Autowired)) @Tag(name = "用户 APP - 用户地址") @RequestMapping("/address") public class AppUserAddressController { private final AppUserAddressService appUserAddressService; @GetMapping("/city_list") @Operation(summary = "城市列表") public CommonResult> getTest() { Area area = AreaUtils.getArea(Area.ID_CHINA); Assert.notNull(area, "获取不到中国"); return success(AreaConvert.INSTANCE.convertList(area.getChildren())); } /** * 添加或修改地址 */ @PreAuthenticated @PostMapping("/addAndEdit") @Operation(summary = "添加或修改地址") public CommonResult addYxUserAddress(@RequestBody @Valid AppAddressParam param){ Long uid = getLoginUserId(); Long id = appUserAddressService.addAndEdit(uid,param); return success(id); } /** * 设置默认地址 */ @PreAuthenticated @PostMapping("/default/set/{id}") @Parameter(name = "id", description = "地址id", required = true) @Operation(summary = "设置默认地址") public CommonResult setDefault(@PathVariable String id){ Long uid = getLoginUserId(); appUserAddressService.setDefault(uid,id); return success(true); } /** * 删除用户地址 */ @PreAuthenticated @PostMapping("/del/{id}") @Operation(summary = "删除用户地址") public CommonResult deleteYxUserAddress(@PathVariable String id){ if(StrUtil.isBlank(id) || !NumberUtil.isNumber(id)){ throw exception(USER_ADDRESS_PARAM_NOT_EXISTS); } appUserAddressService.removeById(id); return success(true); } /** * 用户地址列表 */ @PreAuthenticated @GetMapping("/list") @Operation(summary = "用户地址列表") public CommonResult> getYxUserAddressPageList(@RequestParam(value = "page",defaultValue = "1") int page, @RequestParam(value = "limit",defaultValue = "10") int limit){ Long uid = getLoginUserId(); return success(appUserAddressService.getList(uid,page,limit)); } /** * 用户地址列表 */ @PostMapping("/getDistanceFromLocation") @Operation(summary = "用户地址列表") public CommonResult getDistanceFromLocation( @RequestBody AppUserAddressLocationVo addressLocation){ return success(LocationUtils.getDistance(addressLocation.getLat(),addressLocation.getLng(), addressLocation.getLat2(),addressLocation.getLng2())); } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/address/param/AddressDetailParam.java ================================================ package co.yixiang.yshop.module.member.controller.app.address.param; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; /** * @ClassName AddressDetailParam * @Author hupeng <610796224@qq.com> * @Date 2023/6/28 **/ @Data public class AddressDetailParam implements Serializable { @Schema(description = "城市ID", required = true) private Integer cityId; @Schema(description = "城市", required = true) private String city; @Schema(description = "地区", required = true) private String district; @Schema(description = "省份", required = true) private String province; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/address/param/AppAddressParam.java ================================================ package co.yixiang.yshop.module.member.controller.app.address.param; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.Data; import java.io.Serializable; /** * @ClassName AddressParam * @Author hupeng <610796224@qq.com> * @Date 2023/6/28 **/ @Data @JsonIgnoreProperties(ignoreUnknown = true) public class AppAddressParam implements Serializable { @Schema(description = "地址ID", required = true) private String id; @NotBlank @Size(min = 1, max = 30,message = "长度超过了限制") @Schema(description = "收货地址真实名字", required = true) private String realName; @Schema(description = "收货地址邮编", required = true) private String postCode; @Schema(description = "是否默认收货地址 1是 0否", required = true) private Integer isDefault; @NotBlank @Size(min = 1, max = 60,message = "长度超过了限制") @Schema(description = "收货详细地址", required = true) private String detail; @NotBlank @Schema(description = "收货手机号码", required = true) private String phone; @Schema(description = "收货地址详情", required = true) private String address; @Schema(description = "经度", required = true) private String longitude; @Schema(description = "纬度", required = true) private String latitude; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/address/vo/AppUserAddressLocationVo.java ================================================ package co.yixiang.yshop.module.member.controller.app.address.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; /** *

* 用户地址表 查询结果对象 *

* * @author hupeng * @date 2023-6-28 */ @Data @Schema(description = "用户地址表查询参数") public class AppUserAddressLocationVo implements Serializable { private static final long serialVersionUID = 1L; private double lat; private double lng; private double lat2; private double lng2; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/address/vo/AppUserAddressQueryVo.java ================================================ package co.yixiang.yshop.module.member.controller.app.address.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; /** *

* 用户地址表 查询结果对象 *

* * @author hupeng * @date 2023-6-28 */ @Data @Schema(description = "用户地址表查询参数") public class AppUserAddressQueryVo implements Serializable { private static final long serialVersionUID = 1L; @Schema(description = "用户地址id", required = true, example = "24169") private Long id; @Schema(description = "用户id", required = true, example = "24169") private Long uid; @Schema(description = "收货人姓名", required = true, example = "24169") private String realName; @Schema(description = "收货人电话", required = true, example = "24169") private String phone; @Schema(description = "收货人所在省", required = true, example = "24169") private String address; @Schema(description = "收货人详细地址", required = true, example = "24169") private String detail; @Schema(description = "经度", required = true, example = "24169") private String longitude; @Schema(description = "纬度", required = true, example = "24169") private String latitude; @Schema(description = "是否默认", required = true, example = "24169") private Integer isDefault; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/address/vo/AreaNodeRespVO.java ================================================ package co.yixiang.yshop.module.member.controller.app.address.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.List; @Schema(description = "管理后台 - 地区节点 Response VO") @Data public class AreaNodeRespVO { @Schema(description = "编号", required = true, example = "110000") private Integer id; @Schema(description = "名字", required = true, example = "北京") private String name; /** * 子节点 */ private List children; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/auth/AppAuthController.java ================================================ package co.yixiang.yshop.module.member.controller.app.auth; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.security.config.SecurityProperties; import co.yixiang.yshop.framework.security.core.annotations.PreAuthenticated; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import co.yixiang.yshop.module.member.controller.app.auth.vo.*; import co.yixiang.yshop.module.member.service.auth.MemberAuthService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.annotation.security.PermitAll; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @Tag(name = "用户 APP - 认证") @RestController @RequestMapping("/member/auth") @Validated @Slf4j public class AppAuthController { @Resource private MemberAuthService authService; @Resource private SecurityProperties securityProperties; @Value("${yshop.info.isActive}") private Boolean isActive; @PostMapping("/login") @Operation(summary = "使用手机 + 密码登录") public CommonResult login(@RequestBody @Valid AppAuthLoginReqVO reqVO) { return success(authService.login(reqVO)); } @PostMapping("/logout") @PermitAll @Operation(summary = "登出系统") public CommonResult logout(HttpServletRequest request) { String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader(),securityProperties.getTokenParameter()); if (StrUtil.isNotBlank(token)) { authService.logout(token); } return success(true); } @PostMapping("/refresh-token") @Operation(summary = "刷新令牌") @Parameter(name = "refreshToken", description = "刷新令牌", required = true) public CommonResult refreshToken(@RequestParam("refreshToken") String refreshToken) { return success(authService.refreshToken(refreshToken)); } // ========== 短信登录相关 ========== @PostMapping("/sms-login") @Operation(summary = "使用手机 + 验证码登录") public CommonResult smsLogin(@RequestBody @Valid AppAuthSmsLoginReqVO reqVO) { return success(authService.smsLogin(reqVO)); } @PostMapping("/send-sms-code") @Operation(summary = "发送手机验证码") public CommonResult sendSmsCode(@RequestBody @Valid AppAuthSmsSendReqVO reqVO) { authService.sendSmsCode(getLoginUserId(), reqVO); return success(true); } // @PostMapping("/reset-password") // @Operation(summary = "重置密码", description = "用户忘记密码时使用") // @PreAuthenticated // public CommonResult resetPassword(@RequestBody @Valid AppAuthResetPasswordReqVO reqVO) { // authService.resetPassword(reqVO); // return success(true); // } @PostMapping("/update-password") @Operation(summary = "修改用户密码", description = "用户修改密码时使用") @PreAuthenticated public CommonResult updatePassword(@RequestBody @Valid AppAuthUpdatePasswordReqVO reqVO) { authService.updatePassword(getLoginUserId(), reqVO); return success(true); } // ========== 社交登录相关 ========== // @GetMapping("/social-auth-redirect") // @Operation(summary = "社交授权的跳转") // @Parameters({ // @Parameter(name = "type", description = "社交类型", required = true), // @Parameter(name = "redirectUri", description = "回调路径") // }) // public CommonResult socialAuthRedirect(@RequestParam("type") Integer type, // @RequestParam("redirectUri") String redirectUri) { // return CommonResult.success(authService.getSocialAuthorizeUrl(type, redirectUri)); // } // // @PostMapping("/social-login") // @Operation(summary = "社交快捷登录,使用 code 授权码", description = "适合未登录的用户,但是社交账号已绑定用户") // public CommonResult socialLogin(@RequestBody @Valid AppAuthSocialLoginReqVO reqVO) { // return success(authService.socialLogin(reqVO)); // } @PostMapping("/weixin-mini-app-login") @Operation(summary = "微信小程序的一键登录") public CommonResult weixinMiniAppLogin(@RequestBody @Valid AppAuthWeixinMiniAppLoginReqVO reqVO) { return success(authService.weixinMiniAppLogin(reqVO)); } @PostMapping("/auth-session") @Operation(summary = "微信小程序登录") public CommonResult weixinMiniAppLogin2(@RequestBody @Valid AppWeixinMiniLoginVO loginVO) { AppAuthLoginRespVO appAuthLoginRespVO = authService.weixinMiniAppLogin2(loginVO); appAuthLoginRespVO.setIsActive(isActive); return success(appAuthLoginRespVO); } @PostMapping("/auth-miniapp-login") @Operation(summary = "微信小程序登录") public CommonResult weixinMiniAppLogin3(@RequestBody @Valid AppWxMiniLoginVO appWxMiniLoginVO) { return success(authService.weixinMiniAppLogin3(appWxMiniLoginVO.getEncryptedData(),appWxMiniLoginVO.getIv() ,appWxMiniLoginVO.getOpenid())); } @GetMapping("/auth-wechat-login") @Operation(summary = "社交授权的跳转") @Parameters({ @Parameter(name = "code", description = "code码", required = true) }) public CommonResult wechatAuth(@RequestParam("code") String code) { System.out.println("code:"+code); return CommonResult.success(authService.wechatAuth(code)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/auth/vo/AppAuthCheckCodeReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.app.auth.vo; import co.yixiang.yshop.framework.common.validation.InEnum; import co.yixiang.yshop.framework.common.validation.Mobile; import co.yixiang.yshop.module.system.enums.sms.SmsSceneEnum; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.Length; // TODO yshop:code review 相关逻辑 @Schema(description = "用户 APP - 校验验证码 Request VO") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AppAuthCheckCodeReqVO { @Schema(description = "手机号", example = "15601691234") @NotBlank(message = "手机号不能为空") @Mobile private String mobile; @Schema(description = "手机验证码", required = true, example = "1024") @NotBlank(message = "手机验证码不能为空") @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位") @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") private String code; @Schema(description = "发送场景,对应 SmsSceneEnum 枚举", example = "1") @NotNull(message = "发送场景不能为空") @InEnum(SmsSceneEnum.class) private Integer scene; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/auth/vo/AppAuthLoginReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.app.auth.vo; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.validation.InEnum; import co.yixiang.yshop.framework.common.validation.Mobile; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.Length; @Schema(description = "用户 APP - 手机 + 密码登录 Request VO,如果登录并绑定社交用户,需要传递 social 开头的参数") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AppAuthLoginReqVO { @Schema(description = "手机号", required = true, example = "15601691300") @NotEmpty(message = "手机号不能为空") @Mobile private String mobile; @Schema(description = "密码", required = true, example = "buzhidao") @NotEmpty(message = "密码不能为空") @Length(min = 4, max = 16, message = "密码长度为 4-16 位") private String password; // ========== 绑定社交登录时,需要传递如下参数 ========== @Schema(description = "社交平台的类型,参见 SysUserSocialTypeEnum 枚举值", required = true, example = "10") @InEnum(SocialTypeEnum.class) private Integer socialType; @Schema(description = "授权码", required = true, example = "1024") private String socialCode; @Schema(description = "state", required = true, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") private String socialState; @AssertTrue(message = "授权码不能为空") public boolean isSocialCodeValid() { return socialType == null || StrUtil.isNotEmpty(socialCode); } @AssertTrue(message = "授权 state 不能为空") public boolean isSocialState() { return socialType == null || StrUtil.isNotEmpty(socialState); } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/auth/vo/AppAuthLoginRespVO.java ================================================ package co.yixiang.yshop.module.member.controller.app.auth.vo; import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserQueryVo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.time.LocalDateTime; @Schema(description = "用户 APP - 登录 Response VO") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AppAuthLoginRespVO { @Schema(description = "用户编号", required = true, example = "1024") private Long userId; @Schema(description = "访问令牌", required = true, example = "happy") private String accessToken; @Schema(description = "刷新令牌", required = true, example = "nice") private String refreshToken; @Schema(description = "过期时间", required = true) private LocalDateTime expiresTime; private String openId; private AppUserQueryVo userInfo; private Boolean isActive; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/auth/vo/AppAuthResetPasswordReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.app.auth.vo; import co.yixiang.yshop.framework.common.validation.Mobile; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.Length; // TODO yshop:code review 相关逻辑 @Schema(description = "用户 APP - 重置密码 Request VO") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AppAuthResetPasswordReqVO { @Schema(description = "新密码", required = true, example = "buzhidao") @NotEmpty(message = "新密码不能为空") @Length(min = 4, max = 16, message = "密码长度为 4-16 位") private String password; @Schema(description = "手机验证码", required = true, example = "1024") @NotEmpty(message = "手机验证码不能为空") @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位") @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") private String code; @Schema(description = "手机号",required = true,example = "15878962356") @NotBlank(message = "手机号不能为空") @Mobile private String mobile; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/auth/vo/AppAuthSmsLoginReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.app.auth.vo; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.validation.InEnum; import co.yixiang.yshop.framework.common.validation.Mobile; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.Length; @Schema(description = "用户 APP - 手机 + 验证码登录 Request VO,如果登录并绑定社交用户,需要传递 social 开头的参数") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AppAuthSmsLoginReqVO { @Schema(description = "手机号", required = true, example = "15601691300") @NotEmpty(message = "手机号不能为空") @Mobile private String mobile; @Schema(description = "手机验证码", required = true, example = "1024") @NotEmpty(message = "手机验证码不能为空") @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位") @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") private String code; @Schema(description = "设备来源", required = true, example = "h5") private String from; private String openid; // ========== 绑定社交登录时,需要传递如下参数 ========== @Schema(description = "社交平台的类型,参见 SysUserSocialTypeEnum 枚举值", required = true, example = "10") @InEnum(SocialTypeEnum.class) private Integer socialType; @Schema(description = "授权码", required = true, example = "1024") private String socialCode; @Schema(description = "state", required = true, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") private String socialState; @AssertTrue(message = "授权码不能为空") public boolean isSocialCodeValid() { return socialType == null || StrUtil.isNotEmpty(socialCode); } @AssertTrue(message = "授权 state 不能为空") public boolean isSocialState() { return socialType == null || StrUtil.isNotEmpty(socialState); } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/auth/vo/AppAuthSmsSendReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.app.auth.vo; import co.yixiang.yshop.framework.common.validation.InEnum; import co.yixiang.yshop.framework.common.validation.Mobile; import co.yixiang.yshop.module.system.enums.sms.SmsSceneEnum; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.experimental.Accessors; @Schema(description = "用户 APP - 发送手机验证码 Request VO") @Data @Accessors(chain = true) public class AppAuthSmsSendReqVO { @Schema(description = "手机号", example = "15601691234") @Mobile private String mobile; @Schema(description = "发送场景,对应 SmsSceneEnum 枚举", example = "1") @NotNull(message = "发送场景不能为空") @InEnum(SmsSceneEnum.class) private Integer scene; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/auth/vo/AppAuthSocialLoginReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.app.auth.vo; import co.yixiang.yshop.framework.common.validation.InEnum; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Schema(description = "用户 APP - 社交快捷登录 Request VO,使用 code 授权码") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AppAuthSocialLoginReqVO { @Schema(description = "社交平台的类型,参见 SysUserSocialTypeEnum 枚举值", required = true, example = "10") @InEnum(SocialTypeEnum.class) @NotNull(message = "社交平台的类型不能为空") private Integer type; @Schema(description = "授权码", required = true, example = "1024") @NotEmpty(message = "授权码不能为空") private String code; @Schema(description = "state", required = true, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") @NotEmpty(message = "state 不能为空") private String state; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/auth/vo/AppAuthUpdatePasswordReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.app.auth.vo; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.Length; // TODO yshop:code review 相关逻辑 @Schema(description = "用户 APP - 修改密码 Request VO") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AppAuthUpdatePasswordReqVO { @Schema(description = "用户旧密码", required = true, example = "123456") @NotBlank(message = "旧密码不能为空") @Length(min = 4, max = 16, message = "密码长度为 4-16 位") private String oldPassword; @Schema(description = "新密码", required = true, example = "buzhidao") @NotEmpty(message = "新密码不能为空") @Length(min = 4, max = 16, message = "密码长度为 4-16 位") private String password; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/auth/vo/AppAuthWeixinMiniAppLoginReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.app.auth.vo; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Schema(description = "用户 APP - 微信小程序手机登录 Request VO") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AppAuthWeixinMiniAppLoginReqVO { @Schema(description = "手机 code,小程序通过 wx.getPhoneNumber 方法获得", required = true, example = "hello") @NotEmpty(message = "手机 code 不能为空") private String phoneCode; @Schema(description = "登录 code,小程序通过 wx.login 方法获得", required = true, example = "word") @NotEmpty(message = "登录 code 不能为空") private String loginCode; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/auth/vo/AppWeixinMiniLoginVO.java ================================================ package co.yixiang.yshop.module.member.controller.app.auth.vo; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Schema(description = "用户 APP - 微信小程序手机登录 VO") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AppWeixinMiniLoginVO { @Schema(description = "登录 code,小程序通过 wx.login 方法获得", required = true, example = "word") @NotEmpty(message = "登录 code 不能为空") private String code; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/auth/vo/AppWxMiniLoginVO.java ================================================ package co.yixiang.yshop.module.member.controller.app.auth.vo; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Schema(description = "用户 APP - 微信小程序手机登录 VO") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AppWxMiniLoginVO { @Schema(description = "登录encryptedData", required = true, example = "word") @NotEmpty(message = "登录encryptedData不能为空") private String encryptedData; @Schema(description = "登录iv", required = true, example = "word") @NotEmpty(message = "登录iv不能为空") private String iv; @Schema(description = "登录openid", required = true, example = "word") @NotEmpty(message = "登录openid不能为空") private String openid; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/social/AppSocialUserController.java ================================================ package co.yixiang.yshop.module.member.controller.app.social; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.module.member.controller.app.social.vo.AppSocialUserBindReqVO; import co.yixiang.yshop.module.member.controller.app.social.vo.AppSocialUserUnbindReqVO; import co.yixiang.yshop.module.member.convert.social.SocialUserConvert; import co.yixiang.yshop.module.system.api.social.SocialUserApi; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Operation; import jakarta.annotation.Resource; import jakarta.validation.Valid; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @Tag(name = "用户 App - 社交用户") @RestController @RequestMapping("/system/social-user") @Validated public class AppSocialUserController { @Resource private SocialUserApi socialUserApi; @PostMapping("/bind") @Operation(summary = "社交绑定,使用 code 授权码") public CommonResult socialBind(@RequestBody @Valid AppSocialUserBindReqVO reqVO) { socialUserApi.bindSocialUser(SocialUserConvert.INSTANCE.convert(getLoginUserId(), UserTypeEnum.MEMBER.getValue(), reqVO)); return CommonResult.success(true); } @DeleteMapping("/unbind") @Operation(summary = "取消社交绑定") public CommonResult socialUnbind(@RequestBody AppSocialUserUnbindReqVO reqVO) { socialUserApi.unbindSocialUser(SocialUserConvert.INSTANCE.convert(getLoginUserId(), UserTypeEnum.MEMBER.getValue(), reqVO)); return CommonResult.success(true); } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/social/vo/AppSocialUserBindReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.app.social.vo; import co.yixiang.yshop.framework.common.validation.InEnum; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Schema(description = "用户 APP - 社交绑定 Request VO,使用 code 授权码") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AppSocialUserBindReqVO { @Schema(description = "社交平台的类型,参见 SysUserSocialTypeEnum 枚举值", required = true, example = "10") @InEnum(SocialTypeEnum.class) @NotNull(message = "社交平台的类型不能为空") private Integer type; @Schema(description = "授权码", required = true, example = "1024") @NotEmpty(message = "授权码不能为空") private String code; @Schema(description = "state", required = true, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") @NotEmpty(message = "state 不能为空") private String state; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/social/vo/AppSocialUserUnbindReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.app.social.vo; import co.yixiang.yshop.framework.common.validation.InEnum; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Schema(description = "用户 APP - 取消社交绑定 Request VO") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AppSocialUserUnbindReqVO { @Schema(description = "社交平台的类型,参见 SysUserSocialTypeEnum 枚举值", required = true, example = "10") @InEnum(SocialTypeEnum.class) @NotNull(message = "社交平台的类型不能为空") private Integer type; @Schema(description = "社交用户的 openid", required = true, example = "IPRmJ0wvBptiPIlGEZiPewGwiEiE") @NotEmpty(message = "社交用户的 openid 不能为空") private String openid; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/user/AppUserController.java ================================================ package co.yixiang.yshop.module.member.controller.app.user; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.security.core.annotations.PreAuthenticated; import co.yixiang.yshop.module.member.controller.app.user.vo.*; import co.yixiang.yshop.module.member.convert.user.UserConvert; import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO; import co.yixiang.yshop.module.member.service.user.MemberUserService; import co.yixiang.yshop.module.member.service.userbill.UserBillService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; import static co.yixiang.yshop.module.infra.enums.ErrorCodeConstants.FILE_IS_EMPTY; @Tag(name = "用户 APP - 用户个人中心") @RestController @RequestMapping("/member/user") @Validated @Slf4j public class AppUserController { @Resource private MemberUserService userService; @Resource private UserBillService userBillService; @PostMapping("/update-nickname") @Operation(summary = "修改用户昵称或者生日") @Parameters({ @Parameter(name = "nickname", description = "用户昵称", required = true, example = "wang"), @Parameter(name = "birthday", description = "生日", required = true, example = "2023-11-12"), @Parameter(name = "gender", description = "性别", required = true, example = "o"), @Parameter(name = "avatar", description = "用户头像", required = true, example = ""), @Parameter(name = "mobile", description = "手机", required = true, example = "") }) @PreAuthenticated public CommonResult updateUserNickname(@RequestBody @Valid AppUserNickVO appUserNickVO) { userService.updateUserNickname(getLoginUserId(), appUserNickVO.getNickname(),appUserNickVO.getBirthday(), appUserNickVO.getGender(),appUserNickVO.getAvatar(),appUserNickVO.getMobile()); return success(true); } @PostMapping("/update-avatar") @Operation(summary = "修改用户头像") @PreAuthenticated public CommonResult updateUserAvatar(@RequestParam("avatarFile") MultipartFile file) throws Exception { if (file.isEmpty()) { throw exception(FILE_IS_EMPTY); } String avatar = userService.updateUserAvatar(getLoginUserId(), file.getInputStream()); return success(avatar); } @GetMapping("/get") @Operation(summary = "获得基本信息") @PreAuthenticated public CommonResult getUserInfo() { MemberUserDO user = userService.getUser(getLoginUserId()); return success(UserConvert.INSTANCE.convert(user)); } @PostMapping("/update-mobile") @Operation(summary = "修改用户手机") @PreAuthenticated public CommonResult updateMobile(@RequestBody @Valid AppUserUpdateMobileReqVO reqVO) { userService.updateUserMobile(getLoginUserId(), reqVO); return success(true); } @GetMapping("/get-info") @Operation(summary = "获得基本信息") @PreAuthenticated public CommonResult getUserBaseInfo() { return success(userService.getAppUser(getLoginUserId())); } @GetMapping("/getBill") @Operation(summary = "获取用户账单") @PreAuthenticated @Parameters({ @Parameter(name = "cate", description = "状态,0余额 1-积分", required = true, example = "1"), @Parameter(name = "type", description = "状态,0全部 1消费 2充值 3退款", required = true, example = "1"), @Parameter(name = "page", description = "页码,默认为1", required = true, example = "1"), @Parameter(name = "pagesize", description = "页大小,默认为10", required = true, example = "10 ") }) public CommonResult> getUserBill(@RequestParam(value = "cate", defaultValue = "0") int cate, @RequestParam(value = "type", defaultValue = "0") int type, @RequestParam(value = "page", defaultValue = "1") int page, @RequestParam(value = "pagesize", defaultValue = "10") int pagesize) { return success(userBillService.getBillList(getLoginUserId(),cate,type,page,pagesize)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/user/vo/AppUserBillVO.java ================================================ package co.yixiang.yshop.module.member.controller.app.user.vo; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import lombok.*; import java.math.BigDecimal; import java.time.LocalDateTime; /** * 用户账单 vo * * @author yshop */ @Data @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class AppUserBillVO { /** * 用户账单id */ private Long id; /** * 用户uid */ private Long uid; /** * 关联id */ private String linkId; /** * 0 = 支出 1 = 获得 */ private Integer pm; /** * 账单标题 */ private String title; /** * 明细种类 */ private String category; /** * 明细类型 */ private String type; /** * 明细数字 */ private BigDecimal number; /** * 剩余 */ private BigDecimal balance; /** * 备注 */ private String mark; /** * 0 = 带确定 1 = 有效 -1 = 无效 */ private Integer status; private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/user/vo/AppUserInfoRespVO.java ================================================ package co.yixiang.yshop.module.member.controller.app.user.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Schema(description = "用户 APP - 用户个人信息 Response VO") @Data @NoArgsConstructor @AllArgsConstructor public class AppUserInfoRespVO { @Schema(description = "用户id", required = true, example = "yshop") private Long id; @Schema(description = "用户昵称", required = true, example = "yshop") private String nickname; @Schema(description = "用户头像", required = true, example = "/infra/file/get/35a12e57-4297-4faa-bf7d-7ed2f211c952") private String avatar; @Schema(description = "用户手机号", required = true, example = "15601691300") private String mobile; @Schema(description = "生日", required = true, example = "2023-10-11") private String birthday; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/user/vo/AppUserNickVO.java ================================================ package co.yixiang.yshop.module.member.controller.app.user.vo; import jakarta.validation.constraints.NotBlank; import lombok.*; import java.math.BigDecimal; import java.time.LocalDateTime; /** * 用户信息 vo * * @author yshop */ @Data @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class AppUserNickVO { /** * 用户nickname */ @NotBlank(message = "用户昵称不能为空") private String nickname; /** * 用户birthday */ @NotBlank(message = "生日不能为空") private String birthday; /** * avatar */ @NotBlank(message = "用户头像不能为空") private String avatar; /** * gender */ private Integer gender; /** * mobile */ private String mobile; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/user/vo/AppUserOrderCountVo.java ================================================ package co.yixiang.yshop.module.member.controller.app.user.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.io.Serializable; /** * @ClassName OrderCountDTO * @Author hupeng <610796224@qq.com> * @Date 2023/6/18 **/ @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder @Schema(description = "用户 APP - 用户 OrderCountDTO") public class AppUserOrderCountVo implements Serializable { /**订单支付没有退款 数量*/ @Schema(description = "订单支付没有退款数量", required = true) private Long orderCount; /**订单支付没有退款 支付总金额*/ @Schema(description = "订单支付没有退款支付总金额", required = true) private Double sumPrice; /**订单待支付 数量*/ @Schema(description = "订单待支付数量", required = true) private Long unpaidCount; /**订单待发货数量*/ @Schema(description = "订单待发货数量", required = true) private Long unshippedCount; /**订单待收货数量*/ @Schema(description = "订单待收货数量", required = true) private Long receivedCount; /**订单待评价数量*/ @Schema(description = "订单待评价数量", required = true) private Long evaluatedCount; /**订单已完成数量*/ @Schema(description = "订单已完成数量", required = true) private Long completeCount; /**订单退款数量*/ @Schema(description = "订单退款数量", required = true) private Long refundCount; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/user/vo/AppUserQueryVo.java ================================================ package co.yixiang.yshop.module.member.controller.app.user.vo; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.springframework.format.annotation.DateTimeFormat; import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; /** *

* 用户表 查询结果对象 *

* * @author hupeng * @date 2023-6-18 */ @Data @Schema(description = "用户 APP - 用户表查 Response VO") public class AppUserQueryVo implements Serializable { private static final long serialVersionUID = 1L; @Schema(description = "用户id", required = true) private Long id; @Schema(description = "用户账户(跟accout一样)\"", required = true) private String username; @Schema(description = "用户账号", required = true) private String account; @Schema(description = "优惠券数量", required = true) private Long couponCount = 0L; @Schema(description = "订单详情数据", required = true) private AppUserOrderCountVo orderStatusNum; private Integer statu; @Schema(description = "总的签到天数", required = true) private Long sumSignDay; @Schema(description = "当天是否签到", required = true) private Integer isDaySign; @Schema(description = "昨天是否签到", required = true) private Integer isYesterDaySign; @Schema(description = "核销权限", required = true) private Integer checkStatus; @Schema(description = "用户昵称", required = true) private String nickname; @Schema(description = "用户头像", required = true) private String avatar; @Schema(description = "手机号码", required = true) private String mobile; @Schema(description = "用户余额", required = true) private BigDecimal nowMoney; @Schema(description = "用户花费的金额", required = true) private BigDecimal sumMoney; @Schema(description = "佣金金额", required = true) private BigDecimal brokeragePrice; @Schema(description = "用户剩余积分", required = true) private BigDecimal integral; @Schema(description = "连续签到天数", required = true) private Integer signNum; @Schema(description = "推广元id", required = true) private Long spreadUid; @Schema(description = "用户类型", required = true) private String userType; @Schema(description = "是否为推广员", required = true) private Integer isPromoter; @Schema(description = "用户购买次数", required = true) private Integer payCount; @Schema(description = "下级人数", required = true) private Integer spreadCount; @Schema(description = "详细地址", required = true) private String addres; private Integer gender; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime; private String birthday; @Schema(description = "用户登陆类型,h5,wechat,routine", required = true) private String loginType; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/user/vo/AppUserRechargeVO.java ================================================ package co.yixiang.yshop.module.member.controller.app.user.vo; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.*; /** * 用户充值 vo * * @author yshop */ @Data public class AppUserRechargeVO { @Schema(description = "充值ID", required = true) @NotBlank(message = "请选择要充值的面值") private String rechargeId; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/user/vo/AppUserUpdateMobileReqVO.java ================================================ package co.yixiang.yshop.module.member.controller.app.user.vo; import co.yixiang.yshop.framework.common.validation.Mobile; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.Length; @Schema(description = "用户 APP - 修改手机 Request VO") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AppUserUpdateMobileReqVO { @Schema(description = "手机验证码", required = true, example = "1024") @NotEmpty(message = "手机验证码不能为空") @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位") @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") private String code; @Schema(description = "手机号",required = true,example = "15823654487") @NotBlank(message = "手机号不能为空") @Length(min = 8, max = 11, message = "手机号码长度为 8-11 位") @Mobile private String mobile; @Schema(description = "原手机验证码", required = true, example = "1024") @NotEmpty(message = "原手机验证码不能为空") @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位") @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") private String oldCode; // TODO @yshop:oldMobile 应该不用传递 @Schema(description = "原手机号",required = true,example = "15823654487") @NotBlank(message = "手机号不能为空") @Length(min = 8, max = 11, message = "手机号码长度为 8-11 位") @Mobile private String oldMobile; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/controller/app/weixin/AppWxMpController.java ================================================ package co.yixiang.yshop.module.member.controller.app.weixin; import co.yixiang.yshop.framework.common.pojo.CommonResult; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Operation; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.bean.WxJsapiSignature; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.mp.api.WxMpService; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "微信公众号") @RestController @RequestMapping("/member/wx-mp") @Validated @Slf4j public class AppWxMpController { @Resource private WxMpService mpService; @GetMapping("/create-jsapi-signature") @Operation(summary = "创建微信 JS SDK 初始化所需的签名", description = "参考 https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html 文档") public CommonResult createJsapiSignature(@RequestParam("url") String url) throws WxErrorException { return success(mpService.createJsapiSignature(url)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/convert/auth/AuthConvert.java ================================================ package co.yixiang.yshop.module.member.convert.auth; import co.yixiang.yshop.module.member.controller.app.auth.vo.*; import co.yixiang.yshop.module.member.controller.app.social.vo.AppSocialUserUnbindReqVO; import co.yixiang.yshop.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO; import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeSendReqDTO; import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeUseReqDTO; import co.yixiang.yshop.module.system.api.social.dto.SocialUserBindReqDTO; import co.yixiang.yshop.module.system.api.social.dto.SocialUserUnbindReqDTO; import co.yixiang.yshop.module.system.enums.sms.SmsSceneEnum; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; @Mapper public interface AuthConvert { AuthConvert INSTANCE = Mappers.getMapper(AuthConvert.class); SocialUserBindReqDTO convert(Long userId, Integer userType, AppAuthSocialLoginReqVO reqVO); SocialUserUnbindReqDTO convert(Long userId, Integer userType, AppSocialUserUnbindReqVO reqVO); SmsCodeSendReqDTO convert(AppAuthSmsSendReqVO reqVO); SmsCodeUseReqDTO convert(AppAuthResetPasswordReqVO reqVO, SmsSceneEnum scene, String usedIp); SmsCodeUseReqDTO convert(AppAuthSmsLoginReqVO reqVO, Integer scene, String usedIp); AppAuthLoginRespVO convert(OAuth2AccessTokenRespDTO bean); } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/convert/package-info.java ================================================ /** * 提供 POJO 类的实体转换 * * 目前使用 MapStruct 框架 */ package co.yixiang.yshop.module.member.convert; ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/convert/social/SocialUserConvert.java ================================================ package co.yixiang.yshop.module.member.convert.social; import co.yixiang.yshop.module.member.controller.app.social.vo.AppSocialUserBindReqVO; import co.yixiang.yshop.module.member.controller.app.social.vo.AppSocialUserUnbindReqVO; import co.yixiang.yshop.module.system.api.social.dto.SocialUserBindReqDTO; import co.yixiang.yshop.module.system.api.social.dto.SocialUserUnbindReqDTO; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; @Mapper public interface SocialUserConvert { SocialUserConvert INSTANCE = Mappers.getMapper(SocialUserConvert.class); SocialUserBindReqDTO convert(Long userId, Integer userType, AppSocialUserBindReqVO reqVO); SocialUserUnbindReqDTO convert(Long userId, Integer userType, AppSocialUserUnbindReqVO reqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/convert/user/UserConvert.java ================================================ package co.yixiang.yshop.module.member.convert.user; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.member.api.user.dto.MemberUserRespDTO; import co.yixiang.yshop.module.member.controller.admin.user.vo.UserCreateReqVO; import co.yixiang.yshop.module.member.controller.admin.user.vo.UserRespVO; import co.yixiang.yshop.module.member.controller.admin.user.vo.UserUpdateReqVO; import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserInfoRespVO; import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserQueryVo; import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import java.util.List; @Mapper public interface UserConvert { UserConvert INSTANCE = Mappers.getMapper(UserConvert.class); AppUserInfoRespVO convert(MemberUserDO bean); MemberUserRespDTO convert2(MemberUserDO bean); AppUserQueryVo convert3(MemberUserDO bean); UserRespVO convert4(MemberUserDO bean); List convertList2(List list); MemberUserDO convert(UserCreateReqVO bean); MemberUserDO convert(UserUpdateReqVO bean); UserRespVO convert(MemberUserDO bean,Boolean bool); List convertList(List list); PageResult convertPage(PageResult page); } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/convert/useraddress/AreaConvert.java ================================================ package co.yixiang.yshop.module.member.convert.useraddress; import co.yixiang.yshop.framework.ip.core.Area; import co.yixiang.yshop.module.member.controller.app.address.vo.AreaNodeRespVO; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import java.util.List; @Mapper public interface AreaConvert { AreaConvert INSTANCE = Mappers.getMapper(AreaConvert.class); List convertList(List list); } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/convert/useraddress/UserAddressConvert.java ================================================ package co.yixiang.yshop.module.member.convert.useraddress; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.member.controller.app.address.vo.AppUserAddressQueryVo; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import co.yixiang.yshop.module.member.controller.admin.useraddress.vo.*; import co.yixiang.yshop.module.member.dal.dataobject.useraddress.UserAddressDO; /** * 用户地址 Convert * * @author yshop */ @Mapper public interface UserAddressConvert { UserAddressConvert INSTANCE = Mappers.getMapper(UserAddressConvert.class); UserAddressDO convert(UserAddressCreateReqVO bean); UserAddressDO convert(UserAddressUpdateReqVO bean); UserAddressRespVO convert(UserAddressDO bean); List convertList(List list); List convertList02(List list); PageResult convertPage(PageResult page); } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/convert/userbill/UserBillConvert.java ================================================ package co.yixiang.yshop.module.member.convert.userbill; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserBillVO; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import co.yixiang.yshop.module.member.controller.admin.userbill.vo.*; import co.yixiang.yshop.module.member.dal.dataobject.userbill.UserBillDO; /** * 用户账单 Convert * * @author yshop */ @Mapper public interface UserBillConvert { UserBillConvert INSTANCE = Mappers.getMapper(UserBillConvert.class); UserBillDO convert(UserBillCreateReqVO bean); UserBillDO convert(UserBillUpdateReqVO bean); UserBillRespVO convert(UserBillDO bean); List convertList(List list); List convertList02(List list); PageResult convertPage(PageResult page); } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/dal/dataobject/user/MemberUserDO.java ================================================ package co.yixiang.yshop.module.member.dal.dataobject.user; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.tenant.core.db.TenantBaseDO; import co.yixiang.yshop.module.member.api.user.dto.WechatUserDto; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler; import lombok.*; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import java.math.BigDecimal; import java.time.LocalDateTime; /** * 会员用户 DO * * uk_mobile 索引:基于 {@link #mobile} 字段 * * @author yshop */ @TableName(value = "yshop_user", autoResultMap = true) @KeySequence("member_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class MemberUserDO extends TenantBaseDO { /** * 用户ID */ @TableId private Long id; /** * 用户昵称 */ private String nickname; /** * 用户头像 */ private String avatar; /** * 帐号状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; /** * 手机 */ private String mobile; /** * 加密后的密码 * * 因为目前使用 {@link BCryptPasswordEncoder} 加密器,所以无需自己处理 salt 盐 */ private String password; /** * 注册 IP */ private String registerIp; /** * 最后登录IP */ private String loginIp; /** * 最后登录时间 */ private LocalDateTime loginDate; /** * 用户账户(跟accout一样) */ private String username; /** * 真实姓名 */ private String realName; /** * 生日 */ private String birthday; /** * 身份证号码 */ private String cardId; /** * 用户备注 */ private String mark; /** * 合伙人id */ private Integer partnerId; /** * 用户分组id */ private Integer groupId; /** * 添加ip */ private String addIp; /** * 最后一次登录ip */ private String lastIp; /** * 用户余额 */ private BigDecimal nowMoney; /** * 佣金金额 */ private BigDecimal brokeragePrice; /** * 用户剩余积分 */ private BigDecimal integral; /** * 连续签到天数 */ private Integer signNum; /** * 等级 */ private Integer level; /** * 推广元id */ private Long spreadUid; /** * 推广员关联时间 */ private LocalDateTime spreadTime; /** * 用户类型 */ private String userType; /** * 是否为推广员 */ private Integer isPromoter; /** * 用户购买次数 */ private Integer payCount; /** * 下级人数 */ private Integer spreadCount; /** * 详细地址 */ private String addres; /** * 管理员编号 */ private Integer adminid; /** * 用户登陆类型,h5,wechat,routine */ private String loginType; //公众号openid private String openid; //小程序openid private String routineOpenid; //性别 private Integer gender; /** 微信用户json信息 */ @TableField(typeHandler = FastjsonTypeHandler.class) @Deprecated private WechatUserDto wxProfile; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/dal/dataobject/useraddress/UserAddressDO.java ================================================ package co.yixiang.yshop.module.member.dal.dataobject.useraddress; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; /** * 用户地址 DO * * @author yshop */ @TableName("yshop_user_address") @KeySequence("yshop_user_address_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class UserAddressDO extends BaseDO { /** * 用户地址id */ @TableId private Long id; /** * 用户id */ private Long uid; /** * 收货人姓名 */ private String realName; /** * 收货人电话 */ private String phone; /** * 收货人所在省 */ private String province; /** * 收货人所在市 */ private String city; /** * 城市id */ private Integer cityId; /** * 收货人所在区 */ private String district; /** * 收货人详细地址 */ private String detail; /** * 邮编 */ private String postCode; /** * 经度 */ private String longitude; /** * 纬度 */ private String latitude; /** * 是否默认 */ private Integer isDefault; /** * 地址 */ private String address; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/dal/dataobject/userbill/UserBillDO.java ================================================ package co.yixiang.yshop.module.member.dal.dataobject.userbill; import lombok.*; import java.util.*; import java.math.BigDecimal; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; /** * 用户账单 DO * * @author yshop */ @TableName("yshop_user_bill") @KeySequence("yshop_user_bill_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class UserBillDO extends BaseDO { /** * 用户账单id */ @TableId private Long id; /** * 用户uid */ private Long uid; /** * 关联id */ private String linkId; /** * 0 = 支出 1 = 获得 */ private Integer pm; /** * 账单标题 */ private String title; /** * 明细种类 */ private String category; /** * 明细类型 */ private String type; /** * 明细数字 */ private BigDecimal number; /** * 剩余 */ private BigDecimal balance; /** * 备注 */ private String mark; /** * 0 = 带确定 1 = 有效 -1 = 无效 */ private Integer status; @TableField(exist = false) private BigDecimal sumAll; private String extendField; } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/dal/mysql/user/MemberUserMapper.java ================================================ package co.yixiang.yshop.module.member.dal.mysql.user; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.member.controller.admin.user.vo.UserExportReqVO; import co.yixiang.yshop.module.member.controller.admin.user.vo.UserPageReqVO; import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; import java.math.BigDecimal; import java.util.List; /** * 会员 User Mapper * * @author yshop */ @Mapper public interface MemberUserMapper extends BaseMapperX { default MemberUserDO selectByMobile(String mobile) { return selectOne(MemberUserDO::getMobile, mobile); } default List selectListByNicknameLike(String nickname) { return selectList(new LambdaQueryWrapperX() .likeIfPresent(MemberUserDO::getNickname, nickname)); } default PageResult selectPage(UserPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(MemberUserDO::getLoginType, reqVO.getLoginType()) .likeIfPresent(MemberUserDO::getUsername, reqVO.getUsername()) .likeIfPresent(MemberUserDO::getRealName, reqVO.getRealName()) .likeIfPresent(MemberUserDO::getNickname, reqVO.getNickname()) .likeIfPresent(MemberUserDO::getMobile, reqVO.getPhone()) .betweenIfPresent(MemberUserDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(MemberUserDO::getId)); } default List selectList(UserExportReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .likeIfPresent(MemberUserDO::getUsername, reqVO.getUsername()) .likeIfPresent(MemberUserDO::getRealName, reqVO.getRealName()) .likeIfPresent(MemberUserDO::getNickname, reqVO.getNickname()) .eqIfPresent(MemberUserDO::getMobile, reqVO.getPhone()) .betweenIfPresent(MemberUserDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(MemberUserDO::getId)); } @Update("update yshop_user set now_money=now_money-#{payPrice}" + " where id=#{uid}") int decPrice(@Param("payPrice") BigDecimal payPrice, @Param("uid") Long uid); @Update("update yshop_user set integral=integral-#{score}" + " where id=#{uid}") int decScore(@Param("score") Integer score, @Param("uid") Long uid); @Update("update yshop_user set pay_count=pay_count+1" + " where id=#{uid}") int incPayCount(@Param("uid") Long uid); @Update("update yshop_user set now_money=now_money+#{price}" + " where id=#{uid}") int incMoney(@Param("uid") Long uid,@Param("price") BigDecimal price); } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/dal/mysql/useraddress/UserAddressMapper.java ================================================ package co.yixiang.yshop.module.member.dal.mysql.useraddress; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.member.dal.dataobject.useraddress.UserAddressDO; import org.apache.ibatis.annotations.Mapper; import co.yixiang.yshop.module.member.controller.admin.useraddress.vo.*; /** * 用户地址 Mapper * * @author yshop */ @Mapper public interface UserAddressMapper extends BaseMapperX { default PageResult selectPage(UserAddressPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(UserAddressDO::getUid, reqVO.getUid()) .likeIfPresent(UserAddressDO::getRealName, reqVO.getRealName()) .eqIfPresent(UserAddressDO::getPhone, reqVO.getPhone()) .eqIfPresent(UserAddressDO::getProvince, reqVO.getProvince()) .eqIfPresent(UserAddressDO::getCity, reqVO.getCity()) .eqIfPresent(UserAddressDO::getCityId, reqVO.getCityId()) .eqIfPresent(UserAddressDO::getDistrict, reqVO.getDistrict()) .eqIfPresent(UserAddressDO::getDetail, reqVO.getDetail()) .eqIfPresent(UserAddressDO::getPostCode, reqVO.getPostCode()) .eqIfPresent(UserAddressDO::getLongitude, reqVO.getLongitude()) .eqIfPresent(UserAddressDO::getLatitude, reqVO.getLatitude()) .eqIfPresent(UserAddressDO::getIsDefault, reqVO.getIsDefault()) .betweenIfPresent(UserAddressDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(UserAddressDO::getId)); } default List selectList(UserAddressExportReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .eqIfPresent(UserAddressDO::getUid, reqVO.getUid()) .likeIfPresent(UserAddressDO::getRealName, reqVO.getRealName()) .eqIfPresent(UserAddressDO::getPhone, reqVO.getPhone()) .eqIfPresent(UserAddressDO::getProvince, reqVO.getProvince()) .eqIfPresent(UserAddressDO::getCity, reqVO.getCity()) .eqIfPresent(UserAddressDO::getCityId, reqVO.getCityId()) .eqIfPresent(UserAddressDO::getDistrict, reqVO.getDistrict()) .eqIfPresent(UserAddressDO::getDetail, reqVO.getDetail()) .eqIfPresent(UserAddressDO::getPostCode, reqVO.getPostCode()) .eqIfPresent(UserAddressDO::getLongitude, reqVO.getLongitude()) .eqIfPresent(UserAddressDO::getLatitude, reqVO.getLatitude()) .eqIfPresent(UserAddressDO::getIsDefault, reqVO.getIsDefault()) .betweenIfPresent(UserAddressDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(UserAddressDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/dal/mysql/userbill/UserBillMapper.java ================================================ package co.yixiang.yshop.module.member.dal.mysql.userbill; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.member.dal.dataobject.userbill.UserBillDO; import org.apache.ibatis.annotations.Mapper; import co.yixiang.yshop.module.member.controller.admin.userbill.vo.*; /** * 用户账单 Mapper * * @author yshop */ @Mapper public interface UserBillMapper extends BaseMapperX { default PageResult selectPage(UserBillPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(UserBillDO::getUid, reqVO.getUid()) .eqIfPresent(UserBillDO::getLinkId, reqVO.getLinkId()) .eqIfPresent(UserBillDO::getPm, reqVO.getPm()) .eqIfPresent(UserBillDO::getTitle, reqVO.getTitle()) .eqIfPresent(UserBillDO::getCategory, reqVO.getCategory()) .eqIfPresent(UserBillDO::getType, reqVO.getType()) .eqIfPresent(UserBillDO::getNumber, reqVO.getNumber()) .eqIfPresent(UserBillDO::getBalance, reqVO.getBalance()) .eqIfPresent(UserBillDO::getMark, reqVO.getMark()) .betweenIfPresent(UserBillDO::getCreateTime, reqVO.getCreateTime()) .eqIfPresent(UserBillDO::getStatus, reqVO.getStatus()) .orderByDesc(UserBillDO::getId)); } default List selectList(UserBillExportReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .eqIfPresent(UserBillDO::getUid, reqVO.getUid()) .eqIfPresent(UserBillDO::getLinkId, reqVO.getLinkId()) .eqIfPresent(UserBillDO::getPm, reqVO.getPm()) .eqIfPresent(UserBillDO::getTitle, reqVO.getTitle()) .eqIfPresent(UserBillDO::getCategory, reqVO.getCategory()) .eqIfPresent(UserBillDO::getType, reqVO.getType()) .eqIfPresent(UserBillDO::getNumber, reqVO.getNumber()) .eqIfPresent(UserBillDO::getBalance, reqVO.getBalance()) .eqIfPresent(UserBillDO::getMark, reqVO.getMark()) .betweenIfPresent(UserBillDO::getCreateTime, reqVO.getCreateTime()) .eqIfPresent(UserBillDO::getStatus, reqVO.getStatus()) .orderByDesc(UserBillDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/dal/redis/RedisKeyConstants.java ================================================ package co.yixiang.yshop.module.member.dal.redis; import co.yixiang.yshop.framework.redis.core.RedisKeyDefine; import static co.yixiang.yshop.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING; /** * System Redis Key 枚举类 * * @author yshop */ public interface RedisKeyConstants { RedisKeyDefine YSHOP_MINI_LOGIN_CACHE_KEY = new RedisKeyDefine("小程序登录session", "yshop_mini_login_cache:%s", // 参数为访问uid+key STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.FOREVER); } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/dal/redis/order/MiniRedisDAO.java ================================================ package co.yixiang.yshop.module.member.dal.redis.order; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import jakarta.annotation.Resource; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Repository; import static co.yixiang.yshop.module.member.dal.redis.RedisKeyConstants.YSHOP_MINI_LOGIN_CACHE_KEY; /** * * @author yshop */ @Repository public class MiniRedisDAO { @Resource private StringRedisTemplate stringRedisTemplate; public String get(String key) { String redisKey = formatKey(key); return stringRedisTemplate.opsForValue().get(redisKey); } public String set(String str,String key) { String redisKey = formatKey(key); stringRedisTemplate.opsForValue().set(redisKey, str); return key; } public void delete(String key) { String redisKey = formatKey(key); stringRedisTemplate.delete(redisKey); } private static String formatKey(String key) { return String.format(YSHOP_MINI_LOGIN_CACHE_KEY.getKeyTemplate(), key); } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/framework/package-info.java ================================================ /** * 属于 member 模块的 framework 封装 * * @author yshop */ package co.yixiang.yshop.module.member.framework; ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/framework/web/config/MemberWebConfiguration.java ================================================ package co.yixiang.yshop.module.member.framework.web.config; import co.yixiang.yshop.framework.swagger.config.YshopSwaggerAutoConfiguration; import org.springdoc.core.models.GroupedOpenApi; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * member 模块的 web 组件的 Configuration * * @author yshop */ @Configuration(proxyBeanMethods = false) public class MemberWebConfiguration { /** * member 模块的 API 分组 */ @Bean public GroupedOpenApi memberGroupedOpenApi() { return YshopSwaggerAutoConfiguration.buildGroupedOpenApi("member"); } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/framework/web/package-info.java ================================================ /** * member 模块的 web 配置 */ package co.yixiang.yshop.module.member.framework.web; ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/service/auth/MemberAuthService.java ================================================ package co.yixiang.yshop.module.member.service.auth; import co.yixiang.yshop.module.member.controller.app.auth.vo.*; import jakarta.validation.Valid; import org.springframework.web.bind.annotation.RequestParam; /** * 会员的认证 Service 接口 * * 提供用户的账号密码登录、token 的校验等认证相关的功能 * * @author yshop */ public interface MemberAuthService { /** * 手机 + 密码登录 * * @param reqVO 登录信息 * @return 登录结果 */ AppAuthLoginRespVO login(@Valid AppAuthLoginReqVO reqVO); /** * 基于 token 退出登录 * * @param token token */ void logout(String token); /** * 手机 + 验证码登陆 * * @param reqVO 登陆信息 * @return 登录结果 */ AppAuthLoginRespVO smsLogin(@Valid AppAuthSmsLoginReqVO reqVO); /** * 社交登录,使用 code 授权码 * * @param reqVO 登录信息 * @return 登录结果 */ //AppAuthLoginRespVO socialLogin(@Valid AppAuthSocialLoginReqVO reqVO); /** * 微信小程序的一键登录 * * @param reqVO 登录信息 * @return 登录结果 */ AppAuthLoginRespVO weixinMiniAppLogin(AppAuthWeixinMiniAppLoginReqVO reqVO); /** * 微信小程序的一键登录 * * @param loginVO 登录信息 * @return 登录结果 */ AppAuthLoginRespVO weixinMiniAppLogin2(AppWeixinMiniLoginVO loginVO); /** * 微信小程序的一键登录 * * @param encryptedData encryptedData * @param encryptedData encryptedData * @param encryptedData encryptedData * @return 登录结果 */ AppAuthLoginRespVO weixinMiniAppLogin3(String encryptedData, String iv, String openid); /** * 微信公众号的一键登录 * * @param code code * @return 登录结果 */ AppAuthLoginRespVO wechatAuth(String code); /** * 获得社交认证 URL * * @param type 社交平台类型 * @param redirectUri 跳转地址 * @return 认证 URL */ //String getSocialAuthorizeUrl(Integer type, String redirectUri); /** * 修改用户密码 * @param userId 用户id * @param userReqVO 用户请求实体类 */ void updatePassword(Long userId, AppAuthUpdatePasswordReqVO userReqVO); /** * 忘记密码 * @param userReqVO 用户请求实体类 */ //void resetPassword(AppAuthResetPasswordReqVO userReqVO); /** * 给用户发送短信验证码 * * @param userId 用户编号 * @param reqVO 发送信息 */ void sendSmsCode(Long userId, AppAuthSmsSendReqVO reqVO); /** * 刷新访问令牌 * * @param refreshToken 刷新令牌 * @return 登录结果 */ AppAuthLoginRespVO refreshToken(String refreshToken); } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/service/auth/MemberAuthServiceImpl.java ================================================ package co.yixiang.yshop.module.member.service.auth; import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult; import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.util.monitor.TracerUtils; import co.yixiang.yshop.framework.common.util.servlet.ServletUtils; import co.yixiang.yshop.module.member.controller.app.auth.vo.*; import co.yixiang.yshop.module.member.convert.auth.AuthConvert; import co.yixiang.yshop.module.member.convert.user.UserConvert; import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO; import co.yixiang.yshop.module.member.dal.mysql.user.MemberUserMapper; import co.yixiang.yshop.module.member.dal.redis.order.MiniRedisDAO; import co.yixiang.yshop.module.member.enums.LoginTypeEnum; import co.yixiang.yshop.module.member.service.user.MemberUserService; import co.yixiang.yshop.module.system.api.logger.LoginLogApi; import co.yixiang.yshop.module.system.api.logger.dto.LoginLogCreateReqDTO; import co.yixiang.yshop.module.system.api.oauth2.OAuth2TokenApi; import co.yixiang.yshop.module.system.api.oauth2.dto.OAuth2AccessTokenCreateReqDTO; import co.yixiang.yshop.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO; import co.yixiang.yshop.module.system.api.sms.SmsCodeApi; import co.yixiang.yshop.module.system.api.social.SocialUserApi; import co.yixiang.yshop.module.system.api.social.dto.SocialUserBindReqDTO; import co.yixiang.yshop.module.system.enums.logger.LoginLogTypeEnum; import co.yixiang.yshop.module.system.enums.logger.LoginResultEnum; import co.yixiang.yshop.module.system.enums.oauth2.OAuth2ClientConstants; import co.yixiang.yshop.module.system.enums.sms.SmsSceneEnum; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.google.common.annotations.VisibleForTesting; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.bean.WxOAuth2UserInfo; import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.mp.api.WxMpService; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Objects; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.util.servlet.ServletUtils.getClientIP; import static co.yixiang.yshop.module.member.enums.ErrorCodeConstants.*; /** * 会员的认证 Service 接口 * * @author yshop */ @Service @Slf4j public class MemberAuthServiceImpl implements MemberAuthService { @Resource private MemberUserService userService; @Resource private SmsCodeApi smsCodeApi; @Resource private LoginLogApi loginLogApi; @Resource private SocialUserApi socialUserApi; @Resource private OAuth2TokenApi oauth2TokenApi; @Resource private WxMaService wxMaService; @Resource private WxMpService mpService; @Resource private PasswordEncoder passwordEncoder; @Resource private MemberUserMapper userMapper; @Resource private MiniRedisDAO miniRedisDAO; @Override public AppAuthLoginRespVO login(AppAuthLoginReqVO reqVO) { // 使用手机 + 密码,进行登录。 MemberUserDO user = login0(reqVO.getMobile(), reqVO.getPassword()); // 如果 socialType 非空,说明需要绑定社交用户 if (reqVO.getSocialType() != null) { socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())); } // 创建 Token 令牌,记录登录日志 return createTokenAfterLoginSuccess(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE); } @Override @Transactional public AppAuthLoginRespVO smsLogin(AppAuthSmsLoginReqVO reqVO) { // 校验验证码 String userIp = getClientIP(); smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.MEMBER_LOGIN.getScene(), userIp)); MemberUserDO user = null; if(StrUtil.isNotBlank(reqVO.getOpenid())){ LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); //根据openid查用户是否存在 if(LoginTypeEnum.ROUNTINE.getValue().equals(reqVO.getFrom())){ lambdaQueryWrapper.eq(MemberUserDO::getRoutineOpenid,reqVO.getOpenid()); }else { lambdaQueryWrapper.eq(MemberUserDO::getOpenid,reqVO.getOpenid()); } user = userMapper.selectOne(lambdaQueryWrapper); } if(user == null){ // 获得获得注册用户 user = userService.createUserIfAbsent(reqVO.getMobile(), userIp,reqVO.getFrom()); Assert.notNull(user, "获取用户失败,结果为空"); if(StrUtil.isNotBlank(reqVO.getOpenid())){ if(LoginTypeEnum.ROUNTINE.getValue().equals(reqVO.getFrom())){ user.setRoutineOpenid(reqVO.getOpenid()); }else { user.setOpenid(reqVO.getOpenid()); } userService.updateById(user); } }else { if(StrUtil.isBlank(user.getMobile())){ user.setMobile(reqVO.getMobile()); userService.updateById(user); } } // 如果 socialType 非空,说明需要绑定社交用户 if (reqVO.getSocialType() != null) { socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())); } // 创建 Token 令牌,记录登录日志 AppAuthLoginRespVO appAuthLoginRespVO = createTokenAfterLoginSuccess(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_SMS); appAuthLoginRespVO.setUserInfo(UserConvert.INSTANCE.convert3(user)); return appAuthLoginRespVO; // 创建 Token 令牌,记录登录日志 // return createTokenAfterLoginSuccess(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_SMS); } // @Override // public AppAuthLoginRespVO socialLogin(AppAuthSocialLoginReqVO reqVO) { // // 使用 code 授权码,进行登录。然后,获得到绑定的用户编号 // Long userId = socialUserApi.getBindUserId(UserTypeEnum.MEMBER.getValue(), reqVO.getType(), // reqVO.getCode(), reqVO.getState()); // if (userId == null) { // throw exception(AUTH_THIRD_LOGIN_NOT_BIND); // } // // // 自动登录 // MemberUserDO user = userService.getUser(userId); // if (user == null) { // throw exception(USER_NOT_EXISTS); // } // // // 创建 Token 令牌,记录登录日志 // return createTokenAfterLoginSuccess(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL); // } @Override public AppAuthLoginRespVO weixinMiniAppLogin(AppAuthWeixinMiniAppLoginReqVO reqVO) { // 获得对应的手机号信息 WxMaPhoneNumberInfo phoneNumberInfo; try { phoneNumberInfo = wxMaService.getUserService().getNewPhoneNoInfo(reqVO.getPhoneCode()); } catch (Exception exception) { throw exception(AUTH_WEIXIN_MINI_APP_PHONE_CODE_ERROR); } // 获得获得注册用户 MemberUserDO user = userService.createUserIfAbsent(phoneNumberInfo.getPurePhoneNumber(), getClientIP(), LoginTypeEnum.ROUNTINE.getValue()); Assert.notNull(user, "获取用户失败,结果为空"); // 绑定社交用户 socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), SocialTypeEnum.WECHAT_MINI_APP.getType(), reqVO.getLoginCode(), "")); // 创建 Token 令牌,记录登录日志 return createTokenAfterLoginSuccess(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL); } /** * 微信小程序的一键登录 * * @param loginVO 登录信息 * @return 登录结果 */ @Override public AppAuthLoginRespVO weixinMiniAppLogin2(AppWeixinMiniLoginVO loginVO){ try { WxMaJscode2SessionResult session = wxMaService.getUserService().getSessionInfo(loginVO.getCode()); log.info(session.getOpenid()); miniRedisDAO.set(session.getSessionKey(),session.getOpenid()); //根据openid查用户是否存在 MemberUserDO memberUserDO = userMapper.selectOne(new LambdaQueryWrapper() .eq(MemberUserDO::getRoutineOpenid,session.getOpenid())); AppAuthLoginRespVO appAuthLoginRespVO = new AppAuthLoginRespVO(); if(memberUserDO != null) { appAuthLoginRespVO = createTokenAfterLoginSuccess(memberUserDO, memberUserDO.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL); appAuthLoginRespVO.setUserInfo(UserConvert.INSTANCE.convert3(memberUserDO)); //return appAuthLoginRespVO; } appAuthLoginRespVO.setOpenId(session.getOpenid()); return appAuthLoginRespVO; } catch (WxErrorException e) { log.error(e.getMessage()); throw exception(MINI_AUTH_LOGIN_BAD); } } /** * 微信小程序的一键登录 * * @param encryptedData encryptedData * @param encryptedData encryptedData * @param encryptedData encryptedData * @return 登录结果 */ @Override public AppAuthLoginRespVO weixinMiniAppLogin3(String encryptedData, String iv, String openid){ // 解密用户信息 String sessionKey = miniRedisDAO.get(openid); if(StrUtil.isBlank(sessionKey)) { throw exception(MINI_AUTH_LOGIN_BAD2); } // 解密 WxMaPhoneNumberInfo phoneNoInfo = wxMaService.getUserService().getPhoneNoInfo(sessionKey, encryptedData, iv); // 用户已经存在 MemberUserDO memberUserDO = userMapper.selectByMobile(phoneNoInfo.getPhoneNumber()); if (memberUserDO == null) { // 获得获得注册用户 memberUserDO = userService.createUserIfAbsent(phoneNoInfo.getPhoneNumber(), getClientIP(), LoginTypeEnum.ROUNTINE.getValue()); memberUserDO.setRoutineOpenid(openid); memberUserDO.setNickname("yshop用户_" + memberUserDO.getId()); userMapper.updateById(memberUserDO); } if(StrUtil.isBlank(memberUserDO.getRoutineOpenid())){ memberUserDO.setRoutineOpenid(openid); userMapper.updateById(memberUserDO); } // 创建 Token 令牌,记录登录日志 AppAuthLoginRespVO appAuthLoginRespVO = createTokenAfterLoginSuccess(memberUserDO, memberUserDO.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL); appAuthLoginRespVO.setOpenId(openid); appAuthLoginRespVO.setUserInfo(UserConvert.INSTANCE.convert3(memberUserDO)); return appAuthLoginRespVO; } /** * 微信公众号登录 * @param code code * @return */ @Override public AppAuthLoginRespVO wechatAuth(String code) { WxOAuth2AccessToken wxMpOAuth2AccessToken = null; MemberUserDO memberUserDO = null; try { wxMpOAuth2AccessToken = mpService.getOAuth2Service().getAccessToken(code); WxOAuth2UserInfo wxMpUser = mpService.getOAuth2Service().getUserInfo(wxMpOAuth2AccessToken, null); String openid = wxMpUser.getOpenid(); //根据openid查用户是否存在 memberUserDO = userMapper.selectOne(new LambdaQueryWrapper() .eq(MemberUserDO::getOpenid,openid)); if (memberUserDO == null) { // 获得获得注册用户 memberUserDO = userService.createUserIfAbsent("", getClientIP(), LoginTypeEnum.WECHAT.getValue()); //过滤掉表情 String nickname = wxMpUser.getNickname(); memberUserDO.setOpenid(openid); memberUserDO.setNickname(nickname); memberUserDO.setAvatar(wxMpUser.getHeadImgUrl()); memberUserDO.setUsername(openid); userMapper.updateById(memberUserDO); } }catch (WxErrorException e) { log.error(e.getMessage()); throw exception(MINI_AUTH_LOGIN_BAD); } // 创建 Token 令牌,记录登录日志 AppAuthLoginRespVO appAuthLoginRespVO = createTokenAfterLoginSuccess(memberUserDO, memberUserDO.getNickname(), LoginLogTypeEnum.LOGIN_SOCIAL); appAuthLoginRespVO.setOpenId(memberUserDO.getOpenid()); appAuthLoginRespVO.setUserInfo(UserConvert.INSTANCE.convert3(memberUserDO)); return appAuthLoginRespVO; } private AppAuthLoginRespVO createTokenAfterLoginSuccess(MemberUserDO user, String mobile, LoginLogTypeEnum logType) { // 插入登陆日志 createLoginLog(user.getId(), mobile, logType, LoginResultEnum.SUCCESS); // 创建 Token 令牌 OAuth2AccessTokenRespDTO accessTokenRespDTO = oauth2TokenApi.createAccessToken(new OAuth2AccessTokenCreateReqDTO() .setUserId(user.getId()).setUserType(getUserType().getValue()) .setClientId(OAuth2ClientConstants.CLIENT_ID_DEFAULT)); // 构建返回结果 return AuthConvert.INSTANCE.convert(accessTokenRespDTO); } // @Override // public String getSocialAuthorizeUrl(Integer type, String redirectUri) { // return socialUserApi.getAuthorizeUrl(type, redirectUri); // } private MemberUserDO login0(String mobile, String password) { final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_MOBILE; // 校验账号是否存在 MemberUserDO user = userService.getUserByMobile(mobile); if (user == null) { createLoginLog(null, mobile, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); throw exception(AUTH_LOGIN_BAD_CREDENTIALS); } if (!userService.isPasswordMatch(password, user.getPassword())) { createLoginLog(user.getId(), mobile, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); throw exception(AUTH_LOGIN_BAD_CREDENTIALS); } // 校验是否禁用 if (ObjectUtil.notEqual(user.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { createLoginLog(user.getId(), mobile, logTypeEnum, LoginResultEnum.USER_DISABLED); throw exception(AUTH_LOGIN_USER_DISABLED); } return user; } private void createLoginLog(Long userId, String mobile, LoginLogTypeEnum logType, LoginResultEnum loginResult) { // 插入登录日志 LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); reqDTO.setLogType(logType.getType()); reqDTO.setTraceId(TracerUtils.getTraceId()); reqDTO.setUserId(userId); reqDTO.setUserType(getUserType().getValue()); reqDTO.setUsername(mobile); reqDTO.setUserAgent(ServletUtils.getUserAgent()); reqDTO.setUserIp(getClientIP()); reqDTO.setResult(loginResult.getResult()); loginLogApi.createLoginLog(reqDTO); // 更新最后登录时间 if (userId != null && Objects.equals(LoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) { userService.updateUserLogin(userId, getClientIP()); } } @Override public void logout(String token) { // 删除访问令牌 OAuth2AccessTokenRespDTO accessTokenRespDTO = oauth2TokenApi.removeAccessToken(token); if (accessTokenRespDTO == null) { return; } // 删除成功,则记录登出日志 createLogoutLog(accessTokenRespDTO.getUserId()); } @Override public void updatePassword(Long userId, AppAuthUpdatePasswordReqVO reqVO) { // 检验旧密码 MemberUserDO userDO = checkOldPassword(userId, reqVO.getOldPassword()); // 更新用户密码 // TODO yshop:需要重构到用户模块 userMapper.updateById(MemberUserDO.builder().id(userDO.getId()) .password(passwordEncoder.encode(reqVO.getPassword())).build()); } // @Override // public void resetPassword(AppAuthResetPasswordReqVO reqVO) { // // 检验用户是否存在 // MemberUserDO userDO = checkUserIfExists(reqVO.getMobile()); // // // 使用验证码 // smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.MEMBER_FORGET_PASSWORD, // getClientIP())); // // // 更新密码 // userMapper.updateById(MemberUserDO.builder().id(userDO.getId()) // .password(passwordEncoder.encode(reqVO.getPassword())).build()); // } @Override public void sendSmsCode(Long userId, AppAuthSmsSendReqVO reqVO) { // TODO 要根据不同的场景,校验是否有用户 smsCodeApi.sendSmsCode(AuthConvert.INSTANCE.convert(reqVO).setCreateIp(getClientIP())); } @Override public AppAuthLoginRespVO refreshToken(String refreshToken) { OAuth2AccessTokenRespDTO accessTokenDO = oauth2TokenApi.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT); return AuthConvert.INSTANCE.convert(accessTokenDO); } /** * 校验旧密码 * * @param id 用户 id * @param oldPassword 旧密码 * @return MemberUserDO 用户实体 */ @VisibleForTesting public MemberUserDO checkOldPassword(Long id, String oldPassword) { MemberUserDO user = userMapper.selectById(id); if (user == null) { throw exception(USER_NOT_EXISTS); } // 参数:未加密密码,编码后的密码 if (!passwordEncoder.matches(oldPassword,user.getPassword())) { throw exception(USER_PASSWORD_FAILED); } return user; } public MemberUserDO checkUserIfExists(String mobile) { MemberUserDO user = userMapper.selectByMobile(mobile); if (user == null) { throw exception(USER_NOT_EXISTS); } return user; } private void createLogoutLog(Long userId) { LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); reqDTO.setLogType(LoginLogTypeEnum.LOGOUT_SELF.getType()); reqDTO.setTraceId(TracerUtils.getTraceId()); reqDTO.setUserId(userId); reqDTO.setUserType(getUserType().getValue()); reqDTO.setUsername(getMobile(userId)); reqDTO.setUserAgent(ServletUtils.getUserAgent()); reqDTO.setUserIp(getClientIP()); reqDTO.setResult(LoginResultEnum.SUCCESS.getResult()); loginLogApi.createLoginLog(reqDTO); } private String getMobile(Long userId) { if (userId == null) { return null; } MemberUserDO user = userService.getUser(userId); return user != null ? user.getMobile() : null; } private UserTypeEnum getUserType() { return UserTypeEnum.MEMBER; } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/service/user/MemberUserService.java ================================================ package co.yixiang.yshop.module.member.service.user; import co.yixiang.yshop.framework.common.validation.Mobile; import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserQueryVo; import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserUpdateMobileReqVO; import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO; import com.baomidou.mybatisplus.extension.service.IService; import org.springframework.web.bind.annotation.RequestParam; import java.io.InputStream; import java.math.BigDecimal; import java.util.Collection; import java.util.List; /** * 会员用户 Service 接口 * * @author yshop */ public interface MemberUserService extends IService { /** * 通过手机查询用户 * * @param mobile 手机 * @return 用户对象 */ MemberUserDO getUserByMobile(String mobile); /** * 基于用户昵称,模糊匹配用户列表 * * @param nickname 用户昵称,模糊匹配 * @return 用户信息的列表 */ List getUserListByNickname(String nickname); /** * 基于手机号创建用户。 * 如果用户已经存在,则直接进行返回 * * @param mobile 手机号 * @param registerIp 注册 IP * @return 用户对象 */ MemberUserDO createUserIfAbsent(@Mobile String mobile, String registerIp,String from); /** * 更新用户的最后登陆信息 * * @param id 用户编号 * @param loginIp 登陆 IP */ void updateUserLogin(Long id, String loginIp); /** * 通过用户 ID 查询用户 * * @param id 用户ID * @return 用户对象信息 */ MemberUserDO getUser(Long id); /** * 通过用户 ID 查询用户 * * @param id 用户ID * @return 用户对象信息 */ AppUserQueryVo getAppUser(Long id); /** * 减去用户余额 * @param uid uid * @param payPrice 金额 */ void decPrice(Long uid, BigDecimal payPrice); /** * 减去用户积分 * @param uid uid * @param score 积分 */ void decScore(Long uid, Integer score); /** * 增加购买次数 * @param uid uid */ void incPayCount(Long uid); /** * 通过用户 ID 查询用户们 * * @param ids 用户 ID * @return 用户对象信息数组 */ List getUserList(Collection ids); /** * 修改用户昵称 * @param userId 用户id * @param nickname 用户新昵称 */ void updateUserNickname(Long userId, String nickname,String birthday, Integer gender, String avatar, String mobile); /** * 修改用户头像 * @param userId 用户id * @param inputStream 头像文件 * @return 头像url */ String updateUserAvatar(Long userId, InputStream inputStream) throws Exception; /** * 修改手机 * @param userId 用户id * @param reqVO 请求实体 */ void updateUserMobile(Long userId, AppUserUpdateMobileReqVO reqVO); /** * 判断密码是否匹配 * * @param rawPassword 未加密的密码 * @param encodedPassword 加密后的密码 * @return 是否匹配 */ boolean isPasswordMatch(String rawPassword, String encodedPassword); /** * 更新用户余额 * @param uid y用户id * @param price 金额 */ void incMoney(Long uid,BigDecimal price); } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/service/user/MemberUserServiceImpl.java ================================================ package co.yixiang.yshop.module.member.service.user; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.enums.ShopCommonEnum; import co.yixiang.yshop.module.coupon.dal.dataobject.couponuser.CouponUserDO; import co.yixiang.yshop.module.coupon.service.couponuser.AppCouponUserService; import co.yixiang.yshop.module.infra.api.file.FileApi; import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserQueryVo; import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserUpdateMobileReqVO; import co.yixiang.yshop.module.member.convert.user.UserConvert; import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO; import co.yixiang.yshop.module.member.dal.dataobject.userbill.UserBillDO; import co.yixiang.yshop.module.member.dal.mysql.user.MemberUserMapper; import co.yixiang.yshop.module.member.dal.mysql.userbill.UserBillMapper; import co.yixiang.yshop.module.member.enums.BillDetailEnum; import co.yixiang.yshop.module.member.service.userbill.UserBillService; import co.yixiang.yshop.module.system.api.sms.SmsCodeApi; import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeUseReqDTO; import co.yixiang.yshop.module.system.enums.sms.SmsSceneEnum; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.google.common.annotations.VisibleForTesting; import jakarta.annotation.Resource; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import java.io.InputStream; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Collection; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.util.servlet.ServletUtils.getClientIP; import static co.yixiang.yshop.module.member.enums.ErrorCodeConstants.USER_NOT_EXISTS; /** * 会员 User Service 实现类 * * @author yshop */ @Service @Valid @Slf4j public class MemberUserServiceImpl extends ServiceImpl implements MemberUserService { @Resource private MemberUserMapper memberUserMapper; @Resource private FileApi fileApi; @Resource private SmsCodeApi smsCodeApi; @Resource private PasswordEncoder passwordEncoder; @Resource private AppCouponUserService appCouponUserService; @Resource private UserBillMapper userBillMapper; @Resource private UserBillService billService; @Override public MemberUserDO getUserByMobile(String mobile) { return memberUserMapper.selectByMobile(mobile); } @Override public List getUserListByNickname(String nickname) { return memberUserMapper.selectListByNicknameLike(nickname); } @Override public MemberUserDO createUserIfAbsent(String mobile, String registerIp,String from) { // 用户已经存在 if(StrUtil.isNotEmpty(mobile)){ MemberUserDO user = memberUserMapper.selectByMobile(mobile); if (user != null) { return user; } } // 用户不存在,则进行创建 return this.createUser(mobile, registerIp,from); } private MemberUserDO createUser(String mobile, String registerIp,String from) { // 生成密码 String password = IdUtil.fastSimpleUUID(); // 插入用户 MemberUserDO user = new MemberUserDO(); user.setNickname(mobile); user.setUsername(mobile); user.setMobile(mobile); user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启 user.setPassword(encodePassword(password)); // 加密密码 user.setRegisterIp(registerIp); user.setLoginType(from); memberUserMapper.insert(user); return user; } @Override public void updateUserLogin(Long id, String loginIp) { memberUserMapper.updateById(new MemberUserDO().setId(id) .setLoginIp(loginIp).setLoginDate(LocalDateTime.now())); } @Override public MemberUserDO getUser(Long id) { return memberUserMapper.selectById(id); } @Override public AppUserQueryVo getAppUser(Long id){ AppUserQueryVo appUserQueryVo = UserConvert.INSTANCE.convert3(memberUserMapper.selectById(id)); Long count = appCouponUserService.count(new LambdaQueryWrapper() .eq(CouponUserDO::getUserId,id) .eq(CouponUserDO::getStatus, ShopCommonEnum.IS_STATUS_1)); appUserQueryVo.setCouponCount(count); QueryWrapper wrapper = new QueryWrapper<>(); wrapper.select("SUM(number) as sumAll") .eq("category", BillDetailEnum.CATEGORY_1.getValue()) .eq("type",BillDetailEnum.TYPE_3.getValue()).eq("uid",id); UserBillDO userBillDO = userBillMapper.selectOne(wrapper); if(userBillDO == null){ appUserQueryVo.setSumMoney(BigDecimal.ZERO); }else{ appUserQueryVo.setSumMoney(userBillDO.getSumAll()); } return appUserQueryVo; } /** * 减去用户余额 * @param uid uid * @param payPrice 金额 */ @Override @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public void decPrice(Long uid, BigDecimal payPrice) { memberUserMapper.decPrice(payPrice,uid); } /** * 减去用户积分 * @param uid uid * @param score 积分 */ @Override public void decScore(Long uid, Integer score) { memberUserMapper.decScore(score,uid); } /** * 增加购买次数 * @param uid uid */ @Override @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public void incPayCount(Long uid) { memberUserMapper.incPayCount(uid); } @Override public List getUserList(Collection ids) { return memberUserMapper.selectBatchIds(ids); } @Override public void updateUserNickname(Long userId, String nickname,String birthday, Integer gender, String avatar, String mobile) { MemberUserDO user = this.checkUserExists(userId); // 仅当新昵称不等于旧昵称时进行修改 // if (nickname.equals(user.getNickname())){ // return; // } MemberUserDO userDO = new MemberUserDO(); userDO.setId(user.getId()); userDO.setNickname(nickname); userDO.setBirthday(birthday); userDO.setGender(gender); userDO.setAvatar(avatar); userDO.setMobile(mobile); memberUserMapper.updateById(userDO); } @Override public String updateUserAvatar(Long userId, InputStream avatarFile) throws Exception { this.checkUserExists(userId); // 创建文件 String avatar = fileApi.createFile(IoUtil.readBytes(avatarFile)); // 更新头像路径 memberUserMapper.updateById(MemberUserDO.builder().id(userId).avatar(avatar).build()); return avatar; } @Override @Transactional(rollbackFor = Exception.class) public void updateUserMobile(Long userId, AppUserUpdateMobileReqVO reqVO) { // 检测用户是否存在 checkUserExists(userId); // TODO yshop:oldMobile 应该不用传递 // 校验旧手机和旧验证码 smsCodeApi.useSmsCode(new SmsCodeUseReqDTO().setMobile(reqVO.getOldMobile()).setCode(reqVO.getOldCode()) .setScene(SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene()).setUsedIp(getClientIP())); // 使用新验证码 smsCodeApi.useSmsCode(new SmsCodeUseReqDTO().setMobile(reqVO.getMobile()).setCode(reqVO.getCode()) .setScene(SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene()).setUsedIp(getClientIP())); // 更新用户手机 memberUserMapper.updateById(MemberUserDO.builder().id(userId).mobile(reqVO.getMobile()).build()); } @Override public boolean isPasswordMatch(String rawPassword, String encodedPassword) { return passwordEncoder.matches(rawPassword, encodedPassword); } /** * 更新用户余额 * @param uid y用户id * @param price 金额 */ @Override public void incMoney(Long uid, BigDecimal price) { if(price!=null&&price.doubleValue()>0){ memberUserMapper.incMoney(uid,price); } } /** * 对密码进行加密 * * @param password 密码 * @return 加密后的密码 */ private String encodePassword(String password) { return passwordEncoder.encode(password); } @VisibleForTesting public MemberUserDO checkUserExists(Long id) { if (id == null) { return null; } MemberUserDO user = memberUserMapper.selectById(id); if (user == null) { throw exception(USER_NOT_EXISTS); } return user; } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/service/user/UserService.java ================================================ package co.yixiang.yshop.module.member.service.user; import java.util.*; import co.yixiang.yshop.module.member.controller.admin.user.vo.*; import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO; import co.yixiang.yshop.framework.common.pojo.PageResult; import jakarta.validation.Valid; /** * 用户 Service 接口 * * @author yshop */ public interface UserService { /** * 创建用户 * * @param createReqVO 创建信息 * @return 编号 */ Long createUser(@Valid UserCreateReqVO createReqVO); /** * 更新用户 * * @param updateReqVO 更新信息 */ void updateUser(@Valid UserUpdateReqVO updateReqVO); /** * 更新金额与积分 * * @param updateReqVO 更新信息 */ void updateMony(@Valid UserUpdateMoneyReqVO updateReqVO); /** * 删除用户 * * @param id 编号 */ void deleteUser(Long id); /** * 获得用户 * * @param id 编号 * @return 用户 */ MemberUserDO getUser(Long id); /** * 获得用户列表 * * @param ids 编号 * @return 用户列表 */ List getUserList(Collection ids); /** * 获得用户分页 * * @param pageReqVO 分页查询 * @return 用户分页 */ PageResult getUserPage(UserPageReqVO pageReqVO); /** * 获得用户列表, 用于 Excel 导出 * * @param exportReqVO 查询条件 * @return 用户列表 */ List getUserList(UserExportReqVO exportReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/service/user/UserServiceImpl.java ================================================ package co.yixiang.yshop.module.member.service.user; import cn.hutool.core.util.NumberUtil; import co.yixiang.yshop.framework.common.enums.ShopCommonEnum; import co.yixiang.yshop.module.member.enums.BillDetailEnum; import co.yixiang.yshop.module.member.service.userbill.UserBillService; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.util.*; import co.yixiang.yshop.module.member.controller.admin.user.vo.*; import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.member.convert.user.UserConvert; import co.yixiang.yshop.module.member.dal.mysql.user.MemberUserMapper; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.member.enums.ErrorCodeConstants.*; /** * 用户 Service 实现类 * * @author yshop */ @Service @Validated public class UserServiceImpl implements UserService { @Resource private MemberUserMapper userMapper; @Resource private UserBillService userBillService; @Override public Long createUser(UserCreateReqVO createReqVO) { // 插入 MemberUserDO user = UserConvert.INSTANCE.convert(createReqVO); userMapper.insert(user); // 返回 return user.getId(); } @Override public void updateUser(UserUpdateReqVO updateReqVO) { // 校验存在 validateUserExists(updateReqVO.getId()); // 更新 MemberUserDO updateObj = UserConvert.INSTANCE.convert(updateReqVO); userMapper.updateById(updateObj); } /** * 更新金额与积分 * * @param updateReqVO 更新信息 */ @Override public void updateMony(UserUpdateMoneyReqVO updateReqVO){ MemberUserDO memberUserDO = userMapper.selectById(updateReqVO.getId()); double newMoney = 0d; String mark = ""; if(ShopCommonEnum.ADD_1.getValue().equals(updateReqVO.getPtype())){ mark = "系统增加了"+updateReqVO.getMoney()+"余额"; newMoney = NumberUtil.add(memberUserDO.getNowMoney(),new BigDecimal(updateReqVO.getMoney())).doubleValue(); userBillService.income(memberUserDO.getId(),"系统增加余额", BillDetailEnum.CATEGORY_1.getValue(), BillDetailEnum.TYPE_6.getValue(),Double.valueOf(updateReqVO.getMoney()),newMoney, mark,""); }else{ mark = "系统扣除了"+updateReqVO.getMoney()+"余额"; newMoney = NumberUtil.sub(memberUserDO.getNowMoney(),new BigDecimal(updateReqVO.getMoney())).doubleValue(); if(newMoney < 0) { newMoney = 0d; } userBillService.expend(memberUserDO.getId(), "系统减少余额", BillDetailEnum.CATEGORY_1.getValue(), BillDetailEnum.TYPE_7.getValue(), Double.valueOf(updateReqVO.getMoney()), newMoney, mark); } double newIntegral = 0d; if(ShopCommonEnum.ADD_1.getValue().equals(updateReqVO.getItype())){ mark = "系统增加了"+updateReqVO.getIntegral()+"积分"; newIntegral = NumberUtil.add(memberUserDO.getIntegral(),new BigDecimal(updateReqVO.getIntegral())).doubleValue(); userBillService.income(memberUserDO.getId(),"系统增加积分", BillDetailEnum.CATEGORY_2.getValue(), BillDetailEnum.TYPE_6.getValue(),Double.valueOf(updateReqVO.getIntegral()),newIntegral, mark,""); }else{ mark = "系统扣除了"+updateReqVO.getIntegral()+"积分"; newIntegral = NumberUtil.sub(memberUserDO.getIntegral(),new BigDecimal(updateReqVO.getIntegral())).doubleValue(); if(newIntegral < 0) { newIntegral = 0d; } userBillService.expend(memberUserDO.getId(), "系统减少积分", BillDetailEnum.CATEGORY_2.getValue(), BillDetailEnum.TYPE_7.getValue(), Double.valueOf(updateReqVO.getIntegral()), newIntegral, mark); } memberUserDO.setIntegral(BigDecimal.valueOf(newIntegral)); memberUserDO.setNowMoney(BigDecimal.valueOf(newMoney)); userMapper.updateById(memberUserDO); } @Override public void deleteUser(Long id) { // 校验存在 validateUserExists(id); // 删除 userMapper.deleteById(id); } private void validateUserExists(Long id) { if (userMapper.selectById(id) == null) { throw exception(USER_NOT_EXISTS); } } @Override public MemberUserDO getUser(Long id) { return userMapper.selectById(id); } @Override public List getUserList(Collection ids) { return userMapper.selectBatchIds(ids); } @Override public PageResult getUserPage(UserPageReqVO pageReqVO) { return userMapper.selectPage(pageReqVO); } @Override public List getUserList(UserExportReqVO exportReqVO) { return userMapper.selectList(exportReqVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/service/user/dto/WechatUserDto.java ================================================ //package co.yixiang.yshop.module.member.service.user.dto; // //import lombok.*; // ///** // * @ClassName WechatUserDTO // * @Author hupeng <610796224@qq.com> // * @Date 2023/7/18 // **/ //@Getter //@Setter //@Builder //@AllArgsConstructor //@NoArgsConstructor //public class WechatUserDto { // // private String openid; // // private String unionId; // // private String routineOpenid; // // private String nickname; // // private String headimgurl; // // private Integer sex; // // private String city; // // private String language; // // private String province; // // private String country; // // private Boolean subscribe; // // private Long subscribeTime; // //} ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/service/useraddress/AppUserAddressService.java ================================================ package co.yixiang.yshop.module.member.service.useraddress; import co.yixiang.yshop.module.member.controller.app.address.param.AppAddressParam; import co.yixiang.yshop.module.member.controller.app.address.vo.AppUserAddressQueryVo; import co.yixiang.yshop.module.member.dal.dataobject.useraddress.UserAddressDO; import com.baomidou.mybatisplus.extension.service.IService; import java.util.List; /** * 用户地址 Service 接口 * * @author yshop */ public interface AppUserAddressService extends IService { /** * 添加或者修改地址 * @param uid uid * @param param AddressParam */ Long addAndEdit(Long uid, AppAddressParam param); /** * 设置默认地址 * @param uid uid * @param addressId 地址id */ void setDefault(Long uid,String addressId); /** * 获取用户地址 * @param uid uid * @param page page * @param limit limit * @return List */ List getList(Long uid, int page, int limit); } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/service/useraddress/AppUserAddressServiceImpl.java ================================================ package co.yixiang.yshop.module.member.service.useraddress; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.enums.ShopCommonEnum; import co.yixiang.yshop.module.member.controller.app.address.param.AppAddressParam; import co.yixiang.yshop.module.member.controller.app.address.vo.AppUserAddressQueryVo; import co.yixiang.yshop.module.member.convert.useraddress.UserAddressConvert; import co.yixiang.yshop.module.member.dal.dataobject.useraddress.UserAddressDO; import co.yixiang.yshop.module.member.dal.mysql.useraddress.UserAddressMapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.member.enums.ErrorCodeConstants.USER_ADDRESS_NOT_EXISTS; import static co.yixiang.yshop.module.member.enums.ErrorCodeConstants.USER_ADDRESS_PARAM_NOT_EXISTS; /** * 用户地址 Service 实现类 * * @author yshop */ @Service @Validated public class AppUserAddressServiceImpl extends ServiceImpl implements AppUserAddressService { @Resource private UserAddressMapper userAddressMapper; /** * 添加或者修改地址 * @param uid uid * @param param AddressParam * @return id 地址id */ @Override public Long addAndEdit(Long uid, AppAddressParam param){ if(ShopCommonEnum.ENABLE_1.getValue().equals(param.getIsDefault())){ userAddressMapper.update(UserAddressDO.builder().isDefault(ShopCommonEnum.DEFAULT_0.getValue()).build(), new LambdaQueryWrapper().eq(UserAddressDO::getUid,uid)); } UserAddressDO userAddress = UserAddressDO.builder() .address(param.getAddress()) .detail(param.getDetail()) .uid(uid) .isDefault(param.getIsDefault()) .phone(param.getPhone()) .postCode(param.getPostCode()) .realName(param.getRealName()) .latitude(param.getLatitude()) .longitude(param.getLongitude()) .build(); if(StrUtil.isBlank(param.getId())){ this.save(userAddress); }else{ userAddress.setId(Long.valueOf(param.getId())); this.updateById(userAddress); } return userAddress.getId(); } /** * 设置默认地址 * @param uid uid * @param addressId 地址id */ @Override public void setDefault(Long uid,String addressId){ if(StrUtil.isBlank(addressId) || !NumberUtil.isNumber(addressId)){ throw exception(USER_ADDRESS_PARAM_NOT_EXISTS); } UserAddressDO address = new UserAddressDO(); address.setIsDefault(ShopCommonEnum.DEFAULT_0.getValue()); userAddressMapper.update(address, new LambdaQueryWrapper().eq(UserAddressDO::getUid,uid)); UserAddressDO userAddress = new UserAddressDO(); userAddress.setIsDefault(ShopCommonEnum.DEFAULT_1.getValue()); userAddress.setId(Long.valueOf(addressId)); userAddressMapper.updateById(userAddress); } /** * 获取用户地址 * @param uid uid * @param page page * @param limit limit * @return List */ @Override public List getList(Long uid, int page, int limit){ Page pageModel = new Page<>(page, limit); IPage pageList = this.lambdaQuery().eq(UserAddressDO::getUid,uid).page(pageModel); return UserAddressConvert.INSTANCE.convertList02(pageList.getRecords()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/service/useraddress/UserAddressService.java ================================================ package co.yixiang.yshop.module.member.service.useraddress; import java.util.*; import co.yixiang.yshop.module.member.controller.admin.useraddress.vo.*; import co.yixiang.yshop.module.member.dal.dataobject.useraddress.UserAddressDO; import co.yixiang.yshop.framework.common.pojo.PageResult; import jakarta.validation.Valid; /** * 用户地址 Service 接口 * * @author yshop */ public interface UserAddressService { /** * 创建用户地址 * * @param createReqVO 创建信息 * @return 编号 */ Long createUserAddress(@Valid UserAddressCreateReqVO createReqVO); /** * 更新用户地址 * * @param updateReqVO 更新信息 */ void updateUserAddress(@Valid UserAddressUpdateReqVO updateReqVO); /** * 删除用户地址 * * @param id 编号 */ void deleteUserAddress(Long id); /** * 获得用户地址 * * @param id 编号 * @return 用户地址 */ UserAddressDO getUserAddress(Long id); /** * 获得用户地址列表 * * @param ids 编号 * @return 用户地址列表 */ List getUserAddressList(Collection ids); /** * 获得用户地址分页 * * @param pageReqVO 分页查询 * @return 用户地址分页 */ PageResult getUserAddressPage(UserAddressPageReqVO pageReqVO); /** * 获得用户地址列表, 用于 Excel 导出 * * @param exportReqVO 查询条件 * @return 用户地址列表 */ List getUserAddressList(UserAddressExportReqVO exportReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/service/useraddress/UserAddressServiceImpl.java ================================================ package co.yixiang.yshop.module.member.service.useraddress; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.util.*; import co.yixiang.yshop.module.member.controller.admin.useraddress.vo.*; import co.yixiang.yshop.module.member.dal.dataobject.useraddress.UserAddressDO; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.member.convert.useraddress.UserAddressConvert; import co.yixiang.yshop.module.member.dal.mysql.useraddress.UserAddressMapper; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.member.enums.ErrorCodeConstants.*; /** * 用户地址 Service 实现类 * * @author yshop */ @Service @Validated public class UserAddressServiceImpl implements UserAddressService { @Resource private UserAddressMapper userAddressMapper; @Override public Long createUserAddress(UserAddressCreateReqVO createReqVO) { // 插入 UserAddressDO userAddress = UserAddressConvert.INSTANCE.convert(createReqVO); userAddressMapper.insert(userAddress); // 返回 return userAddress.getId(); } @Override public void updateUserAddress(UserAddressUpdateReqVO updateReqVO) { // 校验存在 validateUserAddressExists(updateReqVO.getId()); // 更新 UserAddressDO updateObj = UserAddressConvert.INSTANCE.convert(updateReqVO); userAddressMapper.updateById(updateObj); } @Override public void deleteUserAddress(Long id) { // 校验存在 validateUserAddressExists(id); // 删除 userAddressMapper.deleteById(id); } private void validateUserAddressExists(Long id) { if (userAddressMapper.selectById(id) == null) { throw exception(USER_ADDRESS_NOT_EXISTS); } } @Override public UserAddressDO getUserAddress(Long id) { return userAddressMapper.selectById(id); } @Override public List getUserAddressList(Collection ids) { return userAddressMapper.selectBatchIds(ids); } @Override public PageResult getUserAddressPage(UserAddressPageReqVO pageReqVO) { return userAddressMapper.selectPage(pageReqVO); } @Override public List getUserAddressList(UserAddressExportReqVO exportReqVO) { return userAddressMapper.selectList(exportReqVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/service/userbill/UserBillService.java ================================================ package co.yixiang.yshop.module.member.service.userbill; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.member.controller.admin.userbill.vo.UserBillPageReqVO; import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserBillVO; import co.yixiang.yshop.module.member.dal.dataobject.userbill.UserBillDO; import com.baomidou.mybatisplus.extension.service.IService; import java.util.List; /** * 用户账单 Service 接口 * * @author yshop */ public interface UserBillService extends IService { /** * 获得用户账单分页 * * @param pageReqVO 分页查询 * @return 用户账单分页 */ PageResult getUserBillPage(UserBillPageReqVO pageReqVO); /** * 增加支出流水 * @param uid uid * @param title 账单标题 * @param category 明细种类 * @param type 明细类型 * @param number 明细数字 * @param balance 剩余 * @param mark 备注 */ void expend(Long uid,String title,String category,String type,double number,double balance,String mark); /** * 增加收入/支入流水 * @param uid uid * @param title 账单标题 * @param category 明细种类 * @param type 明细类型 * @param number 明细数字 * @param balance 剩余 * @param mark 备注 * @param linkid 关联id */ void income(Long uid,String title,String category,String type,double number, double balance,String mark,String linkid); void income(Long uid,String title,String category,String type,double number, double balance,String mark,String linkid,String extendField); /** * 获取用户账单 * @param uid * @param cate 0余额 1-积分 * @param type 状态,0全部 1消费 2充值 3退款 * @param page * @param limit * @return */ List getBillList(Long uid, int cate, int type,int page, int limit); } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/main/java/co/yixiang/yshop/module/member/service/userbill/UserBillServiceImpl.java ================================================ package co.yixiang.yshop.module.member.service.userbill; import co.yixiang.yshop.framework.common.enums.ShopCommonEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.member.controller.admin.userbill.vo.UserBillPageReqVO; import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserBillVO; import co.yixiang.yshop.module.member.convert.userbill.UserBillConvert; import co.yixiang.yshop.module.member.dal.dataobject.userbill.UserBillDO; import co.yixiang.yshop.module.member.dal.mysql.userbill.UserBillMapper; import co.yixiang.yshop.module.member.enums.BillDetailEnum; import co.yixiang.yshop.module.member.enums.BillEnum; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.util.List; /** * 用户账单 Service 实现类 * * @author yshop */ @Service @Validated public class UserBillServiceImpl extends ServiceImpl implements UserBillService { @Resource private UserBillMapper userBillMapper; @Override public PageResult getUserBillPage(UserBillPageReqVO pageReqVO) { return userBillMapper.selectPage(pageReqVO); } /** * 增加支出流水 * @param uid uid * @param title 账单标题 * @param category 明细种类 * @param type 明细类型 * @param number 明细数字 * @param balance 剩余 * @param mark 备注 */ @Override public void expend(Long uid,String title,String category,String type,double number,double balance,String mark){ UserBillDO userBill = UserBillDO.builder() .uid(uid) .title(title) .category(category) .type(type) .number(BigDecimal.valueOf(number)) .balance(BigDecimal.valueOf(balance)) .mark(mark) .pm(BillEnum.PM_0.getValue()) .build(); userBillMapper.insert(userBill); } /** * 增加收入/支入流水 * @param uid uid * @param title 账单标题 * @param category 明细种类 * @param type 明细类型 * @param number 明细数字 * @param balance 剩余 * @param mark 备注 * @param linkid 关联id */ @Override public void income(Long uid,String title,String category,String type,double number, double balance,String mark,String linkid){ UserBillDO userBill = UserBillDO.builder() .uid(uid) .title(title) .category(category) .type(type) .number(BigDecimal.valueOf(number)) .balance(BigDecimal.valueOf(balance)) .mark(mark) .pm(BillEnum.PM_1.getValue()) .linkId(linkid) .build(); userBillMapper.insert(userBill); } @Override public void income(Long uid, String title, String category, String type, double number, double balance, String mark, String linkid, String extendField) { UserBillDO userBill = UserBillDO.builder() .uid(uid) .title(title) .category(category) .type(type) .number(BigDecimal.valueOf(number)) .balance(BigDecimal.valueOf(balance)) .mark(mark) .pm(BillEnum.PM_1.getValue()) .linkId(linkid) .extendField(extendField) .status(ShopCommonEnum.IS_STATUS_1.getValue()) .build(); userBillMapper.insert(userBill); } /** * 获取用户账单 * @param uid * @param cate 0余额 1-积分 * @param type 状态,0全部 1消费 2充值 3退款 * @param page * @param limit * @return */ @Override public List getBillList(Long uid,int cate, int type, int page, int limit) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); if(cate == 1){ wrapper.eq(UserBillDO::getCategory,BillDetailEnum.CATEGORY_2.getValue()); }else{ wrapper.eq(UserBillDO::getCategory,BillDetailEnum.CATEGORY_1.getValue()); } wrapper.eq(UserBillDO::getUid, uid) .eq(UserBillDO::getStatus,ShopCommonEnum.IS_STATUS_1.getValue()) //.eq(UserBillDO::getCategory,BillDetailEnum.CATEGORY_1.getValue()) .orderByDesc(UserBillDO::getId); switch (type){ case 1: wrapper.eq(UserBillDO::getType, BillDetailEnum.TYPE_3.getValue()); break; case 2: wrapper.eq(UserBillDO::getType, BillDetailEnum.TYPE_1.getValue()); break; case 3: wrapper.eq(UserBillDO::getType, BillDetailEnum.TYPE_5.getValue()); break; default: } Page pageModel = new Page<>(page, limit); IPage pageList = userBillMapper.selectPage(pageModel, wrapper); return UserBillConvert.INSTANCE.convertList02(pageList.getRecords()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/test/java/co/yixiang/yshop/module/member/service/auth/MemberAuthServiceTest.java ================================================ package co.yixiang.yshop.module.member.service.auth; import cn.binarywang.wx.miniapp.api.WxMaService; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.util.collection.ArrayUtils; import co.yixiang.yshop.framework.redis.config.YshopRedisAutoConfiguration; import co.yixiang.yshop.framework.test.core.ut.BaseDbAndRedisUnitTest; import co.yixiang.yshop.module.member.controller.app.auth.vo.AppAuthResetPasswordReqVO; import co.yixiang.yshop.module.member.controller.app.auth.vo.AppAuthUpdatePasswordReqVO; import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO; import co.yixiang.yshop.module.member.dal.mysql.user.MemberUserMapper; import co.yixiang.yshop.module.member.service.user.MemberUserService; import co.yixiang.yshop.module.system.api.oauth2.OAuth2TokenApi; import co.yixiang.yshop.module.system.api.logger.LoginLogApi; import co.yixiang.yshop.module.system.api.sms.SmsCodeApi; import co.yixiang.yshop.module.system.api.social.SocialUserApi; import jakarta.annotation.Resource; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import org.springframework.security.crypto.password.PasswordEncoder; import java.util.function.Consumer; import static cn.hutool.core.util.RandomUtil.randomEle; import static cn.hutool.core.util.RandomUtil.randomNumbers; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomPojo; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomString; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; // TODO @yshop:单测的 review,等逻辑都达成一致后 /** * {@link MemberAuthService} 的单元测试类 * * @author 宋天 */ @Import({MemberAuthServiceImpl.class, YshopRedisAutoConfiguration.class}) public class MemberAuthServiceTest extends BaseDbAndRedisUnitTest { // TODO @yshop:登录相关的单测,待补全 @Resource private MemberAuthServiceImpl authService; @MockBean private MemberUserService userService; @MockBean private SmsCodeApi smsCodeApi; @MockBean private LoginLogApi loginLogApi; @MockBean private OAuth2TokenApi oauth2TokenApi; @MockBean private SocialUserApi socialUserApi; @MockBean private WxMaService wxMaService; @MockBean private PasswordEncoder passwordEncoder; @Resource private MemberUserMapper memberUserMapper; @Test public void testUpdatePassword_success(){ // 准备参数 MemberUserDO userDO = randomUserDO(); memberUserMapper.insert(userDO); // 新密码 String newPassword = randomString(); // 请求实体 AppAuthUpdatePasswordReqVO reqVO = AppAuthUpdatePasswordReqVO.builder() .oldPassword(userDO.getPassword()) .password(newPassword) .build(); // 测试桩 // 这两个相等是为了返回ture这个结果 when(passwordEncoder.matches(reqVO.getOldPassword(),reqVO.getOldPassword())).thenReturn(true); when(passwordEncoder.encode(newPassword)).thenReturn(newPassword); // 更新用户密码 authService.updatePassword(userDO.getId(), reqVO); assertEquals(memberUserMapper.selectById(userDO.getId()).getPassword(),newPassword); } @Test public void testResetPassword_success(){ // 准备参数 MemberUserDO userDO = randomUserDO(); memberUserMapper.insert(userDO); // 随机密码 String password = randomNumbers(11); // 随机验证码 String code = randomNumbers(4); // mock when(passwordEncoder.encode(password)).thenReturn(password); // 更新用户密码 AppAuthResetPasswordReqVO reqVO = new AppAuthResetPasswordReqVO(); reqVO.setMobile(userDO.getMobile()); reqVO.setPassword(password); reqVO.setCode(code); // authService.resetPassword(reqVO); assertEquals(memberUserMapper.selectById(userDO.getId()).getPassword(),password); } // ========== 随机对象 ========== @SafeVarargs private static MemberUserDO randomUserDO(Consumer... consumers) { Consumer consumer = (o) -> { o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 o.setPassword(randomString()); }; return randomPojo(MemberUserDO.class, ArrayUtils.append(consumer, consumers)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/test/resources/application-unit-test.yaml ================================================ spring: main: lazy-initialization: true # 开启懒加载,加快速度 banner-mode: off # 单元测试,禁用 Banner --- #################### 数据库相关配置 #################### spring: # 数据源配置项 datasource: name: yshop-pro url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 driver-class-name: org.h2.Driver username: sa password: druid: async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度 initial-size: 1 # 单元测试,配置为 1,提升启动速度 sql: init: schema-locations: classpath:/sql/create_tables.sql # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 redis: host: 127.0.0.1 # 地址 port: 16379 # 端口(单元测试,使用 16379 端口) database: 0 # 数据库索引 mybatis: lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 --- #################### 定时任务相关配置 #################### --- #################### 配置中心相关配置 #################### --- #################### 服务保障相关配置 #################### # Lock4j 配置项(单元测试,禁用 Lock4j) # Resilience4j 配置项 --- #################### 监控相关配置 #################### --- #################### yshop相关配置 #################### # yshop配置项,设置当前项目所有自定义的配置 yshop: info: base-package: co.yixiang.yshop.module ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/test/resources/logback.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/test/resources/sql/clean.sql ================================================ DELETE FROM "member_user"; DELETE FROM "member_address"; ================================================ FILE: yshop-drink-boot3/yshop-module-member/yshop-module-member-biz/src/test/resources/sql/create_tables.sql ================================================ CREATE TABLE IF NOT EXISTS "member_user" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号', "nickname" varchar(30) NOT NULL DEFAULT '' COMMENT '用户昵称', "avatar" varchar(255) NOT NULL DEFAULT '' COMMENT '头像', "status" tinyint NOT NULL COMMENT '状态', "mobile" varchar(11) NOT NULL COMMENT '手机号', "password" varchar(100) NOT NULL DEFAULT '' COMMENT '密码', "register_ip" varchar(32) NOT NULL COMMENT '注册 IP', "login_ip" varchar(50) NULL DEFAULT '' COMMENT '最后登录IP', "login_date" datetime NULL DEFAULT NULL COMMENT '最后登录时间', "creator" varchar(64) NULL DEFAULT '' COMMENT '创建者', "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', "updater" varchar(64) NULL DEFAULT '' COMMENT '更新者', "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', "deleted" bit(1) NOT NULL DEFAULT '0' COMMENT '是否删除', "tenant_id" bigint not null default '0', PRIMARY KEY ("id") ) COMMENT '会员表'; CREATE TABLE IF NOT EXISTS "member_address" ( "id" bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY, "user_id" bigint(20) NOT NULL, "name" varchar(10) NOT NULL, "mobile" varchar(20) NOT NULL, "area_id" bigint(20) NOT NULL, "post_code" varchar(16) NOT NULL, "detail_address" varchar(250) NOT NULL, "defaulted" bit NOT NULL, "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, "creator" varchar(64) DEFAULT '', "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, "updater" varchar(64) DEFAULT '', PRIMARY KEY ("id") ) COMMENT '用户收件地址'; ================================================ FILE: yshop-drink-boot3/yshop-module-message/pom.xml ================================================ co.yixiang.boot yshop ${revision} 4.0.0 yshop-module-message pom yshop-module-message-api yshop-module-message-biz ${project.artifactId} message 模块 ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-api/pom.xml ================================================ yshop-module-message co.yixiang.boot ${revision} 4.0.0 yshop-module-message-api jar ${project.artifactId} message 模块 API,暴露给其它模块调用 co.yixiang.boot yshop-common org.springframework.boot spring-boot-starter-validation true co.yixiang.boot yshop-spring-boot-starter-mybatis ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-api/src/main/java/co/yixiang/yshop/module/message/enums/ErrorCodeConstants.java ================================================ package co.yixiang.yshop.module.message.enums; import co.yixiang.yshop.framework.common.exception.ErrorCode; public interface ErrorCodeConstants { ErrorCode WECHAT_TEMPLATE_NOT_EXISTS = new ErrorCode(1008011000, "微信模板不存在"); } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-api/src/main/java/co/yixiang/yshop/module/message/enums/WechatTempateEnum.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co * 注意: * 本软件为www.yixiang.co开发研制,未经购买不得使用 * 购买后可获得全部源代码(禁止转卖、分享、上传到码云、github等开源平台) * 一经发现盗用、分享等行为,将追究法律责任,后果自负 */ package co.yixiang.yshop.module.message.enums; import lombok.AllArgsConstructor; import lombok.Getter; /** * @author hupeng * 微信公众号模板枚举 */ @Getter @AllArgsConstructor public enum WechatTempateEnum { PAY_SUCCESS("pay_success","支付成功通知"), DELIVERY_SUCCESS("delivery_success","发货成功通知"), REFUND_SUCCESS("refund_success","退款成功通知"), RECHARGE_SUCCESS("recharge_success","充值成功通知"), TEMPLATES("template","公众号模板消息"), SUBSCRIBE("subscribe","小程序订阅消息"); private String value; //模板编号 private String desc; //模板id } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/pom.xml ================================================ yshop-module-message co.yixiang.boot ${revision} 4.0.0 yshop-module-message-biz jar ${project.artifactId} message 模块,模板消息 websocket 推送消息等等 co.yixiang.boot yshop-module-message-api ${revision} co.yixiang.boot yshop-module-member-biz ${revision} co.yixiang.boot yshop-spring-boot-starter-biz-tenant co.yixiang.boot yshop-spring-boot-starter-security co.yixiang.boot yshop-spring-boot-starter-redis co.yixiang.boot yshop-spring-boot-starter-mq co.yixiang.boot yshop-spring-boot-starter-test test co.yixiang.boot yshop-spring-boot-starter-excel ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/controller/admin/wechattemplate/WechatTemplateController.java ================================================ package co.yixiang.yshop.module.message.controller.admin.wechattemplate; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import org.springframework.security.access.prepost.PreAuthorize; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.constraints.*; import jakarta.validation.*; import java.util.*; import java.io.IOException; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.pojo.CommonResult; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.message.controller.admin.wechattemplate.vo.*; import co.yixiang.yshop.module.message.dal.dataobject.wechattemplate.WechatTemplateDO; import co.yixiang.yshop.module.message.convert.wechattemplate.WechatTemplateConvert; import co.yixiang.yshop.module.message.service.wechattemplate.WechatTemplateService; @Tag(name = "管理后台 - 微信模板") @RestController @RequestMapping("/message/wechat-template") @Validated public class WechatTemplateController { @Resource private WechatTemplateService wechatTemplateService; @PostMapping("/create") @Operation(summary = "创建微信模板") @PreAuthorize("@ss.hasPermission('message:wechat-template:create')") public CommonResult createWechatTemplate(@Valid @RequestBody WechatTemplateCreateReqVO createReqVO) { return success(wechatTemplateService.createWechatTemplate(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新微信模板") @PreAuthorize("@ss.hasPermission('message:wechat-template:update')") public CommonResult updateWechatTemplate(@Valid @RequestBody WechatTemplateUpdateReqVO updateReqVO) { wechatTemplateService.updateWechatTemplate(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除微信模板") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('message:wechat-template:delete')") public CommonResult deleteWechatTemplate(@RequestParam("id") Integer id) { wechatTemplateService.deleteWechatTemplate(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得微信模板") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('message:wechat-template:query')") public CommonResult getWechatTemplate(@RequestParam("id") Integer id) { WechatTemplateDO wechatTemplate = wechatTemplateService.getWechatTemplate(id); return success(WechatTemplateConvert.INSTANCE.convert(wechatTemplate)); } @GetMapping("/list") @Operation(summary = "获得微信模板列表") @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") @PreAuthorize("@ss.hasPermission('message:wechat-template:query')") public CommonResult> getWechatTemplateList(@RequestParam("ids") Collection ids) { List list = wechatTemplateService.getWechatTemplateList(ids); return success(WechatTemplateConvert.INSTANCE.convertList(list)); } @GetMapping("/page") @Operation(summary = "获得微信模板分页") @PreAuthorize("@ss.hasPermission('message:wechat-template:query')") public CommonResult> getWechatTemplatePage(@Valid WechatTemplatePageReqVO pageVO) { PageResult pageResult = wechatTemplateService.getWechatTemplatePage(pageVO); return success(WechatTemplateConvert.INSTANCE.convertPage(pageResult)); } @GetMapping("/export-excel") @Operation(summary = "导出微信模板 Excel") @PreAuthorize("@ss.hasPermission('message:wechat-template:export')") public void exportWechatTemplateExcel(@Valid WechatTemplateExportReqVO exportReqVO, HttpServletResponse response) throws IOException { List list = wechatTemplateService.getWechatTemplateList(exportReqVO); // 导出 Excel List datas = WechatTemplateConvert.INSTANCE.convertList02(list); ExcelUtils.write(response, "微信模板.xls", "数据", WechatTemplateExcelVO.class, datas); } } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/controller/admin/wechattemplate/vo/WechatTemplateBaseVO.java ================================================ package co.yixiang.yshop.module.message.controller.admin.wechattemplate.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import jakarta.validation.constraints.*; /** * 微信模板 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class WechatTemplateBaseVO { @Schema(description = "模板编号", required = true) @NotNull(message = "模板编号不能为空") private String tempkey; @Schema(description = "模板名", required = true, example = "张三") @NotNull(message = "模板名不能为空") private String name; @Schema(description = "回复内容", required = true) private String content; @Schema(description = "模板ID", example = "15656") private String tempid; @Schema(description = "状态", required = true, example = "1") @NotNull(message = "状态不能为空") private Byte status; @Schema(description = "类型:template:模板消息 subscribe:订阅消息", example = "1") private String type; } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/controller/admin/wechattemplate/vo/WechatTemplateCreateReqVO.java ================================================ package co.yixiang.yshop.module.message.controller.admin.wechattemplate.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 微信模板创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class WechatTemplateCreateReqVO extends WechatTemplateBaseVO { } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/controller/admin/wechattemplate/vo/WechatTemplateExcelVO.java ================================================ package co.yixiang.yshop.module.message.controller.admin.wechattemplate.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.alibaba.excel.annotation.ExcelProperty; /** * 微信模板 Excel VO * * @author yshop */ @Data public class WechatTemplateExcelVO { @ExcelProperty("模板id") private Integer id; @ExcelProperty("模板编号") private String tempkey; @ExcelProperty("模板名") private String name; @ExcelProperty("回复内容") private String content; @ExcelProperty("模板ID") private String tempid; @ExcelProperty("添加时间") private LocalDateTime createTime; @ExcelProperty("状态") private Byte status; @ExcelProperty("类型:template:模板消息 subscribe:订阅消息") private String type; } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/controller/admin/wechattemplate/vo/WechatTemplateExportReqVO.java ================================================ package co.yixiang.yshop.module.message.controller.admin.wechattemplate.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import java.time.LocalDateTime; import org.springframework.format.annotation.DateTimeFormat; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 微信模板 Excel 导出 Request VO,参数和 WechatTemplatePageReqVO 是一致的") @Data public class WechatTemplateExportReqVO { @Schema(description = "模板编号") private String tempkey; @Schema(description = "模板名", example = "张三") private String name; @Schema(description = "回复内容") private String content; @Schema(description = "模板ID", example = "15656") private String tempid; @Schema(description = "添加时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; @Schema(description = "状态", example = "1") private Byte status; @Schema(description = "类型:template:模板消息 subscribe:订阅消息", example = "1") private String type; } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/controller/admin/wechattemplate/vo/WechatTemplatePageReqVO.java ================================================ package co.yixiang.yshop.module.message.controller.admin.wechattemplate.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 微信模板分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class WechatTemplatePageReqVO extends PageParam { @Schema(description = "模板编号") private String tempkey; @Schema(description = "模板名", example = "张三") private String name; @Schema(description = "回复内容") private String content; @Schema(description = "模板ID", example = "15656") private String tempid; @Schema(description = "添加时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; @Schema(description = "状态", example = "1") private Byte status; @Schema(description = "类型:template:模板消息 subscribe:订阅消息", example = "1") private String type; } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/controller/admin/wechattemplate/vo/WechatTemplateRespVO.java ================================================ package co.yixiang.yshop.module.message.controller.admin.wechattemplate.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; @Schema(description = "管理后台 - 微信模板 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class WechatTemplateRespVO extends WechatTemplateBaseVO { @Schema(description = "模板id", required = true, example = "8445") private Integer id; @Schema(description = "添加时间", required = true) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/controller/admin/wechattemplate/vo/WechatTemplateUpdateReqVO.java ================================================ package co.yixiang.yshop.module.message.controller.admin.wechattemplate.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 微信模板更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class WechatTemplateUpdateReqVO extends WechatTemplateBaseVO { @Schema(description = "模板id", required = true, example = "8445") @NotNull(message = "模板id不能为空") private Integer id; } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/convert/wechattemplate/WechatTemplateConvert.java ================================================ package co.yixiang.yshop.module.message.convert.wechattemplate; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import co.yixiang.yshop.module.message.controller.admin.wechattemplate.vo.*; import co.yixiang.yshop.module.message.dal.dataobject.wechattemplate.WechatTemplateDO; /** * 微信模板 Convert * * @author yshop */ @Mapper public interface WechatTemplateConvert { WechatTemplateConvert INSTANCE = Mappers.getMapper(WechatTemplateConvert.class); WechatTemplateDO convert(WechatTemplateCreateReqVO bean); WechatTemplateDO convert(WechatTemplateUpdateReqVO bean); WechatTemplateRespVO convert(WechatTemplateDO bean); List convertList(List list); PageResult convertPage(PageResult page); List convertList02(List list); } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/dal/dataobject/wechattemplate/WechatTemplateDO.java ================================================ package co.yixiang.yshop.module.message.dal.dataobject.wechattemplate; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; /** * 微信模板 DO * * @author yshop */ @TableName("yshop_wechat_template") @KeySequence("yshop_wechat_template_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class WechatTemplateDO extends BaseDO { /** * 模板id */ @TableId private Integer id; /** * 模板编号 */ private String tempkey; /** * 模板名 */ private String name; /** * 回复内容 */ private String content; /** * 模板ID */ private String tempid; /** * 状态 */ private Byte status; /** * 类型:template:模板消息 subscribe:订阅消息 */ private String type; } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/dal/mysql/wechattemplate/WechatTemplateMapper.java ================================================ package co.yixiang.yshop.module.message.dal.mysql.wechattemplate; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.message.dal.dataobject.wechattemplate.WechatTemplateDO; import org.apache.ibatis.annotations.Mapper; import co.yixiang.yshop.module.message.controller.admin.wechattemplate.vo.*; /** * 微信模板 Mapper * * @author yshop */ @Mapper public interface WechatTemplateMapper extends BaseMapperX { default PageResult selectPage(WechatTemplatePageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(WechatTemplateDO::getTempkey, reqVO.getTempkey()) .likeIfPresent(WechatTemplateDO::getName, reqVO.getName()) .eqIfPresent(WechatTemplateDO::getContent, reqVO.getContent()) .eqIfPresent(WechatTemplateDO::getTempid, reqVO.getTempid()) .betweenIfPresent(WechatTemplateDO::getCreateTime, reqVO.getCreateTime()) .eqIfPresent(WechatTemplateDO::getStatus, reqVO.getStatus()) .eqIfPresent(WechatTemplateDO::getType, reqVO.getType()) .orderByDesc(WechatTemplateDO::getId)); } default List selectList(WechatTemplateExportReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .eqIfPresent(WechatTemplateDO::getTempkey, reqVO.getTempkey()) .likeIfPresent(WechatTemplateDO::getName, reqVO.getName()) .eqIfPresent(WechatTemplateDO::getContent, reqVO.getContent()) .eqIfPresent(WechatTemplateDO::getTempid, reqVO.getTempid()) .betweenIfPresent(WechatTemplateDO::getCreateTime, reqVO.getCreateTime()) .eqIfPresent(WechatTemplateDO::getStatus, reqVO.getStatus()) .eqIfPresent(WechatTemplateDO::getType, reqVO.getType()) .orderByDesc(WechatTemplateDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/mq/consumer/WeixinNoticeConsumer.java ================================================ package co.yixiang.yshop.module.message.mq.consumer; import co.yixiang.yshop.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener; import co.yixiang.yshop.module.message.enums.WechatTempateEnum; import co.yixiang.yshop.module.message.mq.message.WeixinNoticeMessage; import co.yixiang.yshop.module.message.supply.WeiXinSubscribeService; import co.yixiang.yshop.module.message.supply.WeixinTemplateService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; /** * 消息队列处理消息推送 */ @Component @Slf4j public class WeixinNoticeConsumer extends AbstractRedisStreamMessageListener { @Resource private WeiXinSubscribeService weiXinSubscribeService; @Resource private WeixinTemplateService weixinTemplateService; @Override public void onMessage(WeixinNoticeMessage message) { log.info("[onMessage][消息内容({})]", message); //公众号 if(WechatTempateEnum.TEMPLATES.getValue().equals(message.getType())) { if(WechatTempateEnum.PAY_SUCCESS.getValue().equals(message.getTempkey())){ weixinTemplateService.paySuccessNotice(message.getOrderId(),message.getPrice(),message.getUid()); }else if(WechatTempateEnum.DELIVERY_SUCCESS.getValue().equals(message.getTempkey())){ weixinTemplateService.deliverySuccessNotice(message.getOrderId(),message.getDeliveryName(), message.getDeliveryId(),message.getUid()); } else if(WechatTempateEnum.REFUND_SUCCESS.getValue().equals(message.getTempkey())){ weixinTemplateService.refundSuccessNotice("您的订单退款申请被通过,钱款将很快还至您的支付账户。", message.getOrderId(),message.getDeliveryName(),message.getUid(),message.getTime()); } }else if(WechatTempateEnum.SUBSCRIBE.getValue().equals(message.getType())){ //小程序 if(WechatTempateEnum.PAY_SUCCESS.getValue().equals(message.getTempkey())){ weiXinSubscribeService.paySuccessNotice(message.getNumberId().toString(),message.getProductName() ,message.getShopName(),message.getUid(),message.getId(),message.getOrderId()); }else if(WechatTempateEnum.DELIVERY_SUCCESS.getValue().equals(message.getTempkey())){ weiXinSubscribeService.deliverySuccessNotice(message.getOrderId(),message.getDeliveryName(), message.getDeliveryId(),message.getUid()); } else if(WechatTempateEnum.REFUND_SUCCESS.getValue().equals(message.getTempkey())){ weiXinSubscribeService.refundSuccessNotice(message.getOrderId(),message.getDeliveryName(), message.getUid(),message.getTime()); } } } } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/mq/message/WeixinNoticeMessage.java ================================================ package co.yixiang.yshop.module.message.mq.message; import co.yixiang.yshop.framework.mq.redis.core.stream.AbstractRedisStreamMessage; import lombok.Data; @Data public class WeixinNoticeMessage extends AbstractRedisStreamMessage { /** * 模板编号 */ private String tempkey; //消息类型 private String type; //订单好 private String orderId; //价格 private String price; //用户 private Long uid; //时间 private String time; // 快递公司 private String deliveryName; // 快递单号 private String deliveryId; //订单ID private Long id; //取餐号 private Integer numberId; //产品名称 private String productName; //门店名称 private String shopName; @Override public String getStreamKey() { return "weixin.msg.notice"; } } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/mq/producer/WeixinNoticeProducer.java ================================================ package co.yixiang.yshop.module.message.mq.producer; import co.yixiang.yshop.framework.mq.redis.core.RedisMQTemplate; import co.yixiang.yshop.module.message.mq.message.WeixinNoticeMessage; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; @Slf4j @Component public class WeixinNoticeProducer { @Resource private RedisMQTemplate redisMQTemplate; /** * 发送消息 * @param tempkey 订单编号 * @param type 类型 */ public void sendNoticeMessage( Long uid,String tempkey,String type,String orderId,String price, String time,String deliveryName,String deliveryId, Long id,Integer numberId,String productName,String shopName) { WeixinNoticeMessage weixinNoticeMessage = new WeixinNoticeMessage() .setTempkey(tempkey).setType(type).setOrderId(orderId).setUid(uid) .setPrice(price).setDeliveryId(deliveryId).setTime(time).setDeliveryName(deliveryName) .setId(id).setNumberId(numberId).setProductName(productName).setShopName(shopName); redisMQTemplate.send(weixinNoticeMessage); } } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/redismq/DelayedQueueListener.java ================================================ package co.yixiang.yshop.module.message.redismq; /** * 延时队列接口 * @author hupeng * @date 2024.6.25 */ public interface DelayedQueueListener { /** * 是否启用 * * @return boolean */ default boolean isEnable() { return true; } /** * 队列键 * * @return String */ String delayedQueueKey(); /** * 消费 * * @param message Object * @throws Exception Exception */ void consume(T message) throws Exception; /** * 发生异常时最终处理 */ default void whenExceptionFinally() { } } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/redismq/DelayedQueueListenerConfigurer.java ================================================ package co.yixiang.yshop.module.message.redismq; import com.google.common.util.concurrent.ThreadFactoryBuilder; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import java.util.List; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * 配置 * @author hupeng * @date 2024.6.25 */ @Component public class DelayedQueueListenerConfigurer implements InitializingBean, DisposableBean { private ThreadPoolExecutor delayedThreadPoolExecutor; private final List> delayedQueueListenerList; private final RedissonClient redissonClient; public DelayedQueueListenerConfigurer(List> delayedQueueListenerList, RedissonClient redissonClient) { this.delayedQueueListenerList = delayedQueueListenerList; this.redissonClient = redissonClient; } @Override public void destroy() throws Exception { if (delayedThreadPoolExecutor != null) { delayedThreadPoolExecutor.shutdownNow(); } } @Override public void afterPropertiesSet() throws Exception { Assert.notEmpty(delayedQueueListenerList, "delayedQueueListenerList must not be empty!"); ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("delayed-queue-pool-%d").build(); int numberOfJob = delayedQueueListenerList.size(); delayedThreadPoolExecutor = new ThreadPoolExecutor( numberOfJob, numberOfJob, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(numberOfJob), namedThreadFactory ); delayedQueueListenerList.forEach(delayedQueueListener -> delayedThreadPoolExecutor.execute(new DelayedQueuePollTask<>(redissonClient, delayedQueueListener))); } } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/redismq/DelayedQueuePollTask.java ================================================ package co.yixiang.yshop.module.message.redismq; import lombok.extern.slf4j.Slf4j; import org.redisson.api.RBlockingDeque; import org.redisson.api.RedissonClient; /** * 延迟队列轮询任务类 * @author hupeng * @date 2024.6.25 */ @Slf4j public class DelayedQueuePollTask implements Runnable{ private final RedissonClient redissonClient; private final DelayedQueueListener delayedQueueListener; public DelayedQueuePollTask(RedissonClient redissonClient, DelayedQueueListener delayedQueueListener) { this.redissonClient = redissonClient; this.delayedQueueListener = delayedQueueListener; } @Override public void run() { String threadName = "delayed-queue-listener-" + delayedQueueListener.getClass().getSimpleName(); Thread.currentThread().setName(threadName); if (!delayedQueueListener.isEnable()) { return; } RBlockingDeque blockingDeque = redissonClient.getBlockingDeque(delayedQueueListener.delayedQueueKey()); // 解决消息丢失问题,发送subscribe命令订阅redis队列 redissonClient.getDelayedQueue(blockingDeque); while (!Thread.currentThread().isInterrupted()) { try { T message = blockingDeque.take(); delayedQueueListener.consume(message); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (Exception e) { log.error(e.getMessage(), e); } finally { delayedQueueListener.whenExceptionFinally(); } } } } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/redismq/msg/OrderMsg.java ================================================ package co.yixiang.yshop.module.message.redismq.msg; import lombok.Builder; import lombok.Data; import java.io.Serializable; @Data @Builder public class OrderMsg implements Serializable { private String orderId; } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/service/wechattemplate/WechatTemplateService.java ================================================ package co.yixiang.yshop.module.message.service.wechattemplate; import java.util.*; import jakarta.validation.*; import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO; import co.yixiang.yshop.module.message.controller.admin.wechattemplate.vo.*; import co.yixiang.yshop.module.message.dal.dataobject.wechattemplate.WechatTemplateDO; import co.yixiang.yshop.framework.common.pojo.PageResult; import com.baomidou.mybatisplus.extension.service.IService; /** * 微信模板 Service 接口 * * @author yshop */ public interface WechatTemplateService extends IService { /** * 创建微信模板 * * @param createReqVO 创建信息 * @return 编号 */ Integer createWechatTemplate(@Valid WechatTemplateCreateReqVO createReqVO); /** * 更新微信模板 * * @param updateReqVO 更新信息 */ void updateWechatTemplate(@Valid WechatTemplateUpdateReqVO updateReqVO); /** * 删除微信模板 * * @param id 编号 */ void deleteWechatTemplate(Integer id); /** * 获得微信模板 * * @param id 编号 * @return 微信模板 */ WechatTemplateDO getWechatTemplate(Integer id); /** * 获得微信模板列表 * * @param ids 编号 * @return 微信模板列表 */ List getWechatTemplateList(Collection ids); /** * 获得微信模板分页 * * @param pageReqVO 分页查询 * @return 微信模板分页 */ PageResult getWechatTemplatePage(WechatTemplatePageReqVO pageReqVO); /** * 获得微信模板列表, 用于 Excel 导出 * * @param exportReqVO 查询条件 * @return 微信模板列表 */ List getWechatTemplateList(WechatTemplateExportReqVO exportReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/service/wechattemplate/WechatTemplateServiceImpl.java ================================================ package co.yixiang.yshop.module.message.service.wechattemplate; import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO; import co.yixiang.yshop.module.member.dal.mysql.user.MemberUserMapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import java.util.*; import co.yixiang.yshop.module.message.controller.admin.wechattemplate.vo.*; import co.yixiang.yshop.module.message.dal.dataobject.wechattemplate.WechatTemplateDO; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.message.convert.wechattemplate.WechatTemplateConvert; import co.yixiang.yshop.module.message.dal.mysql.wechattemplate.WechatTemplateMapper; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.message.enums.ErrorCodeConstants.*; /** * 微信模板 Service 实现类 * * @author yshop */ @Service @Validated public class WechatTemplateServiceImpl extends ServiceImpl implements WechatTemplateService { @Resource private WechatTemplateMapper wechatTemplateMapper; @Override public Integer createWechatTemplate(WechatTemplateCreateReqVO createReqVO) { // 插入 WechatTemplateDO wechatTemplate = WechatTemplateConvert.INSTANCE.convert(createReqVO); wechatTemplateMapper.insert(wechatTemplate); // 返回 return wechatTemplate.getId(); } @Override public void updateWechatTemplate(WechatTemplateUpdateReqVO updateReqVO) { // 校验存在 validateWechatTemplateExists(updateReqVO.getId()); // 更新 WechatTemplateDO updateObj = WechatTemplateConvert.INSTANCE.convert(updateReqVO); wechatTemplateMapper.updateById(updateObj); } @Override public void deleteWechatTemplate(Integer id) { // 校验存在 validateWechatTemplateExists(id); // 删除 wechatTemplateMapper.deleteById(id); } private void validateWechatTemplateExists(Integer id) { if (wechatTemplateMapper.selectById(id) == null) { throw exception(WECHAT_TEMPLATE_NOT_EXISTS); } } @Override public WechatTemplateDO getWechatTemplate(Integer id) { return wechatTemplateMapper.selectById(id); } @Override public List getWechatTemplateList(Collection ids) { return wechatTemplateMapper.selectBatchIds(ids); } @Override public PageResult getWechatTemplatePage(WechatTemplatePageReqVO pageReqVO) { return wechatTemplateMapper.selectPage(pageReqVO); } @Override public List getWechatTemplateList(WechatTemplateExportReqVO exportReqVO) { return wechatTemplateMapper.selectList(exportReqVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/supply/WeiXinSubscribeService.java ================================================ package co.yixiang.yshop.module.message.supply; import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.bean.WxMaSubscribeMessage; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.constant.ShopConstants; import co.yixiang.yshop.framework.common.enums.ShopCommonEnum; import co.yixiang.yshop.framework.common.exception.ErrorCode; import co.yixiang.yshop.framework.tenant.core.aop.TenantIgnore; import co.yixiang.yshop.framework.tenant.core.util.TenantUtils; import co.yixiang.yshop.module.member.api.user.dto.WechatUserDto; import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO; import co.yixiang.yshop.module.member.service.user.MemberUserService; import co.yixiang.yshop.module.message.dal.dataobject.wechattemplate.WechatTemplateDO; import co.yixiang.yshop.module.message.enums.WechatTempateEnum; import co.yixiang.yshop.module.message.service.wechattemplate.WechatTemplateService; import me.chanjar.weixin.common.error.WxErrorException; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Map; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.USER_USERNAME_EXISTS; ; /** * 小程序订阅消息通知 */ @Service public class WeiXinSubscribeService { @Resource private MemberUserService userService; @Resource private WechatTemplateService wechatTemplateService; @Resource private WxMaService wxMaService; /** * 充值成功通知 * @param time 时间 * @param price 金额 * @param uid uid */ public void rechargeSuccessNotice(String time,String price,Long uid){ String openid = this.getUserOpenid(uid); if(StrUtil.isBlank(openid)) { return; } Map map = new HashMap<>(); map.put("first","您的账户金币发生变动,详情如下:"); map.put("keyword1","充值"); map.put("keyword2",time); map.put("keyword3",price); map.put("remark", ShopConstants.YSHOP_WECHAT_PUSH_REMARK); String tempId = this.getTempId(WechatTempateEnum.RECHARGE_SUCCESS.getValue()); if(StrUtil.isNotBlank(tempId)) { this.sendSubscribeMsg( openid, tempId, "/user/account",map); } } /** * 支付成功通知 * @param numberId 取餐号 * @param productName 商品名称 * @param shopName 门店名称 * @param uid uid * @param id 订单ID */ public void paySuccessNotice(String numberId,String productName,String shopName,Long uid,Long id,String orderId){ TenantUtils.executeIgnore(() -> { String openid = this.getUserOpenid(uid); if(StrUtil.isBlank(openid)) { return; } SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss"); Map map = new HashMap<>(); //取餐号 map.put("character_string1",numberId); //商品名 map.put("thing2", productName); map.put("time3",simpleDateFormat.format(new Date())); //下单门店 map.put("thing4",shopName); map.put("thing5","意向点餐系统为您服务!"); String tempId = this.getTempId(WechatTempateEnum.PAY_SUCCESS.getValue()); if(StrUtil.isNotBlank(tempId)) { this.sendSubscribeMsg( openid,tempId, "/pages/components/pages/orders/detail?id="+orderId,map); } }); } /** * 退款成功通知 * @param orderId 订单号 * @param price 金额 * @param uid uid * @param time 时间 */ public void refundSuccessNotice(String orderId,String price,Long uid,String time){ TenantUtils.executeIgnore(() -> { String openid = this.getUserOpenid(uid); if(StrUtil.isBlank(openid)) { return; } Map map = new HashMap<>(); map.put("first","您的订单退款申请被通过,钱款将很快还至您的支付账户。"); //订单号 map.put("keyword1",orderId); map.put("keyword2",price); map.put("keyword3", time); map.put("remark",ShopConstants.YSHOP_WECHAT_PUSH_REMARK); String tempId = this.getTempId(WechatTempateEnum.REFUND_SUCCESS.getValue()); if(StrUtil.isNotBlank(tempId)) { this.sendSubscribeMsg( openid,tempId, "/order/detail/"+orderId,map); } }); } /** * 发货成功通知 * @param orderId 单号 * @param deliveryName 快递公司 * @param deliveryId 快递单号 * @param uid uid */ public void deliverySuccessNotice(String orderId,String deliveryName, String deliveryId,Long uid){ TenantUtils.executeIgnore(() -> { String openid = this.getUserOpenid(uid); if(StrUtil.isEmpty(openid)) { return; } Map map = new HashMap<>(); map.put("first","亲,宝贝已经启程了,好想快点来到你身边。"); map.put("keyword2",deliveryName); map.put("keyword1",orderId); map.put("keyword3",deliveryId); map.put("remark",ShopConstants.YSHOP_WECHAT_PUSH_REMARK); String tempId = this.getTempId(WechatTempateEnum.DELIVERY_SUCCESS.getValue()); if(StrUtil.isNotBlank(tempId)) { this.sendSubscribeMsg( openid,tempId, "/order/detail/"+orderId,map); } }); } /** * 构建小程序一次性订阅消息 * @param openId 单号 * @param templateId 模板id * @param page 跳转页面 * @param map map内容 * @return String */ private void sendSubscribeMsg(String openId, String templateId, String page, Map map){ WxMaSubscribeMessage wxMaSubscribeMessage = WxMaSubscribeMessage.builder() .toUser(openId) .templateId(templateId) .page(page) .build(); map.forEach( (k,v)-> { wxMaSubscribeMessage.addData(new WxMaSubscribeMessage.MsgData(k, v));} ); // WxMaService wxMaService = WxMaConfiguration.getWxMaService(); try { wxMaService.getMsgService().sendSubscribeMsg(wxMaSubscribeMessage); } catch (WxErrorException e) { e.printStackTrace(); } } /** * 获取模板消息id * @param key 模板key * @return string */ @TenantIgnore private String getTempId(String key){ WechatTemplateDO yxWechatTemplate = wechatTemplateService.lambdaQuery() .eq(WechatTemplateDO::getType,"subscribe") .eq(WechatTemplateDO::getTempkey,key) .one(); if (yxWechatTemplate == null) { throw exception(new ErrorCode(9999999,"请后台配置key:" + key + "订阅消息id")); } if(ShopCommonEnum.IS_STATUS_0.getValue().equals(yxWechatTemplate.getStatus())){ return ""; } return yxWechatTemplate.getTempid(); } /** * 获取openid * @param uid uid * @return String */ @TenantIgnore private String getUserOpenid(Long uid){ MemberUserDO yxUser = userService.getById(uid); if(yxUser == null) { return ""; } if(StrUtil.isBlank(yxUser.getRoutineOpenid())) { return ""; } return yxUser.getRoutineOpenid(); } } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/java/co/yixiang/yshop/module/message/supply/WeixinTemplateService.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co * 注意: * 本软件为www.yixiang.co开发研制,未经购买不得使用 * 购买后可获得全部源代码(禁止转卖、分享、上传到码云、github等开源平台) * 一经发现盗用、分享等行为,将追究法律责任,后果自负 */ package co.yixiang.yshop.module.message.supply; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.constant.ShopConstants; import co.yixiang.yshop.framework.common.enums.ShopCommonEnum; import co.yixiang.yshop.framework.common.exception.ErrorCode; import co.yixiang.yshop.module.member.api.user.dto.WechatUserDto; import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO; import co.yixiang.yshop.module.member.service.user.MemberUserService; import co.yixiang.yshop.module.message.dal.dataobject.wechattemplate.WechatTemplateDO; import co.yixiang.yshop.module.message.enums.WechatTempateEnum; import co.yixiang.yshop.module.message.service.wechattemplate.WechatTemplateService; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.template.WxMpTemplateData; import me.chanjar.weixin.mp.bean.template.WxMpTemplateMessage; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import java.util.HashMap; import java.util.Map; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; /** * @ClassName 微信公众号模板通知 * @Author hupeng <610796224@qq.com> * @Date 2020/6/27 **/ @Slf4j @Service public class WeixinTemplateService { @Resource private MemberUserService userService; @Resource private WechatTemplateService wechatTemplateService; @Resource private WxMpService wxMpService; /** * 充值成功通知 * @param time 时间 * @param price 金额 * @param uid uid */ public void rechargeSuccessNotice(String time,String price,Long uid){ String openid = this.getUserOpenid(uid); if(StrUtil.isBlank(openid)) { return; } Map map = new HashMap<>(); map.put("first","您的账户金币发生变动,详情如下:"); map.put("keyword1","充值"); map.put("keyword2",time); map.put("keyword3",price); map.put("remark", ShopConstants.YSHOP_WECHAT_PUSH_REMARK); String tempId = this.getTempId(WechatTempateEnum.RECHARGE_SUCCESS.getValue()); if(StrUtil.isNotBlank(tempId)) { this.sendWxMpTemplateMessage( openid, tempId, "https://www.yixiang.co",map); } } /** * 支付成功通知 * @param orderId 订单号 * @param price 金额 * @param uid uid */ public void paySuccessNotice(String orderId,String price,Long uid){ String openid = this.getUserOpenid(uid); if(StrUtil.isBlank(openid)) { return; } Map map = new HashMap<>(); map.put("first","您的订单已支付成功,我们会尽快为您发货。"); //订单号 map.put("keyword1",orderId); map.put("keyword2",price); map.put("remark",ShopConstants.YSHOP_WECHAT_PUSH_REMARK); String tempId = this.getTempId(WechatTempateEnum.PAY_SUCCESS.getValue()); if(StrUtil.isNotBlank(tempId)) { this.sendWxMpTemplateMessage( openid,tempId, "https://www.yixiang.co",map); } } /** * 退款成功通知 * @param orderId 订单号 * @param price 金额 * @param uid uid * @param time 时间 */ public void refundSuccessNotice(String title,String orderId,String price,Long uid,String time){ String openid = this.getUserOpenid(uid); if(StrUtil.isBlank(openid)) { return; } Map map = new HashMap<>(); map.put("first",title); //订单号 map.put("keyword1",orderId); map.put("keyword2",price); map.put("keyword3", time); map.put("remark",ShopConstants.YSHOP_WECHAT_PUSH_REMARK); String tempId = this.getTempId(WechatTempateEnum.REFUND_SUCCESS.getValue()); if(StrUtil.isNotBlank(tempId)) { this.sendWxMpTemplateMessage( openid,tempId, "https://www.yixiang.co",map); } } /** * 发货成功通知 * @param orderId 单号 * @param deliveryName 快递公司 * @param deliveryId 快递单号 * @param uid uid */ public void deliverySuccessNotice(String orderId,String deliveryName, String deliveryId,Long uid){ String openid = this.getUserOpenid(uid); if(StrUtil.isEmpty(openid)) { return; } Map map = new HashMap<>(); map.put("first","亲,宝贝已经启程了,好想快点来到你身边。"); map.put("keyword2",deliveryName); map.put("keyword1",orderId); map.put("keyword3",deliveryId); map.put("remark",ShopConstants.YSHOP_WECHAT_PUSH_REMARK); String tempId = this.getTempId(WechatTempateEnum.DELIVERY_SUCCESS.getValue()); if(StrUtil.isNotBlank(tempId)) { this.sendWxMpTemplateMessage( openid,tempId, "https://www.yixiang.co",map); } } /** * 构建微信模板通知 * @param openId 单号 * @param templateId 模板id * @param url 跳转url * @param map map内容 * @return String */ private String sendWxMpTemplateMessage(String openId, String templateId, String url, Map map){ WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder() .toUser(openId) .templateId(templateId) .url(url) .build(); map.forEach( (k,v)-> { templateMessage.addData(new WxMpTemplateData(k, v, "#000000"));} ); String msgId = null; try { msgId = wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage); } catch (WxErrorException e) { e.printStackTrace(); } return msgId; } /** * 获取模板消息id * @param key 模板key * @return string */ private String getTempId(String key){ WechatTemplateDO yxWechatTemplate = wechatTemplateService.lambdaQuery() .eq(WechatTemplateDO::getType,"template") .eq(WechatTemplateDO::getTempkey,key) .one(); if (yxWechatTemplate == null) { throw exception(new ErrorCode(9999999,"请后台配置key:" + key + "订阅消息id")); } if(ShopCommonEnum.IS_STATUS_0.getValue().equals(yxWechatTemplate.getStatus())){ return ""; } return yxWechatTemplate.getTempid(); } /** * 获取openid * @param uid uid * @return String */ private String getUserOpenid(Long uid){ MemberUserDO yxUser = userService.getById(uid); if(yxUser == null) { return ""; } WechatUserDto wechatUserDto = yxUser.getWxProfile(); if(wechatUserDto == null) { return ""; } if(StrUtil.isBlank(wechatUserDto.getOpenid())) { return ""; } return wechatUserDto.getOpenid(); } } ================================================ FILE: yshop-drink-boot3/yshop-module-message/yshop-module-message-biz/src/main/resources/mapper/wechattemplate/WechatTemplateMapper.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-mp/pom.xml ================================================ yshop co.yixiang.boot ${revision} 4.0.0 yshop-module-mp pom wechat 模块,主要实现微信平台的相关业务。 例如:微信公众号、企业微信 SCRM 等 yshop-module-mp-api yshop-module-mp-biz ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-api/pom.xml ================================================ yshop-module-mp co.yixiang.boot ${revision} 4.0.0 yshop-module-mp-api jar ${project.artifactId} mp 模块 API,暴露给其它模块调用 co.yixiang.boot yshop-common ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-api/src/main/java/co/yixiang/yshop/module/mp/enums/ErrorCodeConstants.java ================================================ package co.yixiang.yshop.module.mp.enums; import co.yixiang.yshop.framework.common.exception.ErrorCode; /** * Mp 错误码枚举类 * * mp 系统,使用 1-006-000-000 段 */ public interface ErrorCodeConstants { // ========== 公众号账号 1-006-000-000 ============ ErrorCode ACCOUNT_NOT_EXISTS = new ErrorCode(1_006_000_000, "公众号账号不存在"); ErrorCode ACCOUNT_GENERATE_QR_CODE_FAIL = new ErrorCode(1_006_000_001, "生成公众号二维码失败,原因:{}"); ErrorCode ACCOUNT_CLEAR_QUOTA_FAIL = new ErrorCode(1_006_000_002, "清空公众号的 API 配额失败,原因:{}"); // ========== 公众号统计 1-006-001-000 ============ ErrorCode STATISTICS_GET_USER_SUMMARY_FAIL = new ErrorCode(1_006_001_000, "获取粉丝增减数据失败,原因:{}"); ErrorCode STATISTICS_GET_USER_CUMULATE_FAIL = new ErrorCode(1_006_001_001, "获得粉丝累计数据失败,原因:{}"); ErrorCode STATISTICS_GET_UPSTREAM_MESSAGE_FAIL = new ErrorCode(1_006_001_002, "获得消息发送概况数据失败,原因:{}"); ErrorCode STATISTICS_GET_INTERFACE_SUMMARY_FAIL = new ErrorCode(1_006_001_003, "获得接口分析数据失败,原因:{}"); // ========== 公众号标签 1-006-002-000 ============ ErrorCode TAG_NOT_EXISTS = new ErrorCode(1_006_002_000, "标签不存在"); ErrorCode TAG_CREATE_FAIL = new ErrorCode(1_006_002_001, "创建标签失败,原因:{}"); ErrorCode TAG_UPDATE_FAIL = new ErrorCode(1_006_002_002, "更新标签失败,原因:{}"); ErrorCode TAG_DELETE_FAIL = new ErrorCode(1_006_002_003, "删除标签失败,原因:{}"); ErrorCode TAG_GET_FAIL = new ErrorCode(1_006_002_004, "获得标签失败,原因:{}"); // ========== 公众号粉丝 1-006-003-000 ============ ErrorCode USER_NOT_EXISTS = new ErrorCode(1_006_003_000, "粉丝不存在"); ErrorCode USER_UPDATE_TAG_FAIL = new ErrorCode(1_006_003_001, "更新粉丝标签失败,原因:{}"); // ========== 公众号素材 1-006-004-000 ============ ErrorCode MATERIAL_NOT_EXISTS = new ErrorCode(1_006_004_000, "素材不存在"); ErrorCode MATERIAL_UPLOAD_FAIL = new ErrorCode(1_006_004_001, "上传素材失败,原因:{}"); ErrorCode MATERIAL_IMAGE_UPLOAD_FAIL = new ErrorCode(1_006_004_002, "上传图片失败,原因:{}"); ErrorCode MATERIAL_DELETE_FAIL = new ErrorCode(1_006_004_003, "删除素材失败,原因:{}"); // ========== 公众号消息 1-006-005-000 ============ ErrorCode MESSAGE_SEND_FAIL = new ErrorCode(1_006_005_000, "发送消息失败,原因:{}"); // ========== 公众号发布能力 1-006-006-000 ============ ErrorCode FREE_PUBLISH_LIST_FAIL = new ErrorCode(1_006_006_000, "获得已成功发布列表失败,原因:{}"); ErrorCode FREE_PUBLISH_SUBMIT_FAIL = new ErrorCode(1_006_006_001, "提交发布失败,原因:{}"); ErrorCode FREE_PUBLISH_DELETE_FAIL = new ErrorCode(1_006_006_002, "删除发布失败,原因:{}"); // ========== 公众号草稿 1-006-007-000 ============ ErrorCode DRAFT_LIST_FAIL = new ErrorCode(1_006_007_000, "获得草稿列表失败,原因:{}"); ErrorCode DRAFT_CREATE_FAIL = new ErrorCode(1_006_007_001, "创建草稿失败,原因:{}"); ErrorCode DRAFT_UPDATE_FAIL = new ErrorCode(1_006_007_002, "更新草稿失败,原因:{}"); ErrorCode DRAFT_DELETE_FAIL = new ErrorCode(1_006_007_003, "删除草稿失败,原因:{}"); // ========== 公众号菜单 1-006-008-000 ============ ErrorCode MENU_SAVE_FAIL = new ErrorCode(1_006_008_000, "创建菜单失败,原因:{}"); ErrorCode MENU_DELETE_FAIL = new ErrorCode(1_006_008_001, "删除菜单失败,原因:{}"); // ========== 公众号自动回复 1-006-009-000 ============ ErrorCode AUTO_REPLY_NOT_EXISTS = new ErrorCode(1_006_009_000, "自动回复不存在"); ErrorCode AUTO_REPLY_ADD_SUBSCRIBE_FAIL_EXISTS = new ErrorCode(1_006_009_001, "操作失败,原因:已存在关注时的回复"); ErrorCode AUTO_REPLY_ADD_MESSAGE_FAIL_EXISTS = new ErrorCode(1_006_009_002, "操作失败,原因:已存在该消息类型的回复"); ErrorCode AUTO_REPLY_ADD_KEYWORD_FAIL_EXISTS = new ErrorCode(1_006_009_003, "操作失败,原因:已关在该关键字的回复"); } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-api/src/main/java/co/yixiang/yshop/module/mp/enums/MpAccountEnum.java ================================================ package co.yixiang.yshop.module.mp.enums; import lombok.AllArgsConstructor; import lombok.Getter; /** * 微信账户枚举 * @author yshop */ @Getter @AllArgsConstructor public enum MpAccountEnum { IS_MAIN_0(0, " 不是主账户"), IS_MAIN_1(1, "是主账户"), IS_MINI_0(0, "不是小程序"), IS_MINI_1(1, "是小程序"); /** * 匹配 */ private final Integer value; /** * 匹配的名字 */ private final String name; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-api/src/main/java/co/yixiang/yshop/module/mp/enums/message/MpAutoReplyMatchEnum.java ================================================ package co.yixiang.yshop.module.mp.enums.message; import lombok.AllArgsConstructor; import lombok.Getter; /** * 公众号消息自动回复的匹配模式 * * @author yshop */ @Getter @AllArgsConstructor public enum MpAutoReplyMatchEnum { ALL(1, "完全匹配"), LIKE(2, "半匹配"), ; /** * 匹配 */ private final Integer match; /** * 匹配的名字 */ private final String name; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-api/src/main/java/co/yixiang/yshop/module/mp/enums/message/MpAutoReplyTypeEnum.java ================================================ package co.yixiang.yshop.module.mp.enums.message; import lombok.AllArgsConstructor; import lombok.Getter; /** * 公众号消息自动回复的类型 * * @author yshop */ @Getter @AllArgsConstructor public enum MpAutoReplyTypeEnum { SUBSCRIBE(1, "关注时回复"), MESSAGE(2, "收到消息回复"), KEYWORD(3, "关键词回复"), ; /** * 来源 */ private final Integer type; /** * 类型的名字 */ private final String name; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-api/src/main/java/co/yixiang/yshop/module/mp/enums/message/MpMessageSendFromEnum.java ================================================ package co.yixiang.yshop.module.mp.enums.message; import lombok.AllArgsConstructor; import lombok.Getter; /** * 微信公众号消息的发送来源 * * @author yshop */ @Getter @AllArgsConstructor public enum MpMessageSendFromEnum { USER_TO_MP(1, "粉丝发送给公众号"), MP_TO_USER(2, "公众号发给粉丝"), ; /** * 来源 */ private final Integer from; /** * 来源的名字 */ private final String name; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/pom.xml ================================================ yshop-module-mp co.yixiang.boot ${revision} 4.0.0 yshop-module-mp-biz jar ${project.artifactId} mp 模块,我们放微信微信公众号。 例如说:提供微信公众号的账号、菜单、粉丝、标签、消息、自动回复、素材、模板通知、运营数据等功能 co.yixiang.boot yshop-module-mp-api ${revision} co.yixiang.boot yshop-module-system-api ${revision} co.yixiang.boot yshop-module-infra-api ${revision} co.yixiang.boot yshop-spring-boot-starter-biz-tenant co.yixiang.boot yshop-spring-boot-starter-security org.springframework.boot spring-boot-starter-validation co.yixiang.boot yshop-spring-boot-starter-mybatis co.yixiang.boot yshop-spring-boot-starter-redis co.yixiang.boot yshop-spring-boot-starter-mq co.yixiang.boot yshop-spring-boot-starter-test test co.yixiang.boot yshop-spring-boot-starter-excel com.github.binarywang wx-java-mp-spring-boot-starter com.github.binarywang wx-java-miniapp-spring-boot-starter ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/account/MaAccountController.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.account; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.mp.controller.admin.account.vo.MpAccountCreateReqVO; import co.yixiang.yshop.module.mp.controller.admin.account.vo.MpAccountPageReqVO; import co.yixiang.yshop.module.mp.controller.admin.account.vo.MpAccountRespVO; import co.yixiang.yshop.module.mp.controller.admin.account.vo.MpAccountUpdateReqVO; import co.yixiang.yshop.module.mp.convert.account.MpAccountConvert; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import co.yixiang.yshop.module.mp.enums.MpAccountEnum; import co.yixiang.yshop.module.mp.service.account.MpAccountService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 小程序账号") @RestController @RequestMapping("/ma/account") @Validated public class MaAccountController { @Resource private MpAccountService mpAccountService; @PostMapping("/create") @Operation(summary = "创建小程序账号") @PreAuthorize("@ss.hasPermission('ma:account:create')") public CommonResult createAccount(@Valid @RequestBody MpAccountCreateReqVO createReqVO) { createReqVO.setIsMiapp(MpAccountEnum.IS_MINI_1.getValue()); return success(mpAccountService.createAccount(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新小程序账号") @PreAuthorize("@ss.hasPermission('ma:account:update')") public CommonResult updateAccount(@Valid @RequestBody MpAccountUpdateReqVO updateReqVO) { mpAccountService.updateAccount(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除小程序账号") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('ma:account:delete')") public CommonResult deleteAccount(@RequestParam("id") Long id) { mpAccountService.deleteAccount(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得小程序账号") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('ma:account:query')") public CommonResult getAccount(@RequestParam("id") Long id) { MpAccountDO wxAccount = mpAccountService.getAccount(id); return success(MpAccountConvert.INSTANCE.convert(wxAccount)); } @GetMapping("/page") @Operation(summary = "获得小程序账号分页") @PreAuthorize("@ss.hasPermission('ma:account:query')") public CommonResult> getAccountPage(@Valid MpAccountPageReqVO pageVO) { pageVO.setIsMini(MpAccountEnum.IS_MAIN_1.getValue()); PageResult pageResult = mpAccountService.getAccountPage(pageVO); return success(MpAccountConvert.INSTANCE.convertPage(pageResult)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/account/MpAccountController.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.account; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.mp.controller.admin.account.vo.*; import co.yixiang.yshop.module.mp.convert.account.MpAccountConvert; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import co.yixiang.yshop.module.mp.enums.MpAccountEnum; import co.yixiang.yshop.module.mp.service.account.MpAccountService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 公众号账号") @RestController @RequestMapping("/mp/account") @Validated public class MpAccountController { @Resource private MpAccountService mpAccountService; @PostMapping("/create") @Operation(summary = "创建公众号账号") @PreAuthorize("@ss.hasPermission('mp:account:create')") public CommonResult createAccount(@Valid @RequestBody MpAccountCreateReqVO createReqVO) { return success(mpAccountService.createAccount(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新公众号账号") @PreAuthorize("@ss.hasPermission('mp:account:update')") public CommonResult updateAccount(@Valid @RequestBody MpAccountUpdateReqVO updateReqVO) { mpAccountService.updateAccount(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除公众号账号") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('mp:account:delete')") public CommonResult deleteAccount(@RequestParam("id") Long id) { mpAccountService.deleteAccount(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得公众号账号") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('mp:account:query')") public CommonResult getAccount(@RequestParam("id") Long id) { MpAccountDO wxAccount = mpAccountService.getAccount(id); return success(MpAccountConvert.INSTANCE.convert(wxAccount)); } @GetMapping("/page") @Operation(summary = "获得公众号账号分页") @PreAuthorize("@ss.hasPermission('mp:account:query')") public CommonResult> getAccountPage(@Valid MpAccountPageReqVO pageVO) { pageVO.setIsMini(MpAccountEnum.IS_MAIN_0.getValue()); PageResult pageResult = mpAccountService.getAccountPage(pageVO); return success(MpAccountConvert.INSTANCE.convertPage(pageResult)); } @GetMapping("/list-all-simple") @Operation(summary = "获取公众号账号精简信息列表") @PreAuthorize("@ss.hasPermission('mp:account:query')") public CommonResult> getSimpleAccounts() { List list = mpAccountService.getAccountList(); return success(MpAccountConvert.INSTANCE.convertList02(list)); } @PutMapping("/generate-qr-code") @Operation(summary = "生成公众号二维码") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('mp:account:qr-code')") public CommonResult generateAccountQrCode(@RequestParam("id") Long id) { mpAccountService.generateAccountQrCode(id); return success(true); } @PutMapping("/clear-quota") @Operation(summary = "清空公众号 API 配额") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('mp:account:clear-quota')") public CommonResult clearAccountQuota(@RequestParam("id") Long id) { mpAccountService.clearAccountQuota(id); return success(true); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/account/vo/MpAccountBaseVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.account.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotEmpty; /** * 公众号账号 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 * * @author fengdan */ @Data public class MpAccountBaseVO { @Schema(description = "公众号名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @NotEmpty(message = "名称不能为空") private String name; @Schema(description = "公众号微信号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshopyuanma") //@NotEmpty(message = "公众号微信号不能为空") private String account; @Schema(description = "公众号 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx5b23ba7a5589ecbb") @NotEmpty(message = "appId 不能为空") private String appId; @Schema(description = "公众号密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "3a7b3b20c537e52e74afd395eb85f61f") @NotEmpty(message = "密钥不能为空") private String appSecret; @Schema(description = "公众号 token", requiredMode = Schema.RequiredMode.REQUIRED, example = "kangdayuzhen") //@NotEmpty(message = "公众号 token 不能为空") private String token; @Schema(description = "加密密钥", example = "gjN+Ksei") private String aesKey; @Schema(description = "备注", example = "请关注yshop,学习技术") private String remark; /** * 设置主账户 */ private Integer isMain; private Integer isMiapp; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/account/vo/MpAccountCreateReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.account.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @Schema(description = "管理后台 - 公众号账号创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MpAccountCreateReqVO extends MpAccountBaseVO { } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/account/vo/MpAccountPageReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.account.vo; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @Schema(description = "管理后台 - 公众号账号分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MpAccountPageReqVO extends PageParam { @Schema(name = "公众号名称", description = "模糊匹配") private String name; @Schema(name = "公众号账号", description = "模糊匹配") private String account; @Schema(name = "公众号 appid", description = "模糊匹配") private String appId; @Schema(name = "是否是小程序", description = "") private Integer isMini; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/account/vo/MpAccountRespVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.account.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import java.time.LocalDateTime; @Schema(description = "管理后台 - 公众号账号 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MpAccountRespVO extends MpAccountBaseVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "二维码图片URL", example = "https://www.yixiang.co/1024.png") private String qrCodeUrl; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/account/vo/MpAccountSimpleRespVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.account.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "管理后台 - 公众号账号精简信息 Response VO") @Data public class MpAccountSimpleRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "公众号名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String name; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.account.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 公众号账号更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MpAccountUpdateReqVO extends MpAccountBaseVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "编号不能为空") private Long id; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/material/MpMaterialController.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.material; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.mp.controller.admin.material.vo.*; import co.yixiang.yshop.module.mp.convert.material.MpMaterialConvert; import co.yixiang.yshop.module.mp.dal.dataobject.material.MpMaterialDO; import co.yixiang.yshop.module.mp.service.material.MpMaterialService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import java.io.IOException; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 公众号素材") @RestController @RequestMapping("/mp/material") @Validated public class MpMaterialController { @Resource private MpMaterialService mpMaterialService; @Operation(summary = "上传临时素材") @PostMapping("/upload-temporary") @PreAuthorize("@ss.hasPermission('mp:material:upload-temporary')") public CommonResult uploadTemporaryMaterial( @Valid MpMaterialUploadTemporaryReqVO reqVO) throws IOException { MpMaterialDO material = mpMaterialService.uploadTemporaryMaterial(reqVO); return success(MpMaterialConvert.INSTANCE.convert(material)); } @Operation(summary = "上传永久素材") @PostMapping("/upload-permanent") @PreAuthorize("@ss.hasPermission('mp:material:upload-permanent')") public CommonResult uploadPermanentMaterial( @Valid MpMaterialUploadPermanentReqVO reqVO) throws IOException { MpMaterialDO material = mpMaterialService.uploadPermanentMaterial(reqVO); return success(MpMaterialConvert.INSTANCE.convert(material)); } @Operation(summary = "删除素材") @DeleteMapping("/delete-permanent") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('mp:material:delete')") public CommonResult deleteMaterial(@RequestParam("id") Long id) { mpMaterialService.deleteMaterial(id); return success(true); } @Operation(summary = "上传图文内容中的图片") @PostMapping("/upload-news-image") @PreAuthorize("@ss.hasPermission('mp:material:upload-news-image')") public CommonResult uploadNewsImage(@Valid MpMaterialUploadNewsImageReqVO reqVO) throws IOException { return success(mpMaterialService.uploadNewsImage(reqVO)); } @Operation(summary = "获得素材分页") @GetMapping("/page") @PreAuthorize("@ss.hasPermission('mp:material:query')") public CommonResult> getMaterialPage(@Valid MpMaterialPageReqVO pageReqVO) { PageResult pageResult = mpMaterialService.getMaterialPage(pageReqVO); return success(MpMaterialConvert.INSTANCE.convertPage(pageResult)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/material/vo/MpMaterialPageReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.material.vo; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 公众号素材的分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MpMaterialPageReqVO extends PageParam { @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") @NotNull(message = "公众号账号的编号不能为空") private Long accountId; @Schema(description = "是否永久", example = "true") private Boolean permanent; @Schema(description = "文件类型 参见 WxConsts.MediaFileType 枚举", example = "image") private String type; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/material/vo/MpMaterialRespVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.material.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 公众号素材 Response VO") @Data public class MpMaterialRespVO { @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long accountId; @Schema(description = "公众号账号的 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890") private String appId; @Schema(description = "素材的 media_id", requiredMode = Schema.RequiredMode.REQUIRED, example = "123") private String mediaId; @Schema(description = "文件类型 参见 WxConsts.MediaFileType 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "image") private String type; @Schema(description = "是否永久 true - 永久;false - 临时", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") private Boolean permanent; @Schema(description = "素材的 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.yixiang.co/1.png") private String url; @Schema(description = "名字", example = "yshop.png") private String name; @Schema(description = "公众号文件 URL 只有【永久素材】使用", example = "https://mmbiz.qpic.cn/xxx.mp3") private String mpUrl; @Schema(description = "视频素材的标题 只有【永久素材】使用", example = "我是标题") private String title; @Schema(description = "视频素材的描述 只有【永久素材】使用", example = "我是介绍") private String introduction; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/material/vo/MpMaterialUploadNewsImageReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.material.vo; import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.springframework.web.multipart.MultipartFile; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 公众号素材上传图文内容中的图片 Request VO") @Data public class MpMaterialUploadNewsImageReqVO { @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") @NotNull(message = "公众号账号的编号不能为空") private Long accountId; @Schema(description = "文件附件", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "文件不能为空") @JsonIgnore // 避免被操作日志,进行序列化,导致报错 private MultipartFile file; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/material/vo/MpMaterialUploadPermanentReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.material.vo; import cn.hutool.core.util.ObjectUtil; import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import me.chanjar.weixin.common.api.WxConsts; import org.springframework.web.multipart.MultipartFile; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 公众号素材上传永久 Request VO") @Data public class MpMaterialUploadPermanentReqVO { @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") @NotNull(message = "公众号账号的编号不能为空") private Long accountId; @Schema(description = "文件类型 参见 WxConsts.MediaFileType 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "image") @NotEmpty(message = "文件类型不能为空") private String type; @Schema(description = "文件附件", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "文件不能为空") @JsonIgnore // 避免被操作日志,进行序列化,导致报错 private MultipartFile file; @Schema(description = "名字 如果 name 为空,则使用 file 文件名", example = "wechat.mp") private String name; @Schema(description = "视频素材的标题 文件类型为 video 时,必填", example = "视频素材的标题") private String title; @Schema(description = "视频素材的描述 文件类型为 video 时,必填", example = "视频素材的描述") private String introduction; @AssertTrue(message = "标题不能为空") public boolean isTitleValid() { // 生成场景为管理后台时,必须设置上级菜单,不然生成的菜单 SQL 是无父级菜单的 return ObjectUtil.notEqual(type, WxConsts.MediaFileType.VIDEO) || title != null; } @AssertTrue(message = "描述不能为空") public boolean isIntroductionValid() { // 生成场景为管理后台时,必须设置上级菜单,不然生成的菜单 SQL 是无父级菜单的 return ObjectUtil.notEqual(type, WxConsts.MediaFileType.VIDEO) || introduction != null; } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/material/vo/MpMaterialUploadRespVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.material.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "管理后台 - 公众号素材上传结果 Response VO") @Data public class MpMaterialUploadRespVO { @Schema(description = "素材的 media_id", requiredMode = Schema.RequiredMode.REQUIRED, example = "123") private String mediaId; @Schema(description = "素材的 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.yixiang.co/1.png") private String url; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/material/vo/MpMaterialUploadTemporaryReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.material.vo; import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.springframework.web.multipart.MultipartFile; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 公众号素材上传临时 Request VO") @Data public class MpMaterialUploadTemporaryReqVO { @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") @NotNull(message = "公众号账号的编号不能为空") private Long accountId; @Schema(description = "文件类型 参见 WxConsts.MediaFileType 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "image") @NotEmpty(message = "文件类型不能为空") private String type; @Schema(description = "文件附件", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "文件不能为空") @JsonIgnore // 避免被操作日志,进行序列化,导致报错 private MultipartFile file; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/menu/MpMenuController.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.menu; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.module.mp.controller.admin.menu.vo.MpMenuRespVO; import co.yixiang.yshop.module.mp.controller.admin.menu.vo.MpMenuSaveReqVO; import co.yixiang.yshop.module.mp.convert.menu.MpMenuConvert; import co.yixiang.yshop.module.mp.dal.dataobject.menu.MpMenuDO; import co.yixiang.yshop.module.mp.service.menu.MpMenuService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 公众号菜单") @RestController @RequestMapping("/mp/menu") @Validated public class MpMenuController { @Resource private MpMenuService mpMenuService; @PostMapping("/save") @Operation(summary = "保存公众号菜单") @PreAuthorize("@ss.hasPermission('mp:menu:save')") public CommonResult saveMenu(@Valid @RequestBody MpMenuSaveReqVO createReqVO) { mpMenuService.saveMenu(createReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除公众号菜单") @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "10") @PreAuthorize("@ss.hasPermission('mp:menu:delete')") public CommonResult deleteMenu(@RequestParam("accountId") Long accountId) { mpMenuService.deleteMenuByAccountId(accountId); return success(true); } @GetMapping("/list") @Operation(summary = "获得公众号菜单列表") @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "10") @PreAuthorize("@ss.hasPermission('mp:menu:query')") public CommonResult> getMenuList(@RequestParam("accountId") Long accountId) { List list = mpMenuService.getMenuListByAccountId(accountId); return success(MpMenuConvert.INSTANCE.convertList(list)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/menu/vo/MpMenuBaseVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.menu.vo; import co.yixiang.yshop.module.mp.dal.dataobject.message.MpMessageDO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import me.chanjar.weixin.common.api.WxConsts; import org.hibernate.validator.constraints.URL; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.util.List; import static co.yixiang.yshop.module.mp.framework.mp.core.util.MpUtils.*; /** * 公众号菜单 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class MpMenuBaseVO { /** * 菜单名称 */ private String name; /** * 菜单标识 * * 支持多 DB 类型时,无法直接使用 key + @TableField("menuKey") 来实现转换,原因是 "menuKey" AS key 而存在报错 */ private String menuKey; /** * 父菜单编号 */ private Long parentId; // ========== 按钮操作 ========== /** * 按钮类型 * * 枚举 {@link WxConsts.MenuButtonType} */ private String type; @Schema(description = "网页链接", example = "https://www.yixiang.co/") @NotEmpty(message = "网页链接不能为空", groups = {ViewButtonGroup.class, MiniProgramButtonGroup.class}) @URL(message = "网页链接必须是 URL 格式") private String url; @Schema(description = "小程序的 appId", example = "wx1234567890") @NotEmpty(message = "小程序的 appId 不能为空", groups = MiniProgramButtonGroup.class) private String miniProgramAppId; @Schema(description = "小程序的页面路径", example = "pages/index/index") @NotEmpty(message = "小程序的页面路径不能为空", groups = MiniProgramButtonGroup.class) private String miniProgramPagePath; @Schema(description ="跳转图文的媒体编号", example = "jCQk93AIIgp8ixClWcW_NXXqBKInNWNmq2XnPeDZl7IMVqWiNeL4FfELtggRXd83") @NotEmpty(message = "跳转图文的媒体编号不能为空", groups = ViewLimitedButtonGroup.class) private String articleId; // ========== 消息内容 ========== @Schema(description = "回复的消息类型 枚举 TEXT、IMAGE、VOICE、VIDEO、NEWS、MUSIC", example = "text") @NotEmpty(message = "回复的消息类型不能为空", groups = {ClickButtonGroup.class, ScanCodeWaitMsgButtonGroup.class}) private String replyMessageType; @Schema(description = "回复的消息内容", example = "欢迎关注") @NotEmpty(message = "回复的消息内容不能为空", groups = TextMessageGroup.class) private String replyContent; @Schema(description = "回复的媒体 id", example = "123456") @NotEmpty(message = "回复的消息 mediaId 不能为空", groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class}) private String replyMediaId; @Schema(description = "回复的媒体 URL", example = "https://www.yixiang.co/xxx.jpg") @NotEmpty(message = "回复的消息 mediaId 不能为空", groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class}) private String replyMediaUrl; @Schema(description = "缩略图的媒体 id", example = "123456") @NotEmpty(message = "回复的消息 thumbMediaId 不能为空", groups = {MusicMessageGroup.class}) private String replyThumbMediaId; @Schema(description = "缩略图的媒体 URL",example = "https://www.yixiang.co/xxx.jpg") @NotEmpty(message = "回复的消息 thumbMedia 地址不能为空", groups = {MusicMessageGroup.class}) private String replyThumbMediaUrl; @Schema(description = "回复的标题", example = "视频标题") @NotEmpty(message = "回复的消息标题不能为空", groups = VideoMessageGroup.class) private String replyTitle; @Schema(description = "回复的描述", example = "视频描述") @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class) private String replyDescription; /** * 回复的图文消息数组 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS */ @NotNull(message = "回复的图文消息不能为空", groups = {NewsMessageGroup.class, ViewLimitedButtonGroup.class}) @Valid private List replyArticles; @Schema(description = "回复的音乐链接", example = "https://www.yixiang.co/xxx.mp3") @NotEmpty(message = "回复的音乐链接不能为空", groups = MusicMessageGroup.class) @URL(message = "回复的高质量音乐链接格式不正确", groups = MusicMessageGroup.class) private String replyMusicUrl; @Schema(description = "高质量音乐链接", example = "https://www.yixiang.co/xxx.mp3") @NotEmpty(message = "回复的高质量音乐链接不能为空", groups = MusicMessageGroup.class) @URL(message = "回复的高质量音乐链接格式不正确", groups = MusicMessageGroup.class) private String replyHqMusicUrl; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/menu/vo/MpMenuRespVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.menu.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import java.time.LocalDateTime; @Schema(description = "管理后台 - 公众号菜单 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MpMenuRespVO extends MpMenuBaseVO { @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") private Long accountId; @Schema(description = "公众号 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890ox") private String appId; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/menu/vo/MpMenuSaveReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.menu.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.util.List; @Schema(description = "管理后台 - 公众号菜单保存 Request VO") @Data public class MpMenuSaveReqVO { @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") @NotNull(message = "公众号账号的编号不能为空") private Long accountId; @NotEmpty(message = "菜单不能为空") @Valid private List menus; @Schema(description = "管理后台 - 公众号菜单保存时的每个菜单") @Data public static class Menu extends MpMenuBaseVO { /** * 子菜单数组 */ private List children; } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/message/MpAutoReplyController.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.message; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyCreateReqVO; import co.yixiang.yshop.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyRespVO; import co.yixiang.yshop.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyUpdateReqVO; import co.yixiang.yshop.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; import co.yixiang.yshop.module.mp.convert.message.MpAutoReplyConvert; import co.yixiang.yshop.module.mp.dal.dataobject.message.MpAutoReplyDO; import co.yixiang.yshop.module.mp.service.message.MpAutoReplyService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 公众号自动回复") @RestController @RequestMapping("/mp/auto-reply") @Validated public class MpAutoReplyController { @Resource private MpAutoReplyService mpAutoReplyService; @GetMapping("/page") @Operation(summary = "获得公众号自动回复分页") @PreAuthorize("@ss.hasPermission('mp:auto-reply:query')") public CommonResult> getAutoReplyPage(@Valid MpMessagePageReqVO pageVO) { PageResult pageResult = mpAutoReplyService.getAutoReplyPage(pageVO); return success(MpAutoReplyConvert.INSTANCE.convertPage(pageResult)); } @GetMapping("/get") @Operation(summary = "获得公众号自动回复") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('mp:auto-reply:query')") public CommonResult getAutoReply(@RequestParam("id") Long id) { MpAutoReplyDO autoReply = mpAutoReplyService.getAutoReply(id); return success(MpAutoReplyConvert.INSTANCE.convert(autoReply)); } @PostMapping("/create") @Operation(summary = "创建公众号自动回复") @PreAuthorize("@ss.hasPermission('mp:auto-reply:create')") public CommonResult createAutoReply(@Valid @RequestBody MpAutoReplyCreateReqVO createReqVO) { return success(mpAutoReplyService.createAutoReply(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新公众号自动回复") @PreAuthorize("@ss.hasPermission('mp:auto-reply:update')") public CommonResult updateAutoReply(@Valid @RequestBody MpAutoReplyUpdateReqVO updateReqVO) { mpAutoReplyService.updateAutoReply(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除公众号自动回复") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('mp:auto-reply:delete')") public CommonResult deleteAutoReply(@RequestParam("id") Long id) { mpAutoReplyService.deleteAutoReply(id); return success(true); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/message/MpMessageController.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.message; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; import co.yixiang.yshop.module.mp.controller.admin.message.vo.message.MpMessageRespVO; import co.yixiang.yshop.module.mp.controller.admin.message.vo.message.MpMessageSendReqVO; import co.yixiang.yshop.module.mp.convert.message.MpMessageConvert; import co.yixiang.yshop.module.mp.dal.dataobject.message.MpMessageDO; import co.yixiang.yshop.module.mp.service.message.MpMessageService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 公众号消息") @RestController @RequestMapping("/mp/message") @Validated public class MpMessageController { @Resource private MpMessageService mpMessageService; @GetMapping("/page") @Operation(summary = "获得公众号消息分页") @PreAuthorize("@ss.hasPermission('mp:message:query')") public CommonResult> getMessagePage(@Valid MpMessagePageReqVO pageVO) { PageResult pageResult = mpMessageService.getMessagePage(pageVO); return success(MpMessageConvert.INSTANCE.convertPage(pageResult)); } @PostMapping("/send") @Operation(summary = "给粉丝发送消息") @PreAuthorize("@ss.hasPermission('mp:message:send')") public CommonResult sendMessage(@Valid @RequestBody MpMessageSendReqVO reqVO) { MpMessageDO message = mpMessageService.sendKefuMessage(reqVO); return success(MpMessageConvert.INSTANCE.convert(message)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyBaseVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.message.vo.autoreply; import cn.hutool.core.util.ObjectUtil; import co.yixiang.yshop.module.mp.dal.dataobject.message.MpMessageDO; import co.yixiang.yshop.module.mp.enums.message.MpAutoReplyTypeEnum; import co.yixiang.yshop.module.mp.framework.mp.core.util.MpUtils.*; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import me.chanjar.weixin.common.api.WxConsts; import org.hibernate.validator.constraints.URL; import jakarta.validation.Valid; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.util.List; /** * 公众号自动回复 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class MpAutoReplyBaseVO { @Schema(description = "回复类型 参见 MpAutoReplyTypeEnum 枚举", example = "1") @NotNull(message = "回复类型不能为空") private Integer type; // ==================== 请求消息 ==================== @Schema(description = "请求的关键字 当 type 为 MpAutoReplyTypeEnum#KEYWORD 时,必填", example = "关键字") private String requestKeyword; @Schema(description = "请求的匹配方式 当 type 为 MpAutoReplyTypeEnum#KEYWORD 时,必填", example = "1") private Integer requestMatch; @Schema(description = "请求的消息类型 当 type 为 MpAutoReplyTypeEnum#MESSAGE 时,必填", example = "text") private String requestMessageType; // ==================== 响应消息 ==================== @Schema(description = "回复的消息类型 枚举 TEXT、IMAGE、VOICE、VIDEO、NEWS、MUSIC", example = "text") @NotEmpty(message = "回复的消息类型不能为空") private String responseMessageType; @Schema(description = "回复的消息内容", example = "欢迎关注") @NotEmpty(message = "回复的消息内容不能为空", groups = TextMessageGroup.class) private String responseContent; @Schema(description = "回复的媒体 id", example = "123456") @NotEmpty(message = "回复的消息 mediaId 不能为空", groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class}) private String responseMediaId; @Schema(description = "回复的媒体 URL", example = "https://www.yixiang.co/xxx.jpg") @NotEmpty(message = "回复的消息 mediaId 不能为空", groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class}) private String responseMediaUrl; @Schema(description = "缩略图的媒体 id", example = "123456") @NotEmpty(message = "回复的消息 thumbMediaId 不能为空", groups = {MusicMessageGroup.class}) private String responseThumbMediaId; @Schema(description = "缩略图的媒体 URL",example = "https://www.yixiang.co/xxx.jpg") @NotEmpty(message = "回复的消息 thumbMedia 地址不能为空", groups = {MusicMessageGroup.class}) private String responseThumbMediaUrl; @Schema(description = "回复的标题", example = "视频标题") @NotEmpty(message = "回复的消息标题不能为空", groups = VideoMessageGroup.class) private String responseTitle; @Schema(description = "回复的描述", example = "视频描述") @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class) private String responseDescription; /** * 回复的图文消息 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS */ @NotNull(message = "回复的图文消息不能为空", groups = {NewsMessageGroup.class, ViewLimitedButtonGroup.class}) @Valid private List responseArticles; @Schema(description = "回复的音乐链接", example = "https://www.yixiang.co/xxx.mp3") @NotEmpty(message = "回复的音乐链接不能为空", groups = MusicMessageGroup.class) @URL(message = "回复的高质量音乐链接格式不正确", groups = MusicMessageGroup.class) private String responseMusicUrl; @Schema(description = "高质量音乐链接", example = "https://www.yixiang.co/xxx.mp3") @NotEmpty(message = "回复的高质量音乐链接不能为空", groups = MusicMessageGroup.class) @URL(message = "回复的高质量音乐链接格式不正确", groups = MusicMessageGroup.class) private String responseHqMusicUrl; @AssertTrue(message = "请求的关键字不能为空") public boolean isRequestKeywordValid() { return ObjectUtil.notEqual(type, MpAutoReplyTypeEnum.KEYWORD) || requestKeyword != null; } @AssertTrue(message = "请求的关键字的匹配不能为空") public boolean isRequestMatchValid() { return ObjectUtil.notEqual(type, MpAutoReplyTypeEnum.KEYWORD) || requestMatch != null; } @AssertTrue(message = "请求的消息类型不能为空") public boolean isRequestMessageTypeValid() { return ObjectUtil.notEqual(type, MpAutoReplyTypeEnum.MESSAGE) || requestMessageType != null; } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyCreateReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.message.vo.autoreply; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 公众号自动回复的创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MpAutoReplyCreateReqVO extends MpAutoReplyBaseVO { @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "公众号账号的编号不能为空") private Long accountId; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyPageReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.message.vo.autoreply; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 公众号自动回复的分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MpAutoReplyPageReqVO extends PageParam { @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "公众号账号的编号不能为空") private Long accountId; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyRespVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.message.vo.autoreply; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import java.time.LocalDateTime; @Schema(description = "管理后台 - 公众号自动回复 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MpAutoReplyRespVO extends MpAutoReplyBaseVO { @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long accountId; @Schema(description = "公众号 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890") private String appId; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyUpdateReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.message.vo.autoreply; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 公众号自动回复的更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MpAutoReplyUpdateReqVO extends MpAutoReplyBaseVO { @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "主键不能为空") private Long id; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.message.vo.message; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 公众号消息分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MpMessagePageReqVO extends PageParam { @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "公众号账号的编号不能为空") private Long accountId; @Schema(description = "消息类型 参见 WxConsts.XmlMsgType 枚举", example = "text") private String type; @Schema(description = "公众号粉丝标识", example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M") private String openid; @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @Schema(description = "创建时间") private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/message/vo/message/MpMessageRespVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.message.vo.message; import co.yixiang.yshop.module.mp.dal.dataobject.message.MpMessageDO; import com.baomidou.mybatisplus.annotation.TableField; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import me.chanjar.weixin.common.api.WxConsts; import java.time.LocalDateTime; import java.util.Date; import java.util.List; @Schema(description = "管理后台 - 公众号消息 Response VO") @Data public class MpMessageRespVO { @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Integer id; @Schema(description = "微信公众号消息 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "23953173569869169") private Long msgId; @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long accountId; @Schema(description = "公众号账号的 appid", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890") private String appId; @Schema(description = "公众号粉丝编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") private Long userId; @Schema(description = "公众号粉丝标志", requiredMode = Schema.RequiredMode.REQUIRED, example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M") private String openid; @Schema(description = "消息类型 参见 WxConsts.XmlMsgType 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "text") private String type; @Schema(description = "消息来源 参见 MpMessageSendFromEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer sendFrom; // ========= 普通消息内容 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html @Schema(description = "消息内容 消息类型为 text 时,才有值", example = "你好呀") private String content; @Schema(description = "媒体素材的编号 消息类型为 image、voice、video 时,才有值", example = "1234567890") private String mediaId; @Schema(description = "媒体文件的 URL 消息类型为 image、voice、video 时,才有值", example = "https://www.yixiang.co/xxx.png") private String mediaUrl; @Schema(description = "语音识别后文本 消息类型为 voice 时,才有值", example = "语音识别后文本") private String recognition; @Schema(description = "语音格式 消息类型为 voice 时,才有值", example = "amr") private String format; @Schema(description = "标题 消息类型为 video、music、link 时,才有值", example = "我是标题") private String title; @Schema(description = "描述 消息类型为 video、music 时,才有值", example = "我是描述") private String description; @Schema(description = "缩略图的媒体 id 消息类型为 video、music 时,才有值", example = "1234567890") private String thumbMediaId; @Schema(description = "缩略图的媒体 URL 消息类型为 video、music 时,才有值", example = "https://www.yixiang.co/xxx.png") private String thumbMediaUrl; @Schema(description = "点击图文消息跳转链接 消息类型为 link 时,才有值", example = "https://www.yixiang.co") private String url; @Schema(description = "地理位置维度 消息类型为 location 时,才有值", example = "23.137466") private Double locationX; @Schema(description = "地理位置经度 消息类型为 location 时,才有值", example = "113.352425") private Double locationY; @Schema(description = "地图缩放大小 消息类型为 location 时,才有值", example = "13") private Double scale; @Schema(description = "详细地址 消息类型为 location 时,才有值", example = "杨浦区黄兴路 221-4 号临") private String label; /** * 图文消息数组 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS */ @TableField(typeHandler = MpMessageDO.ArticleTypeHandler.class) private List articles; @Schema(description = "音乐链接 消息类型为 music 时,才有值", example = "https://www.yixiang.co/xxx.mp3") private String musicUrl; @Schema(description = "高质量音乐链接 消息类型为 music 时,才有值", example = "https://www.yixiang.co/xxx.mp3") private String hqMusicUrl; // ========= 事件推送 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html @Schema(description = "事件类型 参见 WxConsts.EventType 枚举", example = "subscribe") private String event; @Schema(description = "事件 Key 参见 WxConsts.EventType 枚举", example = "qrscene_123456") private String eventKey; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/message/vo/message/MpMessageSendReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.message.vo.message; import co.yixiang.yshop.module.mp.dal.dataobject.message.MpMessageDO; import co.yixiang.yshop.module.mp.framework.mp.core.util.MpUtils.*; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.util.List; @Schema(description = "管理后台 - 公众号消息发送 Request VO") @Data public class MpMessageSendReqVO { @Schema(description = "公众号粉丝的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "公众号粉丝的编号不能为空") private Long userId; // ========== 消息内容 ========== @Schema(description = "消息类型 TEXT/IMAGE/VOICE/VIDEO/NEWS", requiredMode = Schema.RequiredMode.REQUIRED, example = "text") @NotEmpty(message = "消息类型不能为空") public String type; @Schema(description = "消息内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好呀") @NotEmpty(message = "消息内容不能为空", groups = TextMessageGroup.class) private String content; @Schema(description = "媒体 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "qqc_2Fot30Jse-HDoZmo5RrUDijz2nGUkP") @NotEmpty(message = "消息内容不能为空", groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class}) private String mediaId; @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "没有标题") @NotEmpty(message = "消息内容不能为空", groups = VideoMessageGroup.class) private String title; @Schema(description = "描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "你猜") @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class) private String description; @Schema(description = "缩略图的媒体 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "qqc_2Fot30Jse-HDoZmo5RrUDijz2nGUkP") @NotEmpty(message = "缩略图的媒体 id 不能为空", groups = MusicMessageGroup.class) private String thumbMediaId; @Schema(description = "图文消息", requiredMode = Schema.RequiredMode.REQUIRED) @Valid @NotNull(message = "图文消息不能为空", groups = NewsMessageGroup.class) private List articles; @Schema(description = "音乐链接 消息类型为 MUSIC 时", example = "https://www.yixiang.co/music.mp3") private String musicUrl; @Schema(description = "高质量音乐链接 消息类型为 MUSIC 时", example = "https://www.yixiang.co/music.mp3") private String hqMusicUrl; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/news/MpDraftController.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.news; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.framework.common.util.object.PageUtils; import co.yixiang.yshop.module.mp.controller.admin.news.vo.MpDraftPageReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.material.MpMaterialDO; import co.yixiang.yshop.module.mp.framework.mp.core.MpServiceFactory; import co.yixiang.yshop.module.mp.service.material.MpMaterialService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.draft.*; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import static co.yixiang.yshop.framework.common.util.collection.MapUtils.findAndThen; import static co.yixiang.yshop.module.mp.enums.ErrorCodeConstants.*; @Tag(name = "管理后台 - 公众号草稿") @RestController @RequestMapping("/mp/draft") @Validated public class MpDraftController { @Resource private MpServiceFactory mpServiceFactory; @Resource private MpMaterialService mpMaterialService; @GetMapping("/page") @Operation(summary = "获得草稿分页") @PreAuthorize("@ss.hasPermission('mp:draft:query')") public CommonResult> getDraftPage(MpDraftPageReqVO reqVO) { // 从公众号查询草稿箱 WxMpService mpService = mpServiceFactory.getRequiredMpService(reqVO.getAccountId()); WxMpDraftList draftList; try { draftList = mpService.getDraftService().listDraft(PageUtils.getStart(reqVO), reqVO.getPageSize()); } catch (WxErrorException e) { throw exception(DRAFT_LIST_FAIL, e.getError().getErrorMsg()); } // 查询对应的图片地址。目的:解决公众号的图片链接无法在我们后台展示 setDraftThumbUrl(draftList.getItems()); // 返回分页 return success(new PageResult<>(draftList.getItems(), draftList.getTotalCount().longValue())); } private void setDraftThumbUrl(List items) { // 1.1 获得 mediaId 数组 Set mediaIds = new HashSet<>(); items.forEach(item -> item.getContent().getNewsItem().forEach(newsItem -> mediaIds.add(newsItem.getThumbMediaId()))); if (CollUtil.isEmpty(mediaIds)) { return; } // 1.2 批量查询对应的 Media 素材 Map materials = CollectionUtils.convertMap(mpMaterialService.getMaterialListByMediaId(mediaIds), MpMaterialDO::getMediaId); // 2. 设置回 WxMpDraftItem 记录 items.forEach(item -> item.getContent().getNewsItem().forEach(newsItem -> findAndThen(materials, newsItem.getThumbMediaId(), material -> newsItem.setThumbUrl(material.getUrl())))); } @PostMapping("/create") @Operation(summary = "创建草稿") @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('mp:draft:create')") public CommonResult deleteDraft(@RequestParam("accountId") Long accountId, @RequestBody WxMpAddDraft draft) { WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); try { String mediaId = mpService.getDraftService().addDraft(draft); return success(mediaId); } catch (WxErrorException e) { throw exception(DRAFT_CREATE_FAIL, e.getError().getErrorMsg()); } } @PutMapping("/update") @Operation(summary = "更新草稿") @Parameters({ @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "1024"), @Parameter(name = "mediaId", description = "草稿素材的编号", required = true, example = "xxx") }) @PreAuthorize("@ss.hasPermission('mp:draft:update')") public CommonResult deleteDraft(@RequestParam("accountId") Long accountId, @RequestParam("mediaId") String mediaId, @RequestBody List articles) { WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); try { for (int i = 0; i < articles.size(); i++) { WxMpDraftArticles article = articles.get(i); mpService.getDraftService().updateDraft(new WxMpUpdateDraft(mediaId, i, article)); } return success(true); } catch (WxErrorException e) { throw exception(DRAFT_UPDATE_FAIL, e.getError().getErrorMsg()); } } @DeleteMapping("/delete") @Operation(summary = "删除草稿") @Parameters({ @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "1024"), @Parameter(name = "mediaId", description = "草稿素材的编号", required = true, example = "xxx") }) @PreAuthorize("@ss.hasPermission('mp:draft:delete')") public CommonResult deleteDraft(@RequestParam("accountId") Long accountId, @RequestParam("mediaId") String mediaId) { WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); try { mpService.getDraftService().delDraft(mediaId); return success(true); } catch (WxErrorException e) { throw exception(DRAFT_DELETE_FAIL, e.getError().getErrorMsg()); } } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/news/MpFreePublishController.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.news; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.framework.common.util.object.PageUtils; import co.yixiang.yshop.module.mp.controller.admin.news.vo.MpFreePublishPageReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.material.MpMaterialDO; import co.yixiang.yshop.module.mp.framework.mp.core.MpServiceFactory; import co.yixiang.yshop.module.mp.service.material.MpMaterialService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.freepublish.WxMpFreePublishItem; import me.chanjar.weixin.mp.bean.freepublish.WxMpFreePublishList; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import static co.yixiang.yshop.framework.common.util.collection.MapUtils.findAndThen; import static co.yixiang.yshop.module.mp.enums.ErrorCodeConstants.*; @Tag(name = "管理后台 - 公众号发布能力") @RestController @RequestMapping("/mp/free-publish") @Validated public class MpFreePublishController { @Resource private MpServiceFactory mpServiceFactory; @Resource private MpMaterialService mpMaterialService; @GetMapping("/page") @Operation(summary = "获得已发布的图文分页") @PreAuthorize("@ss.hasPermission('mp:free-publish:query')") public CommonResult> getFreePublishPage(MpFreePublishPageReqVO reqVO) { // 从公众号查询已发布的图文列表 WxMpService mpService = mpServiceFactory.getRequiredMpService(reqVO.getAccountId()); WxMpFreePublishList publicationRecords; try { publicationRecords = mpService.getFreePublishService().getPublicationRecords( PageUtils.getStart(reqVO), reqVO.getPageSize()); } catch (WxErrorException e) { throw exception(FREE_PUBLISH_LIST_FAIL, e.getError().getErrorMsg()); } // 查询对应的图片地址。目的:解决公众号的图片链接无法在我们后台展示 setFreePublishThumbUrl(publicationRecords.getItems()); // 返回分页 return success(new PageResult<>(publicationRecords.getItems(), publicationRecords.getTotalCount().longValue())); } private void setFreePublishThumbUrl(List items) { // 1.1 获得 mediaId 数组 Set mediaIds = new HashSet<>(); items.forEach(item -> item.getContent().getNewsItem().forEach(newsItem -> mediaIds.add(newsItem.getThumbMediaId()))); if (CollUtil.isEmpty(mediaIds)) { return; } // 1.2 批量查询对应的 Media 素材 Map materials = CollectionUtils.convertMap(mpMaterialService.getMaterialListByMediaId(mediaIds), MpMaterialDO::getMediaId); // 2. 设置回 WxMpFreePublishItem 记录 items.forEach(item -> item.getContent().getNewsItem().forEach(newsItem -> findAndThen(materials, newsItem.getThumbMediaId(), material -> newsItem.setThumbUrl(material.getUrl())))); } @PostMapping("/submit") @Operation(summary = "发布草稿") @Parameters({ @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "1024"), @Parameter(name = "mediaId", description = "要发布的草稿的 media_id", required = true, example = "2048") }) @PreAuthorize("@ss.hasPermission('mp:free-publish:submit')") public CommonResult submitFreePublish(@RequestParam("accountId") Long accountId, @RequestParam("mediaId") String mediaId) { WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); try { String publishId = mpService.getFreePublishService().submit(mediaId); return success(publishId); } catch (WxErrorException e) { throw exception(FREE_PUBLISH_SUBMIT_FAIL, e.getError().getErrorMsg()); } } @DeleteMapping("/delete") @Operation(summary = "删除草稿") @Parameters({ @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "1024"), @Parameter(name = "articleId", description = "发布记录的编号", required = true, example = "2048") }) @PreAuthorize("@ss.hasPermission('mp:free-publish:delete')") public CommonResult deleteFreePublish(@RequestParam("accountId") Long accountId, @RequestParam("articleId") String articleId) { WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); try { mpService.getFreePublishService().deletePushAllArticle(articleId); return success(true); } catch (WxErrorException e) { throw exception(FREE_PUBLISH_DELETE_FAIL, e.getError().getErrorMsg()); } } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/news/vo/MpDraftPageReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.news.vo; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 公众号草稿的分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MpDraftPageReqVO extends PageParam { @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "公众号账号的编号不能为空") private Long accountId; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/news/vo/MpFreePublishPageReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.news.vo; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 公众号已发布列表的分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MpFreePublishPageReqVO extends PageParam { @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "公众号账号的编号不能为空") private Long accountId; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/open/MpOpenController.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.open; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.tenant.core.util.TenantUtils; import co.yixiang.yshop.module.mp.controller.admin.open.vo.MpOpenCheckSignatureReqVO; import co.yixiang.yshop.module.mp.controller.admin.open.vo.MpOpenHandleMessageReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import co.yixiang.yshop.module.mp.framework.mp.core.MpServiceFactory; import co.yixiang.yshop.module.mp.framework.mp.core.context.MpContextHolder; import co.yixiang.yshop.module.mp.service.account.MpAccountService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.mp.api.WxMpMessageRouter; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import java.util.Objects; @Tag(name = "管理后台 - 公众号回调") @RestController @RequestMapping("/mp/open") @Validated @Slf4j public class MpOpenController { @Resource private MpServiceFactory mpServiceFactory; @Resource private MpAccountService mpAccountService; /** * 接收微信公众号的校验签名 * * 对应 文档 */ @Operation(summary = "校验签名") // 参见 @GetMapping(value = "/{appId}", produces = "text/plain;charset=utf-8") public String checkSignature(@PathVariable("appId") String appId, MpOpenCheckSignatureReqVO reqVO) { log.info("[checkSignature][appId({}) 接收到来自微信服务器的认证消息({})]", appId, reqVO); // 校验请求签名 WxMpService wxMpService = mpServiceFactory.getRequiredMpService(appId); // 校验通过 if (wxMpService.checkSignature(reqVO.getTimestamp(), reqVO.getNonce(), reqVO.getSignature())) { return reqVO.getEchostr(); } // 校验不通过 return "非法请求"; } /** * 接收微信公众号的消息推送 * * 文档 */ @Operation(summary = "处理消息") @PostMapping(value = "/{appId}", produces = "application/xml; charset=UTF-8") public String handleMessage(@PathVariable("appId") String appId, @RequestBody String content, MpOpenHandleMessageReqVO reqVO) { log.info("[handleMessage][appId({}) 推送消息,参数({}) 内容({})]", appId, reqVO, content); // 处理 appId + 多租户的上下文 MpAccountDO account = mpAccountService.getAccountFromCache(appId); Assert.notNull(account, "公众号 appId({}) 不存在", appId); try { MpContextHolder.setAppId(appId); return TenantUtils.execute(account.getTenantId(), () -> handleMessage0(appId, content, reqVO)); } finally { MpContextHolder.clear(); } } private String handleMessage0(String appId, String content, MpOpenHandleMessageReqVO reqVO) { // 校验请求签名 WxMpService mppService = mpServiceFactory.getRequiredMpService(appId); Assert.isTrue(mppService.checkSignature(reqVO.getTimestamp(), reqVO.getNonce(), reqVO.getSignature()), "非法请求"); // 第一步,解析消息 WxMpXmlMessage inMessage = null; if (StrUtil.isBlank(reqVO.getEncrypt_type())) { // 明文模式 inMessage = WxMpXmlMessage.fromXml(content); } else if (Objects.equals(reqVO.getEncrypt_type(), MpOpenHandleMessageReqVO.ENCRYPT_TYPE_AES)) { // AES 加密模式 inMessage = WxMpXmlMessage.fromEncryptedXml(content, mppService.getWxMpConfigStorage(), reqVO.getTimestamp(), reqVO.getNonce(), reqVO.getMsg_signature()); } Assert.notNull(inMessage, "消息解析失败,原因:消息为空"); // 第二步,处理消息 WxMpMessageRouter mpMessageRouter = mpServiceFactory.getRequiredMpMessageRouter(appId); WxMpXmlOutMessage outMessage = mpMessageRouter.route(inMessage); if (outMessage == null) { return ""; } // 第三步,返回消息 if (StrUtil.isBlank(reqVO.getEncrypt_type())) { // 明文模式 return outMessage.toXml(); } else if (Objects.equals(reqVO.getEncrypt_type(), MpOpenHandleMessageReqVO.ENCRYPT_TYPE_AES)) { // AES 加密模式 return outMessage.toEncryptedXml(mppService.getWxMpConfigStorage()); } return ""; } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/open/vo/MpOpenCheckSignatureReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.open.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotEmpty; @Schema(description = "管理后台 - 公众号校验签名 Request VO") @Data public class MpOpenCheckSignatureReqVO { @Schema(description = "微信加密签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "490eb57f448b87bd5f20ccef58aa4de46aa1908e") @NotEmpty(message = "微信加密签名不能为空") private String signature; @Schema(description = "时间戳", requiredMode = Schema.RequiredMode.REQUIRED, example = "1672587863") @NotEmpty(message = "时间戳不能为空") private String timestamp; @Schema(description = "随机数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1827365808") @NotEmpty(message = "随机数不能为空") private String nonce; @Schema(description = "随机字符串", requiredMode = Schema.RequiredMode.REQUIRED, example = "2721154047828672511") @NotEmpty(message = "随机字符串不能为空") @SuppressWarnings("SpellCheckingInspection") private String echostr; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/open/vo/MpOpenHandleMessageReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.open.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotEmpty; @Schema(description = "管理后台 - 公众号处理消息 Request VO") @Data public class MpOpenHandleMessageReqVO { public static final String ENCRYPT_TYPE_AES = "aes"; @Schema(description = "微信加密签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "490eb57f448b87bd5f20ccef58aa4de46aa1908e") @NotEmpty(message = "微信加密签名不能为空") private String signature; @Schema(description = "时间戳", requiredMode = Schema.RequiredMode.REQUIRED, example = "1672587863") @NotEmpty(message = "时间戳不能为空") private String timestamp; @Schema(description = "随机数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1827365808") @NotEmpty(message = "随机数不能为空") private String nonce; @Schema(description = "粉丝 openid", requiredMode = Schema.RequiredMode.REQUIRED, example = "oz-Jdtyn-WGm4C4I5Z-nvBMO_ZfY") @NotEmpty(message = "粉丝 openid 不能为空") private String openid; @Schema(description = "消息加密类型", example = "aes") private String encrypt_type; @Schema(description = "微信签名", example = "QW5kcm9pZCBUaGUgQmFzZTY0IGlzIGEgZ2VuZXJhdGVkIHN0cmluZw==") private String msg_signature; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/statistics/MpStatisticsController.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.statistics; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.module.mp.controller.admin.statistics.vo.*; import co.yixiang.yshop.module.mp.convert.statistics.MpStatisticsConvert; import co.yixiang.yshop.module.mp.service.statistics.MpStatisticsService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import me.chanjar.weixin.mp.bean.datacube.WxDataCubeInterfaceResult; import me.chanjar.weixin.mp.bean.datacube.WxDataCubeMsgResult; import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserCumulate; import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserSummary; import org.springframework.security.access.prepost.PreAuthorize; 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 jakarta.annotation.Resource; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 公众号统计") @RestController @RequestMapping("/mp/statistics") @Validated public class MpStatisticsController { @Resource private MpStatisticsService mpStatisticsService; @GetMapping("/user-summary") @Operation(summary = "获得粉丝增减数据") @PreAuthorize("@ss.hasPermission('mp:statistics:query')") public CommonResult> getUserSummary(MpStatisticsGetReqVO getReqVO) { List list = mpStatisticsService.getUserSummary( getReqVO.getAccountId(), getReqVO.getDate()); return success(MpStatisticsConvert.INSTANCE.convertList01(list)); } @GetMapping("/user-cumulate") @Operation(summary = "获得粉丝累计数据") @PreAuthorize("@ss.hasPermission('mp:statistics:query')") public CommonResult> getUserCumulate(MpStatisticsGetReqVO getReqVO) { List list = mpStatisticsService.getUserCumulate( getReqVO.getAccountId(), getReqVO.getDate()); return success(MpStatisticsConvert.INSTANCE.convertList02(list)); } @GetMapping("/upstream-message") @Operation(summary = "获取消息发送概况数据") @PreAuthorize("@ss.hasPermission('mp:statistics:query')") public CommonResult> getUpstreamMessage(MpStatisticsGetReqVO getReqVO) { List list = mpStatisticsService.getUpstreamMessage( getReqVO.getAccountId(), getReqVO.getDate()); return success(MpStatisticsConvert.INSTANCE.convertList03(list)); } @GetMapping("/interface-summary") @Operation(summary = "获取消息发送概况数据") @PreAuthorize("@ss.hasPermission('mp:statistics:query')") public CommonResult> getInterfaceSummary(MpStatisticsGetReqVO getReqVO) { List list = mpStatisticsService.getInterfaceSummary( getReqVO.getAccountId(), getReqVO.getDate()); return success(MpStatisticsConvert.INSTANCE.convertList04(list)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/statistics/vo/MpStatisticsGetReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.statistics.vo; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 获得统计数据 Request VO") @Data public class MpStatisticsGetReqVO { @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "公众号账号的编号不能为空") private Long accountId; @Schema(description = "查询时间范围", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @NotNull(message = "查询时间范围不能为空") private LocalDateTime[] date; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/statistics/vo/MpStatisticsInterfaceSummaryRespVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.statistics.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 某一天的接口分析数据 Response VO") @Data public class MpStatisticsInterfaceSummaryRespVO { @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime refDate; @Schema(description = "通过服务器配置地址获得消息后,被动回复粉丝消息的次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") private Integer callbackCount; @Schema(description = "上述动作的失败次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") private Integer failCount; @Schema(description = "总耗时,除以 callback_count 即为平均耗时", requiredMode = Schema.RequiredMode.REQUIRED, example = "30") private Integer totalTimeCost; @Schema(description = "最大耗时", requiredMode = Schema.RequiredMode.REQUIRED, example = "40") private Integer maxTimeCost; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/statistics/vo/MpStatisticsUpstreamMessageRespVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.statistics.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 某一天的粉丝增减数据 Response VO") @Data public class MpStatisticsUpstreamMessageRespVO { @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime refDate; @Schema(description = "上行发送了(向公众号发送了)消息的粉丝数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") private Integer messageUser; @Schema(description = "上行发送了消息的消息总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") private Integer messageCount; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/statistics/vo/MpStatisticsUserCumulateRespVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.statistics.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 某一天的消息发送概况数据 Response VO") @Data public class MpStatisticsUserCumulateRespVO { @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime refDate; @Schema(description = "累计粉丝量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") private Integer cumulateUser; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/statistics/vo/MpStatisticsUserSummaryRespVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.statistics.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 某一天的粉丝增减数据 Response VO") @Data public class MpStatisticsUserSummaryRespVO { @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime refDate; @Schema(description = "粉丝来源", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") private Integer userSource; @Schema(description = "新关注的粉丝数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") private Integer newUser; @Schema(description = "取消关注的粉丝数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") private Integer cancelUser; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/tag/MpTagController.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.tag; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.mp.controller.admin.tag.vo.*; import co.yixiang.yshop.module.mp.convert.tag.MpTagConvert; import co.yixiang.yshop.module.mp.dal.dataobject.tag.MpTagDO; import co.yixiang.yshop.module.mp.service.tag.MpTagService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 公众号标签") @RestController @RequestMapping("/mp/tag") @Validated public class MpTagController { @Resource private MpTagService mpTagService; @PostMapping("/create") @Operation(summary = "创建公众号标签") @PreAuthorize("@ss.hasPermission('mp:tag:create')") public CommonResult createTag(@Valid @RequestBody MpTagCreateReqVO createReqVO) { return success(mpTagService.createTag(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新公众号标签") @PreAuthorize("@ss.hasPermission('mp:tag:update')") public CommonResult updateTag(@Valid @RequestBody MpTagUpdateReqVO updateReqVO) { mpTagService.updateTag(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除公众号标签") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('mp:tag:delete')") public CommonResult deleteTag(@RequestParam("id") Long id) { mpTagService.deleteTag(id); return success(true); } @GetMapping("/get") @Operation(summary = "获取公众号标签详情") @PreAuthorize("@ss.hasPermission('mp:tag:query')") public CommonResult get(@RequestParam("id") Long id) { MpTagDO mpTagDO = mpTagService.get(id); return success(MpTagConvert.INSTANCE.convert(mpTagDO)); } @GetMapping("/page") @Operation(summary = "获取公众号标签分页") @PreAuthorize("@ss.hasPermission('mp:tag:query')") public CommonResult> getTagPage(MpTagPageReqVO pageReqVO) { PageResult pageResult = mpTagService.getTagPage(pageReqVO); return success(MpTagConvert.INSTANCE.convertPage(pageResult)); } @GetMapping("/list-all-simple") @Operation(summary = "获取公众号账号精简信息列表") @PreAuthorize("@ss.hasPermission('mp:account:query')") public CommonResult> getSimpleTags() { List list = mpTagService.getTagList(); return success(MpTagConvert.INSTANCE.convertList02(list)); } @PostMapping("/sync") @Operation(summary = "同步公众号标签") @Parameter(name = "accountId", description = "公众号账号的编号", required = true) @PreAuthorize("@ss.hasPermission('mp:tag:sync')") public CommonResult syncTag(@RequestParam("accountId") Long accountId) { mpTagService.syncTag(accountId); return success(true); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/tag/vo/MpTagBaseVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.tag.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotEmpty; /** * 公众号标签 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 * * @author fengdan */ @Data public class MpTagBaseVO { @Schema(description = "标签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆") @NotEmpty(message = "标签名不能为空") private String name; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/tag/vo/MpTagCreateReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.tag.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 公众号标签创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MpTagCreateReqVO extends MpTagBaseVO { @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") @NotNull(message = "公众号账号的编号不能为空") private Long accountId; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/tag/vo/MpTagPageReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.tag.vo; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import jakarta.validation.constraints.NotEmpty; @Schema(description = "管理后台 - 公众号标签分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MpTagPageReqVO extends PageParam { @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") @NotEmpty(message = "公众号账号的编号不能为空") private Long accountId; @Schema(description = "标签名,模糊匹配", example = "哈哈") private String name; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/tag/vo/MpTagRespVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.tag.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import java.time.LocalDateTime; @Schema(description = "管理后台 - 公众号标签 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MpTagRespVO extends MpTagBaseVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "此标签下粉丝数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") private Integer count; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/tag/vo/MpTagSimpleRespVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.tag.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "管理后台 - 公众号标签精简信息 Response VO") @Data public class MpTagSimpleRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "公众号的标签编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") private Long tagId; @Schema(description = "标签名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "快乐") private String name; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/tag/vo/MpTagUpdateReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.tag.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 公众号标签更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MpTagUpdateReqVO extends MpTagBaseVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "编号不能为空") private Long id; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/user/MpUserController.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.user; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.mp.controller.admin.user.vo.MpUserPageReqVO; import co.yixiang.yshop.module.mp.controller.admin.user.vo.MpUserRespVO; import co.yixiang.yshop.module.mp.controller.admin.user.vo.MpUserUpdateReqVO; import co.yixiang.yshop.module.mp.convert.user.MpUserConvert; import co.yixiang.yshop.module.mp.dal.dataobject.user.MpUserDO; import co.yixiang.yshop.module.mp.service.user.MpUserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 公众号粉丝") @RestController @RequestMapping("/mp/user") @Validated public class MpUserController { @Resource private MpUserService mpUserService; @GetMapping("/page") @Operation(summary = "获得公众号粉丝分页") @PreAuthorize("@ss.hasPermission('mp:user:query')") public CommonResult> getUserPage(@Valid MpUserPageReqVO pageVO) { PageResult pageResult = mpUserService.getUserPage(pageVO); return success(MpUserConvert.INSTANCE.convertPage(pageResult)); } @GetMapping("/get") @Operation(summary = "获得公众号粉丝") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('mp:user:query')") public CommonResult getUser(@RequestParam("id") Long id) { return success(MpUserConvert.INSTANCE.convert(mpUserService.getUser(id))); } @PutMapping("/update") @Operation(summary = "更新公众号粉丝") @PreAuthorize("@ss.hasPermission('mp:user:update')") public CommonResult updateUser(@Valid @RequestBody MpUserUpdateReqVO updateReqVO) { mpUserService.updateUser(updateReqVO); return success(true); } @PostMapping("/sync") @Operation(summary = "同步公众号粉丝") @Parameter(name = "accountId", description = "公众号账号的编号", required = true) @PreAuthorize("@ss.hasPermission('mp:user:sync')") public CommonResult syncUser(@RequestParam("accountId") Long accountId) { mpUserService.syncUser(accountId); return success(true); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/user/vo/MpUserPageReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.user.vo; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 公众号粉丝分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MpUserPageReqVO extends PageParam { @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") @NotNull(message = "公众号账号的编号不能为空") private Long accountId; @Schema(description = "公众号粉丝标识,模糊匹配", example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M") private String openid; @Schema(description = "微信生态唯一标识,模糊匹配", example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M") private String unionId; @Schema(description = "公众号粉丝昵称,模糊匹配", example = "yshop") private String nickname; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/user/vo/MpUserRespVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.user.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; import java.util.List; @Schema(description = "管理后台 - 公众号粉丝 Response VO") @Data public class MpUserRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "公众号粉丝标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M") private String openid; @Schema(description = "微信生态唯一标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M") private String unionId; @Schema(description = "关注状态 参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer subscribeStatus; @Schema(description = "关注时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime subscribeTime; @Schema(description = "取消关注时间") private LocalDateTime unsubscribeTime; @Schema(description = "昵称", example = "yshop") private String nickname; @Schema(description = "头像地址", example = "https://www.yixiang.co/1.png") private String headImageUrl; @Schema(description = "语言", example = "zh_CN") private String language; @Schema(description = "国家", example = "中国") private String country; @Schema(description = "省份", example = "广东省") private String province; @Schema(description = "城市", example = "广州市") private String city; @Schema(description = "备注", example = "你是一个芋头嘛") private String remark; @Schema(description = "标签编号数组", example = "1,2,3") private List tagIds; @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long accountId; @Schema(description = "公众号账号的 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890") private String appId; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/controller/admin/user/vo/MpUserUpdateReqVO.java ================================================ package co.yixiang.yshop.module.mp.controller.admin.user.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotNull; import java.util.List; @Schema(description = "管理后台 - 公众号粉丝更新 Request VO") @Data public class MpUserUpdateReqVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "编号不能为空") private Long id; @Schema(description = "昵称", example = "yshop") private String nickname; @Schema(description = "备注", example = "你是一个芋头嘛") private String remark; @Schema(description = "标签编号数组", example = "1,2,3") private List tagIds; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/convert/account/MpAccountConvert.java ================================================ package co.yixiang.yshop.module.mp.convert.account; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.mp.controller.admin.account.vo.MpAccountCreateReqVO; import co.yixiang.yshop.module.mp.controller.admin.account.vo.MpAccountRespVO; import co.yixiang.yshop.module.mp.controller.admin.account.vo.MpAccountSimpleRespVO; import co.yixiang.yshop.module.mp.controller.admin.account.vo.MpAccountUpdateReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import java.util.List; @Mapper public interface MpAccountConvert { MpAccountConvert INSTANCE = Mappers.getMapper(MpAccountConvert.class); MpAccountDO convert(MpAccountCreateReqVO bean); MpAccountDO convert(MpAccountUpdateReqVO bean); MpAccountRespVO convert(MpAccountDO bean); List convertList(List list); PageResult convertPage(PageResult page); List convertList02(List list); } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/convert/material/MpMaterialConvert.java ================================================ package co.yixiang.yshop.module.mp.convert.material; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.mp.controller.admin.material.vo.MpMaterialRespVO; import co.yixiang.yshop.module.mp.controller.admin.material.vo.MpMaterialUploadRespVO; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import co.yixiang.yshop.module.mp.dal.dataobject.material.MpMaterialDO; import me.chanjar.weixin.mp.bean.material.WxMpMaterial; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings; import org.mapstruct.factory.Mappers; import java.io.File; @Mapper public interface MpMaterialConvert { MpMaterialConvert INSTANCE = Mappers.getMapper(MpMaterialConvert.class); @Mappings({ @Mapping(target = "id", ignore = true), @Mapping(source = "account.id", target = "accountId"), @Mapping(source = "account.appId", target = "appId"), @Mapping(source = "name", target = "name") }) MpMaterialDO convert(String mediaId, String type, String url, MpAccountDO account, String name); @Mappings({ @Mapping(target = "id", ignore = true), @Mapping(source = "account.id", target = "accountId"), @Mapping(source = "account.appId", target = "appId"), @Mapping(source = "name", target = "name") }) MpMaterialDO convert(String mediaId, String type, String url, MpAccountDO account, String name, String title, String introduction, String mpUrl); MpMaterialUploadRespVO convert(MpMaterialDO bean); default WxMpMaterial convert(String name, File file, String title, String introduction) { return new WxMpMaterial(name, file, title, introduction); } PageResult convertPage(PageResult page); } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/convert/menu/MpMenuConvert.java ================================================ package co.yixiang.yshop.module.mp.convert.menu; import co.yixiang.yshop.module.mp.controller.admin.menu.vo.MpMenuRespVO; import co.yixiang.yshop.module.mp.controller.admin.menu.vo.MpMenuSaveReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.menu.MpMenuDO; import co.yixiang.yshop.module.mp.service.message.bo.MpMessageSendOutReqBO; import me.chanjar.weixin.common.bean.menu.WxMenuButton; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings; import org.mapstruct.factory.Mappers; import java.util.List; @Mapper public interface MpMenuConvert { MpMenuConvert INSTANCE = Mappers.getMapper(MpMenuConvert.class); MpMenuRespVO convert(MpMenuDO bean); List convertList(List list); @Mappings({ @Mapping(source = "menu.appId", target = "appId"), @Mapping(source = "menu.replyMessageType", target = "type"), @Mapping(source = "menu.replyContent", target = "content"), @Mapping(source = "menu.replyMediaId", target = "mediaId"), @Mapping(source = "menu.replyThumbMediaId", target = "thumbMediaId"), @Mapping(source = "menu.replyTitle", target = "title"), @Mapping(source = "menu.replyDescription", target = "description"), @Mapping(source = "menu.replyArticles", target = "articles"), @Mapping(source = "menu.replyMusicUrl", target = "musicUrl"), @Mapping(source = "menu.replyHqMusicUrl", target = "hqMusicUrl"), }) MpMessageSendOutReqBO convert(String openid, MpMenuDO menu); List convert(List list); @Mappings({ @Mapping(source = "menuKey", target = "key"), @Mapping(source = "children", target = "subButtons"), @Mapping(source = "miniProgramAppId", target = "appId"), @Mapping(source = "miniProgramPagePath", target = "pagePath"), }) WxMenuButton convert(MpMenuSaveReqVO.Menu bean); MpMenuDO convert02(MpMenuSaveReqVO.Menu menu); } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/convert/message/MpAutoReplyConvert.java ================================================ package co.yixiang.yshop.module.mp.convert.message; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyCreateReqVO; import co.yixiang.yshop.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyRespVO; import co.yixiang.yshop.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyUpdateReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.message.MpAutoReplyDO; import co.yixiang.yshop.module.mp.service.message.bo.MpMessageSendOutReqBO; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings; import org.mapstruct.factory.Mappers; @Mapper public interface MpAutoReplyConvert { MpAutoReplyConvert INSTANCE = Mappers.getMapper(MpAutoReplyConvert.class); @Mappings({ @Mapping(source = "reply.appId", target = "appId"), @Mapping(source = "reply.responseMessageType", target = "type"), @Mapping(source = "reply.responseContent", target = "content"), @Mapping(source = "reply.responseMediaId", target = "mediaId"), @Mapping(source = "reply.responseTitle", target = "title"), @Mapping(source = "reply.responseDescription", target = "description"), @Mapping(source = "reply.responseArticles", target = "articles"), }) MpMessageSendOutReqBO convert(String openid, MpAutoReplyDO reply); PageResult convertPage(PageResult page); MpAutoReplyRespVO convert(MpAutoReplyDO bean); MpAutoReplyDO convert(MpAutoReplyCreateReqVO bean); MpAutoReplyDO convert(MpAutoReplyUpdateReqVO bean); } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/convert/message/MpMessageConvert.java ================================================ package co.yixiang.yshop.module.mp.convert.message; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.mp.controller.admin.message.vo.message.MpMessageRespVO; import co.yixiang.yshop.module.mp.controller.admin.message.vo.message.MpMessageSendReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import co.yixiang.yshop.module.mp.dal.dataobject.message.MpMessageDO; import co.yixiang.yshop.module.mp.dal.dataobject.user.MpUserDO; import co.yixiang.yshop.module.mp.service.message.bo.MpMessageSendOutReqBO; import me.chanjar.weixin.common.api.WxConsts; import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutNewsMessage; import me.chanjar.weixin.mp.builder.outxml.BaseBuilder; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings; import org.mapstruct.factory.Mappers; import java.util.List; @Mapper public interface MpMessageConvert { MpMessageConvert INSTANCE = Mappers.getMapper(MpMessageConvert.class); MpMessageRespVO convert(MpMessageDO bean); List convertList(List list); PageResult convertPage(PageResult page); default MpMessageDO convert(WxMpXmlMessage wxMessage, MpAccountDO account, MpUserDO user) { MpMessageDO message = convert(wxMessage); if (account != null) { message.setAccountId(account.getId()).setAppId(account.getAppId()); } if (user != null) { message.setUserId(user.getId()).setOpenid(user.getOpenid()); } return message; } @Mappings(value = { @Mapping(source = "msgType", target = "type"), @Mapping(target = "createTime", ignore = true), }) MpMessageDO convert(WxMpXmlMessage bean); default MpMessageDO convert(MpMessageSendOutReqBO sendReqBO, MpAccountDO account, MpUserDO user) { // 构建消息 MpMessageDO message = new MpMessageDO(); message.setType(sendReqBO.getType()); switch (sendReqBO.getType()) { case WxConsts.XmlMsgType.TEXT: // 1. 文本 message.setContent(sendReqBO.getContent()); break; case WxConsts.XmlMsgType.IMAGE: // 2. 图片 case WxConsts.XmlMsgType.VOICE: // 3. 语音 message.setMediaId(sendReqBO.getMediaId()); break; case WxConsts.XmlMsgType.VIDEO: // 4. 视频 message.setMediaId(sendReqBO.getMediaId()) .setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription()); break; case WxConsts.XmlMsgType.NEWS: // 5. 图文 message.setArticles(sendReqBO.getArticles()); case WxConsts.XmlMsgType.MUSIC: // 6. 音乐 message.setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription()) .setMusicUrl(sendReqBO.getMusicUrl()).setHqMusicUrl(sendReqBO.getHqMusicUrl()) .setThumbMediaId(sendReqBO.getThumbMediaId()); break; default: throw new IllegalArgumentException("不支持的消息类型:" + message.getType()); } // 其它字段 if (account != null) { message.setAccountId(account.getId()).setAppId(account.getAppId()); } if (user != null) { message.setUserId(user.getId()).setOpenid(user.getOpenid()); } return message; } default WxMpXmlOutMessage convert02(MpMessageDO message, MpAccountDO account) { BaseBuilder builder; // 个性化字段 switch (message.getType()) { case WxConsts.XmlMsgType.TEXT: builder = WxMpXmlOutMessage.TEXT().content(message.getContent()); break; case WxConsts.XmlMsgType.IMAGE: builder = WxMpXmlOutMessage.IMAGE().mediaId(message.getMediaId()); break; case WxConsts.XmlMsgType.VOICE: builder = WxMpXmlOutMessage.VOICE().mediaId(message.getMediaId()); break; case WxConsts.XmlMsgType.VIDEO: builder = WxMpXmlOutMessage.VIDEO().mediaId(message.getMediaId()) .title(message.getTitle()).description(message.getDescription()); break; case WxConsts.XmlMsgType.NEWS: builder = WxMpXmlOutMessage.NEWS().articles(convertList02(message.getArticles())); break; case WxConsts.XmlMsgType.MUSIC: builder = WxMpXmlOutMessage.MUSIC().title(message.getTitle()).description(message.getDescription()) .musicUrl(message.getMusicUrl()).hqMusicUrl(message.getHqMusicUrl()) .thumbMediaId(message.getThumbMediaId()); break; default: throw new IllegalArgumentException("不支持的消息类型:" + message.getType()); } // 通用字段 builder.fromUser(account.getAccount()); builder.toUser(message.getOpenid()); return builder.build(); } List convertList02(List list); default WxMpKefuMessage convert(MpMessageSendReqVO sendReqVO, MpUserDO user) { me.chanjar.weixin.mp.builder.kefu.BaseBuilder builder; // 个性化字段 switch (sendReqVO.getType()) { case WxConsts.KefuMsgType.TEXT: builder = WxMpKefuMessage.TEXT().content(sendReqVO.getContent()); break; case WxConsts.KefuMsgType.IMAGE: builder = WxMpKefuMessage.IMAGE().mediaId(sendReqVO.getMediaId()); break; case WxConsts.KefuMsgType.VOICE: builder = WxMpKefuMessage.VOICE().mediaId(sendReqVO.getMediaId()); break; case WxConsts.KefuMsgType.VIDEO: builder = WxMpKefuMessage.VIDEO().mediaId(sendReqVO.getMediaId()) .title(sendReqVO.getTitle()).description(sendReqVO.getDescription()); break; case WxConsts.KefuMsgType.NEWS: builder = WxMpKefuMessage.NEWS().articles(convertList03(sendReqVO.getArticles())); break; case WxConsts.KefuMsgType.MUSIC: builder = WxMpKefuMessage.MUSIC().title(sendReqVO.getTitle()).description(sendReqVO.getDescription()) .thumbMediaId(sendReqVO.getThumbMediaId()) .musicUrl(sendReqVO.getMusicUrl()).hqMusicUrl(sendReqVO.getHqMusicUrl()); break; default: throw new IllegalArgumentException("不支持的消息类型:" + sendReqVO.getType()); } // 通用字段 builder.toUser(user.getOpenid()); return builder.build(); } List convertList03(List list); default MpMessageDO convert(WxMpKefuMessage wxMessage, MpAccountDO account, MpUserDO user) { MpMessageDO message = convert(wxMessage); if (account != null) { message.setAccountId(account.getId()).setAppId(account.getAppId()); } if (user != null) { message.setUserId(user.getId()).setOpenid(user.getOpenid()); } return message; } @Mappings(value = { @Mapping(source = "msgType", target = "type"), @Mapping(target = "createTime", ignore = true), }) MpMessageDO convert(WxMpKefuMessage bean); } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/convert/statistics/MpStatisticsConvert.java ================================================ package co.yixiang.yshop.module.mp.convert.statistics; import co.yixiang.yshop.module.mp.controller.admin.statistics.vo.MpStatisticsInterfaceSummaryRespVO; import co.yixiang.yshop.module.mp.controller.admin.statistics.vo.MpStatisticsUpstreamMessageRespVO; import co.yixiang.yshop.module.mp.controller.admin.statistics.vo.MpStatisticsUserCumulateRespVO; import co.yixiang.yshop.module.mp.controller.admin.statistics.vo.MpStatisticsUserSummaryRespVO; import me.chanjar.weixin.mp.bean.datacube.WxDataCubeInterfaceResult; import me.chanjar.weixin.mp.bean.datacube.WxDataCubeMsgResult; import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserCumulate; import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserSummary; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings; import org.mapstruct.Named; import org.mapstruct.factory.Mappers; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; @Mapper public interface MpStatisticsConvert { MpStatisticsConvert INSTANCE = Mappers.getMapper(MpStatisticsConvert.class); List convertList01(List list); List convertList02(List list); List convertList03(List list); @Mappings({ @Mapping(target = "refDate", expression = "java(dateFormat0(bean.getRefDate()))"), @Mapping(source = "msgUser", target = "messageUser"), @Mapping(source = "msgCount", target = "messageCount"), }) MpStatisticsUpstreamMessageRespVO convert(WxDataCubeMsgResult bean); List convertList04(List list); @Mapping(target = "refDate", expression = "java(dateFormat0(bean.getRefDate()))") MpStatisticsInterfaceSummaryRespVO convert(WxDataCubeInterfaceResult bean); @Named("dateFormat0") default LocalDateTime dateFormat0(String date) { return LocalDate.parse(date, DateTimeFormatter.ofPattern(FORMAT_YEAR_MONTH_DAY)).atStartOfDay(); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/convert/tag/MpTagConvert.java ================================================ package co.yixiang.yshop.module.mp.convert.tag; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.mp.controller.admin.tag.vo.MpTagRespVO; import co.yixiang.yshop.module.mp.controller.admin.tag.vo.MpTagSimpleRespVO; import co.yixiang.yshop.module.mp.controller.admin.tag.vo.MpTagUpdateReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import co.yixiang.yshop.module.mp.dal.dataobject.tag.MpTagDO; import me.chanjar.weixin.mp.bean.tag.WxUserTag; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings; import org.mapstruct.factory.Mappers; import java.util.List; @Mapper public interface MpTagConvert { MpTagConvert INSTANCE = Mappers.getMapper(MpTagConvert.class); WxUserTag convert(MpTagUpdateReqVO bean); MpTagRespVO convert(WxUserTag bean); List convertList(List list); PageResult convertPage(PageResult page); @Mappings({ @Mapping(target = "id", ignore = true), @Mapping(source = "tag.id", target = "tagId"), @Mapping(source = "tag.name", target = "name"), @Mapping(source = "tag.count", target = "count"), @Mapping(source = "account.id", target = "accountId"), @Mapping(source = "account.appId", target = "appId"), }) MpTagDO convert(WxUserTag tag, MpAccountDO account); MpTagRespVO convert(MpTagDO mpTagDO); List convertList02(List list); } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/convert/user/MpUserConvert.java ================================================ package co.yixiang.yshop.module.mp.convert.user; import cn.hutool.core.date.LocalDateTimeUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.module.mp.controller.admin.user.vo.MpUserRespVO; import co.yixiang.yshop.module.mp.controller.admin.user.vo.MpUserUpdateReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import co.yixiang.yshop.module.mp.dal.dataobject.user.MpUserDO; import me.chanjar.weixin.mp.bean.result.WxMpUser; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings; import org.mapstruct.factory.Mappers; import java.util.List; @Mapper public interface MpUserConvert { MpUserConvert INSTANCE = Mappers.getMapper(MpUserConvert.class); MpUserRespVO convert(MpUserDO bean); List convertList(List list); PageResult convertPage(PageResult page); @Mappings(value = { @Mapping(source = "openId", target = "openid"), @Mapping(source = "unionId", target = "unionId"), @Mapping(source = "headImgUrl", target = "headImageUrl"), @Mapping(target = "subscribeTime", ignore = true), // 单独转换 }) MpUserDO convert(WxMpUser wxMpUser); default MpUserDO convert(MpAccountDO account, WxMpUser wxMpUser) { MpUserDO user = convert(wxMpUser); user.setSubscribeStatus(wxMpUser.getSubscribe() ? CommonStatusEnum.ENABLE.getStatus() : CommonStatusEnum.DISABLE.getStatus()); user.setSubscribeTime(LocalDateTimeUtil.of(wxMpUser.getSubscribeTime() * 1000L)); if (account != null) { user.setAccountId(account.getId()); user.setAppId(account.getAppId()); } return user; } default List convertList(MpAccountDO account, List wxUsers) { return CollectionUtils.convertList(wxUsers, wxUser -> convert(account, wxUser)); } MpUserDO convert(MpUserUpdateReqVO bean); } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/dal/dataobject/account/MpAccountDO.java ================================================ package co.yixiang.yshop.module.mp.dal.dataobject.account; import co.yixiang.yshop.framework.tenant.core.db.TenantBaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; /** * 公众号账号 DO * * @author yshop */ @TableName("mp_account") @KeySequence("mp_account_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class MpAccountDO extends TenantBaseDO { /** * 编号 */ @TableId private Long id; /** * 公众号名称 */ private String name; /** * 公众号账号 */ private String account; /** * 公众号 appid */ private String appId; /** * 公众号密钥 */ private String appSecret; /** * 公众号token */ private String token; /** * 消息加解密密钥 */ private String aesKey; /** * 二维码图片 URL */ private String qrCodeUrl; /** * 备注 */ private String remark; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/dal/dataobject/material/MpMaterialDO.java ================================================ package co.yixiang.yshop.module.mp.dal.dataobject.material; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; import me.chanjar.weixin.common.api.WxConsts; /** * 公众号素材 DO * * 1. 临时素材 * 2. 永久素材 * * @author yshop */ @TableName("mp_material") @KeySequence("mp_material_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class MpMaterialDO extends BaseDO { /** * 主键 */ @TableId private Long id; /** * 公众号账号的编号 * * 关联 {@link MpAccountDO#getId()} */ private Long accountId; /** * 公众号 appId * * 冗余 {@link MpAccountDO#getAppId()} */ private String appId; /** * 公众号素材 id */ private String mediaId; /** * 文件类型 * * 枚举 {@link WxConsts.MediaFileType} */ private String type; /** * 是否永久 * * true - 永久素材 * false - 临时素材 */ private Boolean permanent; /** * 文件服务器的 URL */ private String url; /** * 名字 * * 永久素材:非空 * 临时素材:可能为空。 * 1. 为空的情况:粉丝主动发送的图片、语音等 * 2. 非空的情况:主动发送给粉丝的图片、语音等 */ private String name; /** * 公众号文件 URL * * 只有【永久素材】使用 */ private String mpUrl; /** * 视频素材的标题 * * 只有【永久素材】使用 */ private String title; /** * 视频素材的描述 * * 只有【永久素材】使用 */ private String introduction; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/dal/dataobject/menu/MpMenuDO.java ================================================ package co.yixiang.yshop.module.mp.dal.dataobject.menu; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import co.yixiang.yshop.module.mp.dal.dataobject.message.MpMessageDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import me.chanjar.weixin.common.api.WxConsts; import me.chanjar.weixin.common.api.WxConsts.MenuButtonType; import java.util.List; /** * 公众号菜单 DO * * @author yshop */ @TableName(value = "mp_menu", autoResultMap = true) @KeySequence("mp_menu_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MpMenuDO extends BaseDO { /** * 编号 - 顶级菜单 */ public static final Long ID_ROOT = 0L; /** * 编号 */ @TableId private Long id; /** * 公众号账号的编号 * * 关联 {@link MpAccountDO#getId()} */ private Long accountId; /** * 公众号 appId * * 冗余 {@link MpAccountDO#getAppId()} */ private String appId; /** * 菜单名称 */ private String name; /** * 菜单标识 * * 支持多 DB 类型时,无法直接使用 key + @TableField("menuKey") 来实现转换,原因是 "menuKey" AS key 而存在报错 */ private String menuKey; /** * 父菜单编号 */ private Long parentId; // ========== 按钮操作 ========== /** * 按钮类型 * * 枚举 {@link MenuButtonType} */ private String type; /** * 网页链接 * * 粉丝点击菜单可打开链接,不超过 1024 字节 * * 类型为 {@link WxConsts.XmlMsgType} 的 VIEW、MINIPROGRAM */ private String url; /** * 小程序的 appId * * 类型为 {@link MenuButtonType} 的 MINIPROGRAM */ private String miniProgramAppId; /** * 小程序的页面路径 * * 类型为 {@link MenuButtonType} 的 MINIPROGRAM */ private String miniProgramPagePath; /** * 跳转图文的媒体编号 */ private String articleId; // ========== 消息内容 ========== /** * 消息类型 * * 当 {@link #type} 为 CLICK、SCANCODE_WAITMSG * * 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS、MUSIC */ private String replyMessageType; /** * 回复的消息内容 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT */ private String replyContent; /** * 回复的媒体 id * * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO */ private String replyMediaId; /** * 回复的媒体 URL * * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO */ private String replyMediaUrl; /** * 回复的标题 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO */ private String replyTitle; /** * 回复的描述 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO */ private String replyDescription; /** * 回复的缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id * * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO */ private String replyThumbMediaId; /** * 回复的缩略图的媒体 URL * * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO */ private String replyThumbMediaUrl; /** * 回复的图文消息数组 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS */ @TableField(typeHandler = MpMessageDO.ArticleTypeHandler.class) private List replyArticles; /** * 回复的音乐链接 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC */ private String replyMusicUrl; /** * 回复的高质量音乐链接 * * WIFI 环境优先使用该链接播放音乐 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC */ private String replyHqMusicUrl; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/dal/dataobject/message/MpAutoReplyDO.java ================================================ package co.yixiang.yshop.module.mp.dal.dataobject.message; import co.yixiang.yshop.framework.common.util.collection.SetUtils; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import co.yixiang.yshop.module.mp.enums.message.MpAutoReplyMatchEnum; import co.yixiang.yshop.module.mp.enums.message.MpAutoReplyTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import me.chanjar.weixin.common.api.WxConsts; import me.chanjar.weixin.common.api.WxConsts.XmlMsgType; import java.util.List; import java.util.Set; /** * 公众号消息自动回复 DO * * @author yshop */ @TableName(value = "mp_auto_reply", autoResultMap = true) @KeySequence("mp_auto_reply_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MpAutoReplyDO extends BaseDO { public static Set REQUEST_MESSAGE_TYPE = SetUtils.asSet(WxConsts.XmlMsgType.TEXT, WxConsts.XmlMsgType.IMAGE, WxConsts.XmlMsgType.VOICE, WxConsts.XmlMsgType.VIDEO, WxConsts.XmlMsgType.SHORTVIDEO, WxConsts.XmlMsgType.LOCATION, WxConsts.XmlMsgType.LINK); /** * 主键 */ @TableId private Long id; /** * 公众号账号的编号 * * 关联 {@link MpAccountDO#getId()} */ private Long accountId; /** * 公众号 appId * * 冗余 {@link MpAccountDO#getAppId()} */ private String appId; /** * 回复类型 * * 枚举 {@link MpAutoReplyTypeEnum} */ private Integer type; // ==================== 请求消息 ==================== /** * 请求的关键字 * * 当 {@link #type} 为 {@link MpAutoReplyTypeEnum#KEYWORD} */ private String requestKeyword; /** * 请求的关键字的匹配 * * 当 {@link #type} 为 {@link MpAutoReplyTypeEnum#KEYWORD} * * 枚举 {@link MpAutoReplyMatchEnum} */ private Integer requestMatch; /** * 请求的消息类型 * * 当 {@link #type} 为 {@link MpAutoReplyTypeEnum#MESSAGE} * * 枚举 {@link XmlMsgType} 中的 {@link #REQUEST_MESSAGE_TYPE} */ private String requestMessageType; // ==================== 响应消息 ==================== /** * 回复的消息类型 * * 枚举 {@link XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS */ private String responseMessageType; /** * 回复的消息内容 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT */ private String responseContent; /** * 回复的媒体 id * * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO */ private String responseMediaId; /** * 回复的媒体 URL */ private String responseMediaUrl; /** * 回复的标题 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO */ private String responseTitle; /** * 回复的描述 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO */ private String responseDescription; /** * 回复的缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id * * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO */ private String responseThumbMediaId; /** * 回复的缩略图的媒体 URL * * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO */ private String responseThumbMediaUrl; /** * 回复的图文消息 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS */ @TableField(typeHandler = MpMessageDO.ArticleTypeHandler.class) private List responseArticles; /** * 回复的音乐链接 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC */ private String responseMusicUrl; /** * 回复的高质量音乐链接 * * WIFI 环境优先使用该链接播放音乐 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC */ private String responseHqMusicUrl; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/dal/dataobject/message/MpMessageDO.java ================================================ package co.yixiang.yshop.module.mp.dal.dataobject.message; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import co.yixiang.yshop.module.mp.dal.dataobject.user.MpUserDO; import co.yixiang.yshop.module.mp.enums.message.MpMessageSendFromEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; import lombok.*; import me.chanjar.weixin.common.api.WxConsts; import me.chanjar.weixin.mp.builder.kefu.NewsBuilder; import jakarta.validation.constraints.NotEmpty; import java.io.Serializable; import java.util.List; /** * 公众号消息 DO * * @author yshop */ @TableName(value = "mp_message", autoResultMap = true) @KeySequence("mp_message_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MpMessageDO extends BaseDO { /** * 主键 */ @TableId private Long id; /** * 微信公众号消息 id */ private Long msgId; /** * 公众号账号的 ID * * 关联 {@link MpAccountDO#getId()} */ private Long accountId; /** * 公众号 appid * * 冗余 {@link MpAccountDO#getAppId()} */ private String appId; /** * 公众号粉丝的编号 * * 关联 {@link MpUserDO#getId()} */ private Long userId; /** * 公众号粉丝标志 * * 冗余 {@link MpUserDO#getOpenid()} */ private String openid; /** * 消息类型 * * 枚举 {@link WxConsts.XmlMsgType} */ private String type; /** * 消息来源 * * 枚举 {@link MpMessageSendFromEnum} */ private Integer sendFrom; // ========= 普通消息内容 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html /** * 消息内容 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT */ private String content; /** * 媒体文件的编号 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO */ private String mediaId; /** * 媒体文件的 URL */ private String mediaUrl; /** * 语音识别后文本 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 VOICE */ private String recognition; /** * 语音格式,如 amr,speex 等 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 VOICE */ private String format; /** * 标题 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC、LINK */ private String title; /** * 描述 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC */ private String description; /** * 缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id * * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO */ private String thumbMediaId; /** * 缩略图的媒体 URL * * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO */ private String thumbMediaUrl; /** * 点击图文消息跳转链接 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 LINK */ private String url; /** * 地理位置维度 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION */ private Double locationX; /** * 地理位置经度 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION */ private Double locationY; /** * 地图缩放大小 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION */ private Double scale; /** * 详细地址 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION * * 例如说杨浦区黄兴路 221-4 号临 */ private String label; /** * 图文消息数组 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS */ @TableField(typeHandler = ArticleTypeHandler.class) private List
articles; /** * 音乐链接 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC */ private String musicUrl; /** * 高质量音乐链接 * * WIFI 环境优先使用该链接播放音乐 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC */ private String hqMusicUrl; // ========= 事件推送 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html /** * 事件类型 * * 枚举 {@link WxConsts.EventType} */ private String event; /** * 事件 Key * * 1. {@link WxConsts.EventType} 的 SCAN:qrscene_ 为前缀,后面为二维码的参数值 * 2. {@link WxConsts.EventType} 的 CLICK:与自定义菜单接口中 KEY 值对应 */ private String eventKey; /** * 文章 */ @Data public static class Article implements Serializable { /** * 图文消息标题 */ @NotEmpty(message = "图文消息标题不能为空", groups = NewsBuilder.class) private String title; /** * 图文消息描述 */ @NotEmpty(message = "图文消息描述不能为空", groups = NewsBuilder.class) private String description; /** * 图片链接 * * 支持 JPG、PNG 格式,较好的效果为大图 360*200,小图 200*200 */ @NotEmpty(message = "图片链接不能为空", groups = NewsBuilder.class) private String picUrl; /** * 点击图文消息跳转链接 */ @NotEmpty(message = "点击图文消息跳转链接不能为空", groups = NewsBuilder.class) private String url; } // TODO @yshop:可以找一些新的思路 public static class ArticleTypeHandler extends AbstractJsonTypeHandler> { @Override protected List
parse(String json) { return JsonUtils.parseArray(json, Article.class); } @Override protected String toJson(List
obj) { return JsonUtils.toJsonString(obj); } } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/dal/dataobject/tag/MpTagDO.java ================================================ package co.yixiang.yshop.module.mp.dal.dataobject.tag; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import lombok.*; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import me.chanjar.weixin.mp.bean.tag.WxUserTag; /** * 公众号标签 DO * * @author yshop */ @TableName("mp_tag") @KeySequence("mp_tag_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class MpTagDO extends BaseDO { /** * 主键 */ @TableId(type = IdType.INPUT) private Long id; /** * 公众号标签 id */ private Long tagId; /** * 标签名 */ private String name; /** * 此标签下粉丝数 * * 冗余:{@link WxUserTag#getCount()} 字段,需要管理员点击【同步】后,更新该字段 */ private Integer count; /** * 公众号账号的编号 * * 关联 {@link MpAccountDO#getId()} */ private Long accountId; /** * 公众号 appId * * 冗余 {@link MpAccountDO#getAppId()} */ private String appId; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/dal/dataobject/user/MpUserDO.java ================================================ package co.yixiang.yshop.module.mp.dal.dataobject.user; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.framework.mybatis.core.type.LongListTypeHandler; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import co.yixiang.yshop.module.mp.dal.dataobject.tag.MpTagDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; import java.time.LocalDateTime; import java.util.List; /** * 微信公众号粉丝 DO * * @author yshop */ @TableName(value = "mp_user", autoResultMap = true) @KeySequence("mp_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class MpUserDO extends BaseDO { /** * 编号 */ @TableId private Long id; /** * 粉丝标识 */ private String openid; /** * 微信生态唯一标识 */ private String unionId; /** * 关注状态 * * 枚举 {@link CommonStatusEnum} * 1. 开启 - 已关注 * 2. 禁用 - 取消关注 */ private Integer subscribeStatus; /** * 关注时间 */ private LocalDateTime subscribeTime; /** * 取消关注时间 */ private LocalDateTime unsubscribeTime; /** * 昵称 * * 注意,2021-12-27 公众号接口不再返回头像和昵称,只能通过微信公众号的网页登录获取 */ private String nickname; /** * 头像地址 * * 注意,2021-12-27 公众号接口不再返回头像和昵称,只能通过微信公众号的网页登录获取 */ private String headImageUrl; /** * 语言 */ private String language; /** * 国家 */ private String country; /** * 省份 */ private String province; /** * 城市 */ private String city; /** * 备注 */ private String remark; /** * 标签编号数组 * * 注意,对应的是 {@link MpTagDO#getTagId()} 字段 */ @TableField(typeHandler = LongListTypeHandler.class) private List tagIds; /** * 公众号账号的编号 * * 关联 {@link MpAccountDO#getId()} */ private Long accountId; /** * 公众号 appId * * 冗余 {@link MpAccountDO#getAppId()} */ private String appId; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/dal/mysql/account/MpAccountMapper.java ================================================ package co.yixiang.yshop.module.mp.dal.mysql.account; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.mp.controller.admin.account.vo.MpAccountPageReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import java.time.LocalDateTime; @Mapper public interface MpAccountMapper extends BaseMapperX { default PageResult selectPage(MpAccountPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(MpAccountDO::getName, reqVO.getName()) .likeIfPresent(MpAccountDO::getAccount, reqVO.getAccount()) .likeIfPresent(MpAccountDO::getAppId, reqVO.getAppId()) .orderByDesc(MpAccountDO::getId)); } default MpAccountDO selectByAppId(String appId) { return selectOne(MpAccountDO::getAppId, appId); } @Select("SELECT COUNT(*) FROM mp_account WHERE update_time > #{maxUpdateTime}") Long selectCountByUpdateTimeGt(LocalDateTime maxUpdateTime); } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/dal/mysql/material/MpMaterialMapper.java ================================================ package co.yixiang.yshop.module.mp.dal.mysql.material; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.mp.controller.admin.material.vo.MpMaterialPageReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.material.MpMaterialDO; import org.apache.ibatis.annotations.Mapper; import java.util.Collection; import java.util.List; @Mapper public interface MpMaterialMapper extends BaseMapperX { default MpMaterialDO selectByAccountIdAndMediaId(Long accountId, String mediaId) { return selectOne(MpMaterialDO::getAccountId, accountId, MpMaterialDO::getMediaId, mediaId); } default PageResult selectPage(MpMaterialPageReqVO pageReqVO) { return selectPage(pageReqVO, new LambdaQueryWrapperX() .eq(MpMaterialDO::getAccountId, pageReqVO.getAccountId()) .eqIfPresent(MpMaterialDO::getPermanent, pageReqVO.getPermanent()) .eqIfPresent(MpMaterialDO::getType, pageReqVO.getType()) .orderByDesc(MpMaterialDO::getId)); } default List selectListByMediaId(Collection mediaIds) { return selectList(MpMaterialDO::getMediaId, mediaIds); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/dal/mysql/menu/MpMenuMapper.java ================================================ package co.yixiang.yshop.module.mp.dal.mysql.menu; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.mp.dal.dataobject.menu.MpMenuDO; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface MpMenuMapper extends BaseMapperX { default MpMenuDO selectByAppIdAndMenuKey(String appId, String menuKey) { return selectOne(MpMenuDO::getAppId, appId, MpMenuDO::getMenuKey, menuKey); } default List selectListByAccountId(Long accountId) { return selectList(MpMenuDO::getAccountId, accountId); } default void deleteByAccountId(Long accountId) { delete(new LambdaQueryWrapperX().eq(MpMenuDO::getAccountId, accountId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/dal/mysql/message/MpAutoReplyMapper.java ================================================ package co.yixiang.yshop.module.mp.dal.mysql.message; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.message.MpAutoReplyDO; import co.yixiang.yshop.module.mp.enums.message.MpAutoReplyMatchEnum; import co.yixiang.yshop.module.mp.enums.message.MpAutoReplyTypeEnum; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface MpAutoReplyMapper extends BaseMapperX { default PageResult selectPage(MpMessagePageReqVO pageVO) { return selectPage(pageVO, new LambdaQueryWrapperX() .eq(MpAutoReplyDO::getAccountId, pageVO.getAccountId()) .eqIfPresent(MpAutoReplyDO::getType, pageVO.getType())); } default List selectListByAppIdAndKeywordAll(String appId, String requestKeyword) { return selectList(new LambdaQueryWrapperX() .eq(MpAutoReplyDO::getAppId, appId) .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.KEYWORD.getType()) .eq(MpAutoReplyDO::getRequestMatch, MpAutoReplyMatchEnum.ALL.getMatch()) .eq(MpAutoReplyDO::getRequestKeyword, requestKeyword)); } default List selectListByAppIdAndKeywordLike(String appId, String requestKeyword) { return selectList(new LambdaQueryWrapperX() .eq(MpAutoReplyDO::getAppId, appId) .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.KEYWORD.getType()) .eq(MpAutoReplyDO::getRequestMatch, MpAutoReplyMatchEnum.LIKE.getMatch()) .like(MpAutoReplyDO::getRequestKeyword, requestKeyword)); } default List selectListByAppIdAndMessage(String appId, String requestMessageType) { return selectList(new LambdaQueryWrapperX() .eq(MpAutoReplyDO::getAppId, appId) .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.MESSAGE.getType()) .eq(MpAutoReplyDO::getRequestMessageType, requestMessageType)); } default List selectListByAppIdAndSubscribe(String appId) { return selectList(new LambdaQueryWrapperX() .eq(MpAutoReplyDO::getAppId, appId) .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.SUBSCRIBE.getType())); } default MpAutoReplyDO selectByAccountIdAndSubscribe(Long accountId) { return selectOne(MpAutoReplyDO::getAccountId, accountId, MpAutoReplyDO::getType, MpAutoReplyTypeEnum.SUBSCRIBE.getType()); } default MpAutoReplyDO selectByAccountIdAndMessage(Long accountId, String requestMessageType) { return selectOne(new LambdaQueryWrapperX() .eq(MpAutoReplyDO::getAccountId, accountId) .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.MESSAGE.getType()) .eq(MpAutoReplyDO::getRequestMessageType, requestMessageType)); } default MpAutoReplyDO selectByAccountIdAndKeyword(Long accountId, String requestKeyword) { return selectOne(new LambdaQueryWrapperX() .eq(MpAutoReplyDO::getAccountId, accountId) .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.KEYWORD.getType()) .eq(MpAutoReplyDO::getRequestKeyword, requestKeyword)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/dal/mysql/message/MpMessageMapper.java ================================================ package co.yixiang.yshop.module.mp.dal.mysql.message; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.message.MpMessageDO; import org.apache.ibatis.annotations.Mapper; @Mapper public interface MpMessageMapper extends BaseMapperX { default PageResult selectPage(MpMessagePageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(MpMessageDO::getAccountId, reqVO.getAccountId()) .eqIfPresent(MpMessageDO::getType, reqVO.getType()) .eqIfPresent(MpMessageDO::getOpenid, reqVO.getOpenid()) .betweenIfPresent(MpMessageDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(MpMessageDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/dal/mysql/tag/MpTagMapper.java ================================================ package co.yixiang.yshop.module.mp.dal.mysql.tag; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.mp.controller.admin.tag.vo.MpTagPageReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.tag.MpTagDO; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface MpTagMapper extends BaseMapperX { default PageResult selectPage(MpTagPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(MpTagDO::getAccountId, reqVO.getAccountId()) .likeIfPresent(MpTagDO::getName, reqVO.getName()) .orderByDesc(MpTagDO::getId)); } default List selectListByAccountId(Long accountId) { return selectList(MpTagDO::getAccountId, accountId); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/dal/mysql/user/MpUserMapper.java ================================================ package co.yixiang.yshop.module.mp.dal.mysql.user; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.mp.controller.admin.user.vo.MpUserPageReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.user.MpUserDO; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface MpUserMapper extends BaseMapperX { default PageResult selectPage(MpUserPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(MpUserDO::getOpenid, reqVO.getOpenid()) .likeIfPresent(MpUserDO::getNickname, reqVO.getNickname()) .eqIfPresent(MpUserDO::getAccountId, reqVO.getAccountId()) .orderByDesc(MpUserDO::getId)); } default MpUserDO selectByAppIdAndOpenid(String appId, String openid) { return selectOne(MpUserDO::getAppId, appId, MpUserDO::getOpenid, openid); } default List selectListByAppIdAndOpenid(String appId, List openids) { return selectList(new LambdaQueryWrapperX() .eq(MpUserDO::getAppId, appId) .in(MpUserDO::getOpenid, openids)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/framework/mp/config/MpConfiguration.java ================================================ package co.yixiang.yshop.module.mp.framework.mp.config; import co.yixiang.yshop.module.mp.framework.mp.core.DefaultMpServiceFactory; import co.yixiang.yshop.module.mp.framework.mp.core.MpServiceFactory; import co.yixiang.yshop.module.mp.service.handler.menu.MenuHandler; import co.yixiang.yshop.module.mp.service.handler.message.MessageReceiveHandler; import co.yixiang.yshop.module.mp.service.handler.message.MessageAutoReplyHandler; import co.yixiang.yshop.module.mp.service.handler.other.KfSessionHandler; import co.yixiang.yshop.module.mp.service.handler.other.NullHandler; import co.yixiang.yshop.module.mp.service.handler.other.ScanHandler; import co.yixiang.yshop.module.mp.service.handler.other.StoreCheckNotifyHandler; import co.yixiang.yshop.module.mp.service.handler.user.LocationHandler; import co.yixiang.yshop.module.mp.service.handler.user.SubscribeHandler; import co.yixiang.yshop.module.mp.service.handler.user.UnsubscribeHandler; import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.StringRedisTemplate; /** * 微信公众号的配置类 * * @author yshop */ @Configuration public class MpConfiguration { @Bean @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") public RedisTemplateWxRedisOps redisTemplateWxRedisOps(StringRedisTemplate stringRedisTemplate) { return new RedisTemplateWxRedisOps(stringRedisTemplate); } @Bean @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") public MpServiceFactory mpServiceFactory(RedisTemplateWxRedisOps redisTemplateWxRedisOps, WxMpProperties wxMpProperties, MessageReceiveHandler messageReceiveHandler, KfSessionHandler kfSessionHandler, StoreCheckNotifyHandler storeCheckNotifyHandler, MenuHandler menuHandler, NullHandler nullHandler, SubscribeHandler subscribeHandler, UnsubscribeHandler unsubscribeHandler, LocationHandler locationHandler, ScanHandler scanHandler, MessageAutoReplyHandler messageAutoReplyHandler) { return new DefaultMpServiceFactory(redisTemplateWxRedisOps, wxMpProperties, messageReceiveHandler, kfSessionHandler, storeCheckNotifyHandler, menuHandler, nullHandler, subscribeHandler, unsubscribeHandler, locationHandler, scanHandler, messageAutoReplyHandler); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/framework/mp/core/DefaultMpServiceFactory.java ================================================ package co.yixiang.yshop.module.mp.framework.mp.core; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import co.yixiang.yshop.module.mp.service.handler.menu.MenuHandler; import co.yixiang.yshop.module.mp.service.handler.message.MessageReceiveHandler; import co.yixiang.yshop.module.mp.service.handler.message.MessageAutoReplyHandler; import co.yixiang.yshop.module.mp.service.handler.other.KfSessionHandler; import co.yixiang.yshop.module.mp.service.handler.other.NullHandler; import co.yixiang.yshop.module.mp.service.handler.other.ScanHandler; import co.yixiang.yshop.module.mp.service.handler.other.StoreCheckNotifyHandler; import co.yixiang.yshop.module.mp.service.handler.user.LocationHandler; import co.yixiang.yshop.module.mp.service.handler.user.SubscribeHandler; import co.yixiang.yshop.module.mp.service.handler.user.UnsubscribeHandler; import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; import com.google.common.collect.Maps; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.api.WxConsts; import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps; import me.chanjar.weixin.mp.api.WxMpMessageRouter; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl; import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl; import me.chanjar.weixin.mp.constant.WxMpEventConstants; import java.util.List; import java.util.Map; /** * 默认的 {@link MpServiceFactory} 实现类 * * @author yshop */ @Slf4j @RequiredArgsConstructor public class DefaultMpServiceFactory implements MpServiceFactory { /** * 微信 appId 与 WxMpService 的映射 */ private volatile Map appId2MpServices; /** * 公众号账号 id 与 WxMpService 的映射 */ private volatile Map id2MpServices; /** * 微信 appId 与 WxMpMessageRouter 的映射 */ private volatile Map mpMessageRouters; private final RedisTemplateWxRedisOps redisTemplateWxRedisOps; private final WxMpProperties mpProperties; // ========== 各种 Handler ========== private final MessageReceiveHandler messageReceiveHandler; private final KfSessionHandler kfSessionHandler; private final StoreCheckNotifyHandler storeCheckNotifyHandler; private final MenuHandler menuHandler; private final NullHandler nullHandler; private final SubscribeHandler subscribeHandler; private final UnsubscribeHandler unsubscribeHandler; private final LocationHandler locationHandler; private final ScanHandler scanHandler; private final MessageAutoReplyHandler messageAutoReplyHandler; @Override public void init(List list) { Map appId2MpServices = Maps.newHashMap(); Map id2MpServices = Maps.newHashMap(); Map mpMessageRouters = Maps.newHashMap(); // 处理 list list.forEach(account -> { // 构建 WxMpService 对象 WxMpService mpService = buildMpService(account); appId2MpServices.put(account.getAppId(), mpService); id2MpServices.put(account.getId(), mpService); // 构建 WxMpMessageRouter 对象 WxMpMessageRouter mpMessageRouter = buildMpMessageRouter(mpService); mpMessageRouters.put(account.getAppId(), mpMessageRouter); }); // 设置到缓存 this.appId2MpServices = appId2MpServices; this.id2MpServices = id2MpServices; this.mpMessageRouters = mpMessageRouters; } @Override public WxMpService getMpService(Long id) { return id2MpServices.get(id); } @Override public WxMpService getMpService(String appId) { return appId2MpServices.get(appId); } @Override public WxMpMessageRouter getMpMessageRouter(String appId) { return mpMessageRouters.get(appId); } private WxMpService buildMpService(MpAccountDO account) { // 第一步,创建 WxMpRedisConfigImpl 对象 WxMpRedisConfigImpl configStorage = new WxMpRedisConfigImpl( redisTemplateWxRedisOps, mpProperties.getConfigStorage().getKeyPrefix()); configStorage.setAppId(account.getAppId()); configStorage.setSecret(account.getAppSecret()); configStorage.setToken(account.getToken()); configStorage.setAesKey(account.getAesKey()); // 第二步,创建 WxMpService 对象 WxMpService service = new WxMpServiceImpl(); service.setWxMpConfigStorage(configStorage); return service; } private WxMpMessageRouter buildMpMessageRouter(WxMpService mpService) { WxMpMessageRouter router = new WxMpMessageRouter(mpService); // 记录所有事件的日志(异步执行) router.rule().handler(messageReceiveHandler).next(); // 接收客服会话管理事件 router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) .event(WxMpEventConstants.CustomerService.KF_CREATE_SESSION) .handler(kfSessionHandler).end(); router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) .event(WxMpEventConstants.CustomerService.KF_CLOSE_SESSION) .handler(kfSessionHandler) .end(); router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) .event(WxMpEventConstants.CustomerService.KF_SWITCH_SESSION) .handler(kfSessionHandler).end(); // 门店审核事件 router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) .event(WxMpEventConstants.POI_CHECK_NOTIFY) .handler(storeCheckNotifyHandler).end(); // 自定义菜单事件 router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) .event(WxConsts.MenuButtonType.CLICK).handler(menuHandler).end(); // 点击菜单连接事件 router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) .event(WxConsts.MenuButtonType.VIEW).handler(nullHandler).end(); // 关注事件 router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) .event(WxConsts.EventType.SUBSCRIBE).handler(subscribeHandler) .end(); // 取消关注事件 router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) .event(WxConsts.EventType.UNSUBSCRIBE) .handler(unsubscribeHandler).end(); // 上报地理位置事件 router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) .event(WxConsts.EventType.LOCATION).handler(locationHandler) .end(); // 接收地理位置消息 router.rule().async(false).msgType(WxConsts.XmlMsgType.LOCATION) .handler(locationHandler).end(); // 扫码事件 router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) .event(WxConsts.EventType.SCAN).handler(scanHandler).end(); // 默认 router.rule().async(false).handler(messageAutoReplyHandler).end(); return router; } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/framework/mp/core/MpServiceFactory.java ================================================ package co.yixiang.yshop.module.mp.framework.mp.core; import cn.hutool.core.lang.Assert; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import me.chanjar.weixin.mp.api.WxMpMessageRouter; import me.chanjar.weixin.mp.api.WxMpService; import java.util.List; /** * {@link WxMpService} 工厂接口 * * @author yshop */ public interface MpServiceFactory { /** * 基于微信公众号的账号,初始化对应的 WxMpService 与 WxMpMessageRouter 实例 * * @param list 公众号的账号列表 */ void init(List list); /** * 获得 id 对应的 WxMpService 实例 * * @param id 微信公众号的编号 * @return WxMpService 实例 */ WxMpService getMpService(Long id); default WxMpService getRequiredMpService(Long id) { WxMpService wxMpService = getMpService(id); Assert.notNull(wxMpService, "找到对应 id({}) 的 WxMpService,请核实!", id); return wxMpService; } /** * 获得 appId 对应的 WxMpService 实例 * * @param appId 微信公众号 appId * @return WxMpService 实例 */ WxMpService getMpService(String appId); default WxMpService getRequiredMpService(String appId) { WxMpService wxMpService = getMpService(appId); Assert.notNull(wxMpService, "找到对应 appId({}) 的 WxMpService,请核实!", appId); return wxMpService; } /** * 获得 appId 对应的 WxMpMessageRouter 实例 * * @param appId 微信公众号 appId * @return WxMpMessageRouter 实例 */ WxMpMessageRouter getMpMessageRouter(String appId); default WxMpMessageRouter getRequiredMpMessageRouter(String appId) { WxMpMessageRouter wxMpMessageRouter = getMpMessageRouter(appId); Assert.notNull(wxMpMessageRouter, "找到对应 appId({}) 的 WxMpMessageRouter,请核实!", appId); return wxMpMessageRouter; } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/framework/mp/core/context/MpContextHolder.java ================================================ /* * Copyright (c) 2018-2025, lengleng All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * Neither the name of the pig4cloud.com developer nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * Author: lengleng (wangiegie@gmail.com) */ package co.yixiang.yshop.module.mp.framework.mp.core.context; import co.yixiang.yshop.module.mp.controller.admin.open.vo.MpOpenHandleMessageReqVO; import com.alibaba.ttl.TransmittableThreadLocal; import lombok.experimental.UtilityClass; import me.chanjar.weixin.mp.api.WxMpMessageHandler; /** * 微信上下文 Context * * 目的:解决微信多公众号的问题,在 {@link WxMpMessageHandler} 实现类中,可以通过 {@link #getAppId()} 获取到当前的 appId * * @see co.yixiang.yshop.module.mp.controller.admin.open.MpOpenController#handleMessage(String, String, MpOpenHandleMessageReqVO) * * @author yshop */ public class MpContextHolder { /** * 微信公众号的 appId 上下文 */ private static final ThreadLocal APPID = new TransmittableThreadLocal<>(); public static void setAppId(String appId) { APPID.set(appId); } public static String getAppId() { return APPID.get(); } public static void clear() { APPID.remove(); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/framework/mp/core/util/MpUtils.java ================================================ package co.yixiang.yshop.module.mp.framework.mp.core.util; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.util.validation.ValidationUtils; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.api.WxConsts; import jakarta.validation.Validator; /** * 公众号工具类 * * @author yshop */ @Slf4j public class MpUtils { /** * 校验消息的格式是否符合要求 * * @param type 类型 * @param message 消息 */ public static void validateMessage(Validator validator, String type, Object message) { // 获得对应的校验 group Class group; switch (type) { case WxConsts.XmlMsgType.TEXT: group = TextMessageGroup.class; break; case WxConsts.XmlMsgType.IMAGE: group = ImageMessageGroup.class; break; case WxConsts.XmlMsgType.VOICE: group = VoiceMessageGroup.class; break; case WxConsts.XmlMsgType.VIDEO: group = VideoMessageGroup.class; break; case WxConsts.XmlMsgType.NEWS: group = NewsMessageGroup.class; break; case WxConsts.XmlMsgType.MUSIC: group = MusicMessageGroup.class; break; default: log.error("[validateMessage][未知的消息类型({})]", message); throw new IllegalArgumentException("不支持的消息类型:" + type); } // 执行校验 ValidationUtils.validate(validator, message, group); } public static void validateButton(Validator validator, String type, String messageType, Object button) { if (StrUtil.isBlank(type)) { return; } // 获得对应的校验 group Class group; switch (type) { case WxConsts.MenuButtonType.CLICK: group = ClickButtonGroup.class; validateMessage(validator, messageType, button); // 需要额外校验回复的消息格式 break; case WxConsts.MenuButtonType.VIEW: group = ViewButtonGroup.class; break; case WxConsts.MenuButtonType.MINIPROGRAM: group = MiniProgramButtonGroup.class; break; case WxConsts.MenuButtonType.SCANCODE_WAITMSG: group = ScanCodeWaitMsgButtonGroup.class; validateMessage(validator, messageType, button); // 需要额外校验回复的消息格式 break; case "article_" + WxConsts.MenuButtonType.VIEW_LIMITED: group = ViewLimitedButtonGroup.class; break; case WxConsts.MenuButtonType.SCANCODE_PUSH: // 不用校验,直接 return 即可 case WxConsts.MenuButtonType.PIC_SYSPHOTO: case WxConsts.MenuButtonType.PIC_PHOTO_OR_ALBUM: case WxConsts.MenuButtonType.PIC_WEIXIN: case WxConsts.MenuButtonType.LOCATION_SELECT: return; default: log.error("[validateButton][未知的按钮({})]", button); throw new IllegalArgumentException("不支持的按钮类型:" + type); } // 执行校验 ValidationUtils.validate(validator, button, group); } /** * 根据消息类型,获得对应的媒体文件类型 * * 注意,不会返回 WxConsts.MediaFileType.THUMB,因为该类型会有明确标注 * * @param messageType 消息类型 {@link WxConsts.XmlMsgType} * @return 媒体文件类型 {@link WxConsts.MediaFileType} */ public static String getMediaFileType(String messageType) { switch (messageType) { case WxConsts.XmlMsgType.IMAGE: return WxConsts.MediaFileType.IMAGE; case WxConsts.XmlMsgType.VOICE: return WxConsts.MediaFileType.VOICE; case WxConsts.XmlMsgType.VIDEO: return WxConsts.MediaFileType.VIDEO; default: return WxConsts.MediaFileType.FILE; } } /** * Text 类型的消息,参数校验 Group */ public interface TextMessageGroup {} /** * Image 类型的消息,参数校验 Group */ public interface ImageMessageGroup {} /** * Voice 类型的消息,参数校验 Group */ public interface VoiceMessageGroup {} /** * Video 类型的消息,参数校验 Group */ public interface VideoMessageGroup {} /** * News 类型的消息,参数校验 Group */ public interface NewsMessageGroup {} /** * Music 类型的消息,参数校验 Group */ public interface MusicMessageGroup {} /** * Click 类型的按钮,参数校验 Group */ public interface ClickButtonGroup {} /** * View 类型的按钮,参数校验 Group */ public interface ViewButtonGroup {} /** * MiniProgram 类型的按钮,参数校验 Group */ public interface MiniProgramButtonGroup {} /** * SCANCODE_WAITMSG 类型的按钮,参数校验 Group */ public interface ScanCodeWaitMsgButtonGroup {} /** * VIEW_LIMITED 类型的按钮,参数校验 Group */ public interface ViewLimitedButtonGroup {} } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/framework/web/config/MpWebConfiguration.java ================================================ package co.yixiang.yshop.module.mp.framework.web.config; import co.yixiang.yshop.framework.swagger.config.YshopSwaggerAutoConfiguration; import org.springdoc.core.models.GroupedOpenApi; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * mp 模块的 web 组件的 Configuration * * @author yshop */ @Configuration(proxyBeanMethods = false) public class MpWebConfiguration { /** * mp 模块的 API 分组 */ @Bean public GroupedOpenApi mpGroupedOpenApi() { return YshopSwaggerAutoConfiguration.buildGroupedOpenApi("mp"); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/account/MpAccountService.java ================================================ package co.yixiang.yshop.module.mp.service.account; import cn.binarywang.wx.miniapp.api.WxMaService; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.mp.controller.admin.account.vo.MpAccountCreateReqVO; import co.yixiang.yshop.module.mp.controller.admin.account.vo.MpAccountPageReqVO; import co.yixiang.yshop.module.mp.controller.admin.account.vo.MpAccountUpdateReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import jakarta.validation.Valid; import me.chanjar.weixin.mp.api.WxMpService; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.mp.enums.ErrorCodeConstants.ACCOUNT_NOT_EXISTS; /** * 公众号账号 Service 接口 * * @author yshop */ public interface MpAccountService { /** * 初始化缓存 */ void initLocalCache(); /** * 创建公众号账号 * * @param createReqVO 创建信息 * @return 编号 */ Long createAccount(@Valid MpAccountCreateReqVO createReqVO); /** * 更新公众号账号 * * @param updateReqVO 更新信息 */ void updateAccount(@Valid MpAccountUpdateReqVO updateReqVO); /** * 删除公众号账号 * * @param id 编号 */ void deleteAccount(Long id); /** * 获得公众号账号 * * @param id 编号 * @return 公众号账号 */ MpAccountDO getAccount(Long id); /** * 获得公众号账号。若不存在,则抛出业务异常 * * @param id 编号 * @return 公众号账号 */ default MpAccountDO getRequiredAccount(Long id) { MpAccountDO account = getAccount(id); if (account == null) { throw exception(ACCOUNT_NOT_EXISTS); } return account; } /** * 从缓存中,获得公众号账号 * * @param appId 微信公众号 appId * @return 公众号账号 */ MpAccountDO getAccountFromCache(String appId); /** * 获得公众号账号分页 * * @param pageReqVO 分页查询 * @return 公众号账号分页 */ PageResult getAccountPage(MpAccountPageReqVO pageReqVO); /** * 获得公众号账号列表 * * @return 公众号账号列表 */ List getAccountList(); /** * 生成公众号账号的二维码 * * @param id 编号 */ void generateAccountQrCode(Long id); /** * 清空公众号账号的 API 配额 * * 参考文档:接口调用频次限制说明 * * @param id 编号 */ void clearAccountQuota(Long id); } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/account/MpAccountServiceImpl.java ================================================ package co.yixiang.yshop.module.mp.service.account; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjUtil; import co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.tenant.core.util.TenantUtils; import co.yixiang.yshop.module.mp.controller.admin.account.vo.MpAccountCreateReqVO; import co.yixiang.yshop.module.mp.controller.admin.account.vo.MpAccountPageReqVO; import co.yixiang.yshop.module.mp.controller.admin.account.vo.MpAccountUpdateReqVO; import co.yixiang.yshop.module.mp.convert.account.MpAccountConvert; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import co.yixiang.yshop.module.mp.dal.mysql.account.MpAccountMapper; import co.yixiang.yshop.module.mp.enums.ErrorCodeConstants; import co.yixiang.yshop.module.mp.framework.mp.core.MpServiceFactory; import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; import com.google.common.annotations.VisibleForTesting; import jakarta.annotation.PostConstruct; import jakarta.annotation.Resource; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket; import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.time.LocalDateTime; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertMap; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.getMaxValue; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.USER_USERNAME_EXISTS; /** * 公众号账号 Service 实现类 * * @author fengdan */ @Slf4j @Service @Validated public class MpAccountServiceImpl implements MpAccountService { /** * 账号缓存 * key:账号编号 {@link MpAccountDO#getAppId()} * * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 */ @Getter private volatile Map accountCache; @Resource private MpAccountMapper mpAccountMapper; @Resource @Lazy // 延迟加载,解决循环依赖的问题 private MpServiceFactory mpServiceFactory; @Resource private RedisTemplateWxRedisOps redisTemplateWxRedisOps; @Resource private WxMpProperties mpProperties; @Override @PostConstruct public void initLocalCache() { // 注意:忽略自动多租户,因为要全局初始化缓存 TenantUtils.executeIgnore(() -> { // 第一步:查询数据 List accounts = Collections.emptyList(); try { accounts = mpAccountMapper.selectList(); } catch (Throwable ex) { if (!ex.getMessage().contains("doesn't exist")) { throw ex; } log.error("[微信公众号 yshop-module-mp - 表结构未导入][参考 https://www.yixiang.co/mp/build/ 开启]"); } log.info("[initLocalCacheIfUpdate][缓存公众号账号,数量为:{}]", accounts.size()); // 第二步:构建缓存。创建或更新支付 Client mpServiceFactory.init(accounts); accountCache = convertMap(accounts, MpAccountDO::getAppId); }); } /** * 通过定时任务轮询,刷新缓存 * * 目的:多节点部署时,通过轮询”通知“所有节点,进行刷新 */ @Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS) public void refreshLocalCache() { // 注意:忽略自动多租户,因为要全局初始化缓存 TenantUtils.executeIgnore(() -> { // 情况一:如果缓存里没有数据,则直接刷新缓存 if (CollUtil.isEmpty(accountCache)) { initLocalCache(); return; } // 情况二,如果缓存里数据,则通过 updateTime 判断是否有数据变更,有变更则刷新缓存 LocalDateTime maxTime = getMaxValue(accountCache.values(), MpAccountDO::getUpdateTime); if (mpAccountMapper.selectCountByUpdateTimeGt(maxTime) > 0) { initLocalCache(); } }); } @Override public Long createAccount(MpAccountCreateReqVO createReqVO) { // 校验 appId 唯一 validateAppIdUnique(null, createReqVO.getAppId()); // 插入 MpAccountDO account = MpAccountConvert.INSTANCE.convert(createReqVO); mpAccountMapper.insert(account); // 刷新缓存 initLocalCache(); return account.getId(); } @Override public void updateAccount(MpAccountUpdateReqVO updateReqVO) { // 校验存在 validateAccountExists(updateReqVO.getId()); // 校验 appId 唯一 validateAppIdUnique(updateReqVO.getId(), updateReqVO.getAppId()); // 更新 MpAccountDO updateObj = MpAccountConvert.INSTANCE.convert(updateReqVO); mpAccountMapper.updateById(updateObj); // 刷新缓存 initLocalCache(); } @Override public void deleteAccount(Long id) { // 校验存在 validateAccountExists(id); // 删除 mpAccountMapper.deleteById(id); // 刷新缓存 initLocalCache(); } private MpAccountDO validateAccountExists(Long id) { MpAccountDO account = mpAccountMapper.selectById(id); if (account == null) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.ACCOUNT_NOT_EXISTS); } return account; } @VisibleForTesting public void validateAppIdUnique(Long id, String appId) { // 多个租户,appId 是不能重复,否则公众号回调会无法识别 TenantUtils.executeIgnore(() -> { MpAccountDO account = mpAccountMapper.selectByAppId(appId); if (account == null) { return; } // 存在 account 记录的情况下 if (id == null // 新增时,说明重复 || ObjUtil.notEqual(id, account.getId())) { // 更新时,如果 id 不一致,说明重复 throw exception(USER_USERNAME_EXISTS); } }); } @Override public MpAccountDO getAccount(Long id) { return mpAccountMapper.selectById(id); } @Override public MpAccountDO getAccountFromCache(String appId) { return accountCache.get(appId); } @Override public PageResult getAccountPage(MpAccountPageReqVO pageReqVO) { return mpAccountMapper.selectPage(pageReqVO); } @Override public List getAccountList() { return mpAccountMapper.selectList(); } @Override public void generateAccountQrCode(Long id) { // 校验存在 MpAccountDO account = validateAccountExists(id); // 生成二维码 WxMpService mpService = mpServiceFactory.getRequiredMpService(account.getAppId()); String qrCodeUrl; try { WxMpQrCodeTicket qrCodeTicket = mpService.getQrcodeService().qrCodeCreateLastTicket("default"); qrCodeUrl = mpService.getQrcodeService().qrCodePictureUrl(qrCodeTicket.getTicket()); } catch (WxErrorException e) { throw exception(ErrorCodeConstants.ACCOUNT_GENERATE_QR_CODE_FAIL, e.getError().getErrorMsg()); } // 保存二维码 mpAccountMapper.updateById(new MpAccountDO().setId(id).setQrCodeUrl(qrCodeUrl)); } @Override public void clearAccountQuota(Long id) { // 校验存在 MpAccountDO account = validateAccountExists(id); // 生成二维码 WxMpService mpService = mpServiceFactory.getRequiredMpService(account.getAppId()); try { mpService.clearQuota(account.getAppId()); } catch (WxErrorException e) { throw exception(ErrorCodeConstants.ACCOUNT_CLEAR_QUOTA_FAIL, e.getError().getErrorMsg()); } } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/handler/menu/MenuHandler.java ================================================ package co.yixiang.yshop.module.mp.service.handler.menu; import co.yixiang.yshop.module.mp.framework.mp.core.context.MpContextHolder; import co.yixiang.yshop.module.mp.service.menu.MpMenuService; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.mp.api.WxMpMenuService; import me.chanjar.weixin.mp.api.WxMpMessageHandler; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; import java.util.Map; import static me.chanjar.weixin.common.api.WxConsts.MenuButtonType; /** * 自定义菜单的事件处理器 * * 逻辑:粉丝点击菜单时,触发对应的回复 * * @author yshop */ @Component public class MenuHandler implements WxMpMessageHandler { @Resource private MpMenuService mpMenuService; @Override public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, WxMpService weixinService, WxSessionManager sessionManager) { return mpMenuService.reply(MpContextHolder.getAppId(), wxMessage.getEventKey(), wxMessage.getFromUser()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/handler/message/MessageAutoReplyHandler.java ================================================ package co.yixiang.yshop.module.mp.service.handler.message; import co.yixiang.yshop.module.mp.dal.dataobject.message.MpAutoReplyDO; import co.yixiang.yshop.module.mp.framework.mp.core.context.MpContextHolder; import co.yixiang.yshop.module.mp.service.message.MpAutoReplyService; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.mp.api.WxMpMessageHandler; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; import java.util.Map; /** * 自动回复消息的事件处理器 * * @author yshop */ @Component @Slf4j public class MessageAutoReplyHandler implements WxMpMessageHandler { @Resource private MpAutoReplyService mpAutoReplyService; @Override public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, WxMpService weixinService, WxSessionManager sessionManager) { // 只处理指定类型的消息 if (!MpAutoReplyDO.REQUEST_MESSAGE_TYPE.contains(wxMessage.getMsgType())) { return null; } // 自动回复 return mpAutoReplyService.replyForMessage(MpContextHolder.getAppId(), wxMessage); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/handler/message/MessageReceiveHandler.java ================================================ package co.yixiang.yshop.module.mp.service.handler.message; import co.yixiang.yshop.module.mp.framework.mp.core.context.MpContextHolder; import co.yixiang.yshop.module.mp.service.message.MpMessageService; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.mp.api.WxMpMessageHandler; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; import java.util.Map; /** * 保存微信消息的事件处理器 * * @author yshop */ @Component @Slf4j public class MessageReceiveHandler implements WxMpMessageHandler { @Resource private MpMessageService mpMessageService; @Override public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, WxMpService wxMpService, WxSessionManager sessionManager) { log.info("[handle][接收到请求消息,内容:{}]", wxMessage); mpMessageService.receiveMessage(MpContextHolder.getAppId(), wxMessage); return null; } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/handler/other/KfSessionHandler.java ================================================ package co.yixiang.yshop.module.mp.service.handler.other; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.mp.api.WxMpMessageHandler; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import org.springframework.stereotype.Component; import java.util.Map; /** * 接收客服会话管理的事件处理器 * * @author yshop */ @Component public class KfSessionHandler implements WxMpMessageHandler { @Override public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, WxMpService wxMpService, WxSessionManager sessionManager) { throw new UnsupportedOperationException("未实现该处理,请自行重写"); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/handler/other/NullHandler.java ================================================ package co.yixiang.yshop.module.mp.service.handler.other; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.mp.api.WxMpMessageHandler; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import org.springframework.stereotype.Component; import java.util.Map; /** * 点击菜单连接的事件处理器 */ @Component public class NullHandler implements WxMpMessageHandler { @Override public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, WxMpService wxMpService, WxSessionManager sessionManager) { throw new UnsupportedOperationException("未实现该处理,请自行重写"); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/handler/other/ScanHandler.java ================================================ package co.yixiang.yshop.module.mp.service.handler.other; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.mp.api.WxMpMessageHandler; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import org.springframework.stereotype.Component; import java.util.Map; /** * 扫码的事件处理器 */ @Component public class ScanHandler implements WxMpMessageHandler { @Override public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map context, WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException { throw new UnsupportedOperationException("未实现该处理,请自行重写"); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/handler/other/StoreCheckNotifyHandler.java ================================================ package co.yixiang.yshop.module.mp.service.handler.other; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.mp.api.WxMpMessageHandler; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import org.springframework.stereotype.Component; import java.util.Map; /** * 门店审核事件的事件处理器 */ @Component public class StoreCheckNotifyHandler implements WxMpMessageHandler { @Override public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, WxMpService wxMpService, WxSessionManager sessionManager) { throw new UnsupportedOperationException("未实现该处理,请自行重写"); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/handler/other/package-info.java ================================================ /** * 本包内的 handler 都是一些不重要的,所以放在 other 其它里 */ package co.yixiang.yshop.module.mp.service.handler.other; ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/handler/user/LocationHandler.java ================================================ package co.yixiang.yshop.module.mp.service.handler.user; import cn.hutool.core.util.ObjectUtil; import co.yixiang.yshop.framework.common.util.object.ObjectUtils; import co.yixiang.yshop.module.mp.framework.mp.core.context.MpContextHolder; import co.yixiang.yshop.module.mp.service.message.MpAutoReplyService; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.api.WxConsts; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.mp.api.WxMpMessageHandler; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; import java.util.Map; /** * 上报地理位置的事件处理器 * * 触发操作:打开微信公众号 -> 点击 + 号 -> 选择「语音」 * * 逻辑:粉丝上传地理位置时,也可以触发自动回复 * * @author yshop */ @Component @Slf4j public class LocationHandler implements WxMpMessageHandler { @Resource private MpAutoReplyService mpAutoReplyService; @Override public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, WxMpService wxMpService, WxSessionManager sessionManager) { // 防御性编程:必须是 LOCATION 消息 if (ObjectUtil.notEqual(wxMessage.getMsgType(), WxConsts.XmlMsgType.LOCATION)) { return null; } log.info("[handle][上报地理位置,纬度({})、经度({})、精度({})", wxMessage.getLatitude(), wxMessage.getLongitude(), wxMessage.getPrecision()); // 自动回复 return mpAutoReplyService.replyForMessage(MpContextHolder.getAppId(), wxMessage); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/handler/user/SubscribeHandler.java ================================================ package co.yixiang.yshop.module.mp.service.handler.user; import co.yixiang.yshop.module.mp.framework.mp.core.context.MpContextHolder; import co.yixiang.yshop.module.mp.service.message.MpAutoReplyService; import co.yixiang.yshop.module.mp.service.user.MpUserService; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.mp.api.WxMpMessageHandler; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import me.chanjar.weixin.mp.bean.result.WxMpUser; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; import java.util.Map; /** * 关注的事件处理器 * * @author yshop */ @Component @Slf4j public class SubscribeHandler implements WxMpMessageHandler { @Resource private MpUserService mpUserService; @Resource private MpAutoReplyService mpAutoReplyService; @Override public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, WxMpService weixinService, WxSessionManager sessionManager) throws WxErrorException { // 第一步,从公众号平台,获取粉丝信息 log.info("[handle][粉丝({}) 关注]", wxMessage.getFromUser()); WxMpUser wxMpUser = null; try { wxMpUser = weixinService.getUserService().userInfo(wxMessage.getFromUser()); } catch (WxErrorException e) { log.error("[handle][粉丝({})] 获取粉丝信息失败!", wxMessage.getFromUser(), e); } // 第二步,保存粉丝信息 mpUserService.saveUser(MpContextHolder.getAppId(), wxMpUser); // 第三步,回复关注的欢迎语 return mpAutoReplyService.replyForSubscribe(MpContextHolder.getAppId(), wxMessage); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/handler/user/UnsubscribeHandler.java ================================================ package co.yixiang.yshop.module.mp.service.handler.user; import co.yixiang.yshop.module.mp.framework.mp.core.context.MpContextHolder; import co.yixiang.yshop.module.mp.service.user.MpUserService; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.mp.api.WxMpMessageHandler; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; import java.util.Map; /** * 取消关注的事件处理器 * * @author yshop */ @Component @Slf4j public class UnsubscribeHandler implements WxMpMessageHandler { @Resource @Lazy // 延迟加载,解决循环依赖的问题 private MpUserService mpUserService; @Override public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, WxMpService wxMpService, WxSessionManager sessionManager) { log.info("[handle][粉丝({}) 取消关注]", wxMessage.getFromUser()); mpUserService.updateUserUnsubscribe(MpContextHolder.getAppId(), wxMessage.getFromUser()); return null; } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/material/MpMaterialService.java ================================================ package co.yixiang.yshop.module.mp.service.material; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.mp.controller.admin.material.vo.MpMaterialPageReqVO; import co.yixiang.yshop.module.mp.controller.admin.material.vo.MpMaterialUploadNewsImageReqVO; import co.yixiang.yshop.module.mp.controller.admin.material.vo.MpMaterialUploadPermanentReqVO; import co.yixiang.yshop.module.mp.controller.admin.material.vo.MpMaterialUploadTemporaryReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.material.MpMaterialDO; import me.chanjar.weixin.common.api.WxConsts; import jakarta.validation.Valid; import java.io.IOException; import java.util.Collection; import java.util.List; /** * 公众号素材 Service 接口 * * @author yshop */ public interface MpMaterialService { /** * 获得素材的 URL * * 该 URL 来自我们自己的文件服务器存储的 URL,不是公众号存储的 URL * * @param accountId 公众号账号编号 * @param mediaId 公众号素材 id * @param type 文件类型 {@link WxConsts.MediaFileType} * @return 素材的 URL */ String downloadMaterialUrl(Long accountId, String mediaId, String type); /** * 上传临时素材 * * @param reqVO 请求 * @return 素材 * @throws IOException 文件操作发生异常 */ MpMaterialDO uploadTemporaryMaterial(@Valid MpMaterialUploadTemporaryReqVO reqVO) throws IOException; /** * 上传永久素材 * * @param reqVO 请求 * @return 素材 * @throws IOException 文件操作发生异常 */ MpMaterialDO uploadPermanentMaterial(@Valid MpMaterialUploadPermanentReqVO reqVO) throws IOException; /** * 上传图文内容中的图片 * * @param reqVO 上传请求 * @return 图片地址 */ String uploadNewsImage(MpMaterialUploadNewsImageReqVO reqVO) throws IOException; /** * 获得素材分页 * * @param pageReqVO 分页请求 * @return 素材分页 */ PageResult getMaterialPage(MpMaterialPageReqVO pageReqVO); /** * 获得素材列表 * * @param mediaIds 素材 mediaId 列表 * @return 素材列表 */ List getMaterialListByMediaId(Collection mediaIds); /** * 删除素材 * * @param id 编号 */ void deleteMaterial(Long id); } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/material/MpMaterialServiceImpl.java ================================================ package co.yixiang.yshop.module.mp.service.material; import cn.hutool.core.io.FileTypeUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.infra.api.file.FileApi; import co.yixiang.yshop.module.mp.controller.admin.material.vo.MpMaterialPageReqVO; import co.yixiang.yshop.module.mp.controller.admin.material.vo.MpMaterialUploadNewsImageReqVO; import co.yixiang.yshop.module.mp.controller.admin.material.vo.MpMaterialUploadPermanentReqVO; import co.yixiang.yshop.module.mp.controller.admin.material.vo.MpMaterialUploadTemporaryReqVO; import co.yixiang.yshop.module.mp.convert.material.MpMaterialConvert; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import co.yixiang.yshop.module.mp.dal.dataobject.material.MpMaterialDO; import co.yixiang.yshop.module.mp.dal.mysql.material.MpMaterialMapper; import co.yixiang.yshop.module.mp.framework.mp.core.MpServiceFactory; import co.yixiang.yshop.module.mp.service.account.MpAccountService; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.material.WxMpMaterialUploadResult; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.io.File; import java.io.IOException; import java.util.Collection; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.mp.enums.ErrorCodeConstants.*; /** * 公众号素材 Service 接口 * * @author yshop */ @Service @Validated @Slf4j public class MpMaterialServiceImpl implements MpMaterialService { @Resource private MpMaterialMapper mpMaterialMapper; @Resource private FileApi fileApi; @Resource @Lazy // 延迟加载,解决循环依赖的问题 private MpAccountService mpAccountService; @Resource @Lazy // 延迟加载,解决循环依赖的问题 private MpServiceFactory mpServiceFactory; @Override public String downloadMaterialUrl(Long accountId, String mediaId, String type) { // 第一步,直接从数据库查询。如果已经下载,直接返回 MpMaterialDO material = mpMaterialMapper.selectByAccountIdAndMediaId(accountId, mediaId); if (material != null) { return material.getUrl(); } // 第二步,尝试从临时素材中下载 String url = downloadMedia(accountId, mediaId); if (url == null) { return null; } MpAccountDO account = mpAccountService.getRequiredAccount(accountId); material = MpMaterialConvert.INSTANCE.convert(mediaId, type, url, account, null) .setPermanent(false); mpMaterialMapper.insert(material); // 不考虑下载永久素材,因为上传的时候已经保存 return url; } @Override public MpMaterialDO uploadTemporaryMaterial(MpMaterialUploadTemporaryReqVO reqVO) throws IOException { WxMpService mpService = mpServiceFactory.getRequiredMpService(reqVO.getAccountId()); // 第一步,上传到公众号 File file = null; WxMediaUploadResult result; String mediaId; String url; try { // 写入到临时文件 file = FileUtil.newFile(FileUtil.getTmpDirPath() + reqVO.getFile().getOriginalFilename()); reqVO.getFile().transferTo(file); // 上传到公众号 result = mpService.getMaterialService().mediaUpload(reqVO.getType(), file); // 上传到文件服务 mediaId = ObjUtil.defaultIfNull(result.getMediaId(), result.getThumbMediaId()); url = uploadFile(mediaId, file); } catch (WxErrorException e) { throw exception(MATERIAL_UPLOAD_FAIL, e.getError().getErrorMsg()); } finally { FileUtil.del(file); } // 第二步,存储到数据库 MpAccountDO account = mpAccountService.getRequiredAccount(reqVO.getAccountId()); MpMaterialDO material = MpMaterialConvert.INSTANCE.convert(mediaId, reqVO.getType(), url, account, reqVO.getFile().getName()).setPermanent(false); mpMaterialMapper.insert(material); return material; } @Override public MpMaterialDO uploadPermanentMaterial(MpMaterialUploadPermanentReqVO reqVO) throws IOException { WxMpService mpService = mpServiceFactory.getRequiredMpService(reqVO.getAccountId()); // 第一步,上传到公众号 String name = StrUtil.blankToDefault(reqVO.getName(), reqVO.getFile().getName()); File file = null; WxMpMaterialUploadResult result; String mediaId; String url; try { // 写入到临时文件 file = FileUtil.newFile(FileUtil.getTmpDirPath() + reqVO.getFile().getOriginalFilename()); reqVO.getFile().transferTo(file); // 上传到公众号 result = mpService.getMaterialService().materialFileUpload(reqVO.getType(), MpMaterialConvert.INSTANCE.convert(name, file, reqVO.getTitle(), reqVO.getIntroduction())); // 上传到文件服务 mediaId = ObjUtil.defaultIfNull(result.getMediaId(), result.getMediaId()); url = uploadFile(mediaId, file); } catch (WxErrorException e) { throw exception(MATERIAL_UPLOAD_FAIL, e.getError().getErrorMsg()); } finally { FileUtil.del(file); } // 第二步,存储到数据库 MpAccountDO account = mpAccountService.getRequiredAccount(reqVO.getAccountId()); MpMaterialDO material = MpMaterialConvert.INSTANCE.convert(mediaId, reqVO.getType(), url, account, name, reqVO.getTitle(), reqVO.getIntroduction(), result.getUrl()).setPermanent(true); mpMaterialMapper.insert(material); return material; } @Override public String uploadNewsImage(MpMaterialUploadNewsImageReqVO reqVO) throws IOException { WxMpService mpService = mpServiceFactory.getRequiredMpService(reqVO.getAccountId()); File file = null; try { // 写入到临时文件 file = FileUtil.newFile(FileUtil.getTmpDirPath() + reqVO.getFile().getOriginalFilename()); reqVO.getFile().transferTo(file); // 上传到公众号 return mpService.getMaterialService().mediaImgUpload(file).getUrl(); } catch (WxErrorException e) { throw exception(MATERIAL_IMAGE_UPLOAD_FAIL, e.getError().getErrorMsg()); } finally { FileUtil.del(file); } } @Override public PageResult getMaterialPage(MpMaterialPageReqVO pageReqVO) { return mpMaterialMapper.selectPage(pageReqVO); } @Override public List getMaterialListByMediaId(Collection mediaIds) { return mpMaterialMapper.selectListByMediaId(mediaIds); } @Override public void deleteMaterial(Long id) { MpMaterialDO material = mpMaterialMapper.selectById(id); if (material == null) { throw exception(MATERIAL_NOT_EXISTS); } // 第一步,从公众号删除 if (material.getPermanent()) { WxMpService mpService = mpServiceFactory.getRequiredMpService(material.getAppId()); try { mpService.getMaterialService().materialDelete(material.getMediaId()); } catch (WxErrorException e) { throw exception(MATERIAL_DELETE_FAIL, e.getError().getErrorMsg()); } } // 第二步,从数据库中删除 mpMaterialMapper.deleteById(id); } /** * 下载微信媒体文件的内容,并上传到文件服务 * * 为什么要下载?媒体文件在微信后台保存时间为 3 天,即 3 天后 media_id 失效。 * * @param accountId 公众号账号的编号 * @param mediaId 媒体文件编号 * @return 上传后的 URL */ public String downloadMedia(Long accountId, String mediaId) { WxMpService mpService = mpServiceFactory.getMpService(accountId); for (int i = 0; i < 3; i++) { try { // 第一步,从公众号下载媒体文件 File file = mpService.getMaterialService().mediaDownload(mediaId); // 第二步,上传到文件服务 return uploadFile(mediaId, file); } catch (WxErrorException e) { log.error("[mediaDownload][media({}) 第 ({}) 次下载失败]", mediaId, i); } } return null; } private String uploadFile(String mediaId, File file) { String path = mediaId + "." + FileTypeUtil.getType(file); return fileApi.createFile(path, FileUtil.readBytes(file)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/menu/MpMenuService.java ================================================ package co.yixiang.yshop.module.mp.service.menu; import co.yixiang.yshop.module.mp.controller.admin.menu.vo.MpMenuSaveReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.menu.MpMenuDO; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import jakarta.validation.Valid; import java.util.List; /** * 公众号菜单 Service 接口 * * @author yshop */ public interface MpMenuService { /** * 保存公众号菜单 * * @param createReqVO 创建信息 */ void saveMenu(@Valid MpMenuSaveReqVO createReqVO); /** * 删除公众号菜单 * * @param accountId 公众号账号的编号 */ void deleteMenuByAccountId(Long accountId); /** * 粉丝点击菜单按钮时,回复对应的消息 * * @param appId 公众号 AppId * @param key 菜单按钮的标识 * @param openid 粉丝的 openid * @return 消息 */ WxMpXmlOutMessage reply(String appId, String key, String openid); /** * 获得公众号菜单列表 * * @param accountId 公众号账号的编号 * @return 公众号菜单列表 */ List getMenuListByAccountId(Long accountId); } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/menu/MpMenuServiceImpl.java ================================================ package co.yixiang.yshop.module.mp.service.menu; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.module.mp.controller.admin.menu.vo.MpMenuSaveReqVO; import co.yixiang.yshop.module.mp.convert.menu.MpMenuConvert; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import co.yixiang.yshop.module.mp.dal.dataobject.menu.MpMenuDO; import co.yixiang.yshop.module.mp.dal.mysql.menu.MpMenuMapper; import co.yixiang.yshop.module.mp.framework.mp.core.MpServiceFactory; import co.yixiang.yshop.module.mp.framework.mp.core.util.MpUtils; import co.yixiang.yshop.module.mp.service.account.MpAccountService; import co.yixiang.yshop.module.mp.service.message.MpMessageService; import co.yixiang.yshop.module.mp.service.message.bo.MpMessageSendOutReqBO; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.bean.menu.WxMenu; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import jakarta.validation.Validator; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.mp.enums.ErrorCodeConstants.MENU_DELETE_FAIL; import static co.yixiang.yshop.module.mp.enums.ErrorCodeConstants.MENU_SAVE_FAIL; /** * 公众号菜单 Service 实现类 * * @author yshop */ @Service @Validated @Slf4j public class MpMenuServiceImpl implements MpMenuService { @Resource private MpMessageService mpMessageService; @Resource @Lazy // 延迟加载,避免循环引用报错 private MpAccountService mpAccountService; @Resource @Lazy // 延迟加载,避免循环引用报错 private MpServiceFactory mpServiceFactory; @Resource private Validator validator; @Resource private MpMenuMapper mpMenuMapper; @Override @Transactional(rollbackFor = Exception.class) public void saveMenu(MpMenuSaveReqVO createReqVO) { MpAccountDO account = mpAccountService.getRequiredAccount(createReqVO.getAccountId()); WxMpService mpService = mpServiceFactory.getRequiredMpService(createReqVO.getAccountId()); // 参数校验 createReqVO.getMenus().forEach(this::validateMenu); // 第一步,同步公众号 WxMenu wxMenu = new WxMenu(); wxMenu.setButtons(MpMenuConvert.INSTANCE.convert(createReqVO.getMenus())); try { mpService.getMenuService().menuCreate(wxMenu); } catch (WxErrorException e) { throw exception(MENU_SAVE_FAIL, e.getError().getErrorMsg()); } // 第二步,存储到数据库 mpMenuMapper.deleteByAccountId(createReqVO.getAccountId()); createReqVO.getMenus().forEach(menu -> { // 先保存顶级菜单 MpMenuDO menuDO = createMenu(menu, null, account); // 再保存子菜单 if (CollUtil.isEmpty(menu.getChildren())) { return; } menu.getChildren().forEach(childMenu -> createMenu(childMenu, menuDO, account)); }); } /** * 校验菜单的格式是否正确 * * @param menu 菜单 */ private void validateMenu(MpMenuSaveReqVO.Menu menu) { MpUtils.validateButton(validator, menu.getType(), menu.getReplyMessageType(), menu); // 子菜单 if (CollUtil.isEmpty(menu.getChildren())) { return; } menu.getChildren().forEach(this::validateMenu); } /** * 创建菜单,并存储到数据库 * * @param wxMenu 菜单信息 * @param parentMenu 父菜单 * @param account 公众号账号 * @return 创建后的菜单 */ private MpMenuDO createMenu(MpMenuSaveReqVO.Menu wxMenu, MpMenuDO parentMenu, MpAccountDO account) { // 创建菜单 MpMenuDO menu = CollUtil.isNotEmpty(wxMenu.getChildren()) ? new MpMenuDO().setName(wxMenu.getName()) : MpMenuConvert.INSTANCE.convert02(wxMenu); // 设置菜单的公众号账号信息 if (account != null) { menu.setAccountId(account.getId()).setAppId(account.getAppId()); } // 设置父编号 if (parentMenu != null) { menu.setParentId(parentMenu.getId()); } else { menu.setParentId(MpMenuDO.ID_ROOT); } // 插入到数据库 mpMenuMapper.insert(menu); return menu; } @Override public void deleteMenuByAccountId(Long accountId) { WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); // 第一步,同步公众号 try { mpService.getMenuService().menuDelete(); } catch (WxErrorException e) { throw exception(MENU_DELETE_FAIL, e.getError().getErrorMsg()); } // 第二步,存储到数据库 mpMenuMapper.deleteByAccountId(accountId); } @Override public WxMpXmlOutMessage reply(String appId, String key, String openid) { // 第一步,获得菜单 MpMenuDO menu = mpMenuMapper.selectByAppIdAndMenuKey(appId, key); if (menu == null) { log.error("[reply][appId({}) key({}) 找不到对应的菜单]", appId, key); return null; } // 按钮必须要有消息类型,不然后续无法回复消息 if (StrUtil.isEmpty(menu.getReplyMessageType())) { log.error("[reply][menu({}) 不存在对应的消息类型]", menu); return null; } // 第二步,回复消息 MpMessageSendOutReqBO sendReqBO = MpMenuConvert.INSTANCE.convert(openid, menu); return mpMessageService.sendOutMessage(sendReqBO); } @Override public List getMenuListByAccountId(Long accountId) { return mpMenuMapper.selectListByAccountId(accountId); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/message/MpAutoReplyService.java ================================================ package co.yixiang.yshop.module.mp.service.message; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyCreateReqVO; import co.yixiang.yshop.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyUpdateReqVO; import co.yixiang.yshop.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.message.MpAutoReplyDO; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; /** * 公众号的自动回复 Service 接口 * * @author yshop */ public interface MpAutoReplyService { /** * 获得公众号自动回复分页 * * @param pageVO 分页请求 * @return 自动回复分页结果 */ PageResult getAutoReplyPage(MpMessagePageReqVO pageVO); /** * 获得公众号自动回复 * * @param id 编号 * @return 自动回复 */ MpAutoReplyDO getAutoReply(Long id); /** * 创建公众号自动回复 * * @param createReqVO 创建请求 * @return 自动回复的编号 */ Long createAutoReply(MpAutoReplyCreateReqVO createReqVO); /** * 更新公众号自动回复 * * @param updateReqVO 更新请求 */ void updateAutoReply(MpAutoReplyUpdateReqVO updateReqVO); /** * 删除公众号自动回复 * * @param id 自动回复的编号 */ void deleteAutoReply(Long id); /** * 当收到消息时,自动回复 * * @param appId 微信公众号 appId * @param wxMessage 消息 * @return 回复的消息 */ WxMpXmlOutMessage replyForMessage(String appId, WxMpXmlMessage wxMessage); /** * 当粉丝关注时,自动回复 * * @param appId 微信公众号 appId * @param wxMessage 消息 * @return 回复的消息 */ WxMpXmlOutMessage replyForSubscribe(String appId, WxMpXmlMessage wxMessage); } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/message/MpAutoReplyServiceImpl.java ================================================ package co.yixiang.yshop.module.mp.service.message; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjUtil; import co.yixiang.yshop.framework.common.exception.ErrorCode; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyCreateReqVO; import co.yixiang.yshop.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyUpdateReqVO; import co.yixiang.yshop.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; import co.yixiang.yshop.module.mp.convert.message.MpAutoReplyConvert; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import co.yixiang.yshop.module.mp.dal.dataobject.message.MpAutoReplyDO; import co.yixiang.yshop.module.mp.dal.mysql.message.MpAutoReplyMapper; import co.yixiang.yshop.module.mp.enums.message.MpAutoReplyTypeEnum; import co.yixiang.yshop.module.mp.framework.mp.core.util.MpUtils; import co.yixiang.yshop.module.mp.service.account.MpAccountService; import co.yixiang.yshop.module.mp.service.message.bo.MpMessageSendOutReqBO; import me.chanjar.weixin.common.api.WxConsts; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import jakarta.validation.Validator; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.mp.enums.ErrorCodeConstants.*; /** * 公众号的自动回复 Service 实现类 * * @author yshop */ @Service @Validated public class MpAutoReplyServiceImpl implements MpAutoReplyService { @Resource private MpMessageService mpMessageService; @Resource @Lazy // 延迟加载,避免循环依赖 private MpAccountService mpAccountService; @Resource private Validator validator; @Resource private MpAutoReplyMapper mpAutoReplyMapper; @Override public PageResult getAutoReplyPage(MpMessagePageReqVO pageVO) { return mpAutoReplyMapper.selectPage(pageVO); } @Override public MpAutoReplyDO getAutoReply(Long id) { return mpAutoReplyMapper.selectById(id); } @Override public Long createAutoReply(MpAutoReplyCreateReqVO createReqVO) { // 第一步,校验数据 if (createReqVO.getResponseMessageType() != null) { MpUtils.validateMessage(validator, createReqVO.getResponseMessageType(), createReqVO); } validateAutoReplyConflict(null, createReqVO.getAccountId(), createReqVO.getType(), createReqVO.getRequestKeyword(), createReqVO.getRequestMessageType()); // 第二步,插入数据 MpAccountDO account = mpAccountService.getRequiredAccount(createReqVO.getAccountId()); MpAutoReplyDO autoReply = MpAutoReplyConvert.INSTANCE.convert(createReqVO) .setAppId(account.getAppId()); mpAutoReplyMapper.insert(autoReply); return autoReply.getId(); } @Override public void updateAutoReply(MpAutoReplyUpdateReqVO updateReqVO) { // 第一步,校验数据 if (updateReqVO.getResponseMessageType() != null) { MpUtils.validateMessage(validator, updateReqVO.getResponseMessageType(), updateReqVO); } MpAutoReplyDO autoReply = validateAutoReplyExists(updateReqVO.getId()); validateAutoReplyConflict(updateReqVO.getId(), autoReply.getAccountId(), updateReqVO.getType(), updateReqVO.getRequestKeyword(), updateReqVO.getRequestMessageType()); // 第二步,更新数据 MpAutoReplyDO updateObj = MpAutoReplyConvert.INSTANCE.convert(updateReqVO) .setAccountId(null).setAppId(null); // 避免前端传递,更新着两个字段 mpAutoReplyMapper.updateById(updateObj); } /** * 校验自动回复是否冲突 * * 不同的 type,会有不同的逻辑: * 1. type = SUBSCRIBE 时,不允许有其他的自动回复 * 2. type = MESSAGE 时,校验 requestMessageType 已经存在自动回复 * 3. type = KEYWORD 时,校验 keyword 已经存在自动回复 * * @param id 自动回复编号 * @param accountId 公众号账号的编号 * @param type 类型 * @param requestKeyword 请求关键词 * @param requestMessageType 请求消息类型 */ private void validateAutoReplyConflict(Long id, Long accountId, Integer type, String requestKeyword, String requestMessageType) { // 获得已经存在的自动回复 MpAutoReplyDO autoReply = null; ErrorCode errorCode = null; if (MpAutoReplyTypeEnum.SUBSCRIBE.getType().equals(type)) { autoReply = mpAutoReplyMapper.selectByAccountIdAndSubscribe(accountId); errorCode = AUTO_REPLY_ADD_SUBSCRIBE_FAIL_EXISTS; } else if (MpAutoReplyTypeEnum.MESSAGE.getType().equals(type)) { autoReply = mpAutoReplyMapper.selectByAccountIdAndMessage(accountId, requestMessageType); errorCode = AUTO_REPLY_ADD_MESSAGE_FAIL_EXISTS; } else if (MpAutoReplyTypeEnum.KEYWORD.getType().equals(type)) { autoReply = mpAutoReplyMapper.selectByAccountIdAndKeyword(accountId, requestKeyword); errorCode = AUTO_REPLY_ADD_KEYWORD_FAIL_EXISTS; } if (autoReply == null) { return; } // 存在冲突,抛出业务异常 if (id == null // 情况一,新增(id == null),存在记录,说明冲突 || ObjUtil.notEqual(id, autoReply.getId())) { // 情况二,修改(id != null),id 不匹配,说明冲突 throw exception(errorCode); } } @Override public void deleteAutoReply(Long id) { // 校验粉丝存在 validateAutoReplyExists(id); // 删除自动回复 mpAutoReplyMapper.deleteById(id); } private MpAutoReplyDO validateAutoReplyExists(Long id) { MpAutoReplyDO autoReply = mpAutoReplyMapper.selectById(id); if (autoReply == null) { throw exception(AUTO_REPLY_NOT_EXISTS); } return autoReply; } @Override public WxMpXmlOutMessage replyForMessage(String appId, WxMpXmlMessage wxMessage) { // 第一步,匹配自动回复 List replies = null; // 1.1 关键字 if (wxMessage.getMsgType().equals(WxConsts.XmlMsgType.TEXT)) { // 完全匹配 replies = mpAutoReplyMapper.selectListByAppIdAndKeywordAll(appId, wxMessage.getContent()); if (CollUtil.isEmpty(replies)) { // 模糊匹配 replies = mpAutoReplyMapper.selectListByAppIdAndKeywordLike(appId, wxMessage.getContent()); } } // 1.2 消息类型 if (CollUtil.isEmpty(replies)) { replies = mpAutoReplyMapper.selectListByAppIdAndMessage(appId, wxMessage.getMsgType()); } if (CollUtil.isEmpty(replies)) { return null; } MpAutoReplyDO reply = CollUtil.getFirst(replies); // 第二步,基于自动回复,创建消息 MpMessageSendOutReqBO sendReqBO = MpAutoReplyConvert.INSTANCE.convert(wxMessage.getFromUser(), reply); return mpMessageService.sendOutMessage(sendReqBO); } @Override public WxMpXmlOutMessage replyForSubscribe(String appId, WxMpXmlMessage wxMessage) { // 第一步,匹配自动回复 List replies = mpAutoReplyMapper.selectListByAppIdAndSubscribe(appId); MpAutoReplyDO reply = CollUtil.isNotEmpty(replies) ? CollUtil.getFirst(replies) : buildDefaultSubscribeAutoReply(appId); // 如果不存在,提供一个默认末班 // 第二步,基于自动回复,创建消息 MpMessageSendOutReqBO sendReqBO = MpAutoReplyConvert.INSTANCE.convert(wxMessage.getFromUser(), reply); return mpMessageService.sendOutMessage(sendReqBO); } private MpAutoReplyDO buildDefaultSubscribeAutoReply(String appId) { MpAccountDO account = mpAccountService.getAccountFromCache(appId); Assert.notNull(account, "公众号账号({}) 不存在", appId); // 构建默认的【关注】自动回复 return new MpAutoReplyDO().setAppId(appId).setAccountId(account.getId()) .setType(MpAutoReplyTypeEnum.SUBSCRIBE.getType()) .setResponseMessageType(WxConsts.XmlMsgType.TEXT).setResponseContent("感谢关注"); } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/message/MpMessageService.java ================================================ package co.yixiang.yshop.module.mp.service.message; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; import co.yixiang.yshop.module.mp.controller.admin.message.vo.message.MpMessageSendReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.message.MpMessageDO; import co.yixiang.yshop.module.mp.service.message.bo.MpMessageSendOutReqBO; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import jakarta.validation.Valid; /** * 公众号消息 Service 接口 * * @author yshop */ public interface MpMessageService { /** * 获得公众号消息分页 * * @param pageReqVO 分页查询 * @return 公众号消息分页 */ PageResult getMessagePage(MpMessagePageReqVO pageReqVO); /** * 从公众号,接收到粉丝消息 * * @param appId 微信公众号 appId * @param wxMessage 消息 */ void receiveMessage(String appId, WxMpXmlMessage wxMessage); /** * 使用公众号,给粉丝回复消息 * * 例如说:自动回复、客服消息、菜单回复消息等场景 * * 注意,该方法只是返回 WxMpXmlOutMessage 对象,不会真的发送消息 * * @param sendReqBO 消息内容 * @return 微信回复消息 XML */ WxMpXmlOutMessage sendOutMessage(@Valid MpMessageSendOutReqBO sendReqBO); /** * 使用公众号,给粉丝发送【客服】消息 * * 注意,该方法会真实发送消息 * * @param sendReqVO 消息内容 * @return 消息 */ MpMessageDO sendKefuMessage(MpMessageSendReqVO sendReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/message/MpMessageServiceImpl.java ================================================ package co.yixiang.yshop.module.mp.service.message; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; import co.yixiang.yshop.module.mp.controller.admin.message.vo.message.MpMessageSendReqVO; import co.yixiang.yshop.module.mp.convert.message.MpMessageConvert; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import co.yixiang.yshop.module.mp.dal.dataobject.message.MpMessageDO; import co.yixiang.yshop.module.mp.dal.dataobject.user.MpUserDO; import co.yixiang.yshop.module.mp.dal.mysql.message.MpMessageMapper; import co.yixiang.yshop.module.mp.enums.message.MpMessageSendFromEnum; import co.yixiang.yshop.module.mp.framework.mp.core.MpServiceFactory; import co.yixiang.yshop.module.mp.framework.mp.core.util.MpUtils; import co.yixiang.yshop.module.mp.service.account.MpAccountService; import co.yixiang.yshop.module.mp.service.material.MpMaterialService; import co.yixiang.yshop.module.mp.service.message.bo.MpMessageSendOutReqBO; import co.yixiang.yshop.module.mp.service.user.MpUserService; import jakarta.annotation.Resource; import jakarta.validation.Validator; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.api.WxConsts; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.mp.enums.ErrorCodeConstants.MESSAGE_SEND_FAIL; /** * 粉丝消息 Service 实现类 * * @author yshop */ @Service @Validated @Slf4j public class MpMessageServiceImpl implements MpMessageService { @Resource @Lazy // 延迟加载,避免循环依赖 private MpAccountService mpAccountService; @Resource private MpUserService mpUserService; @Resource private MpMaterialService mpMaterialService; @Resource private MpMessageMapper mpMessageMapper; @Resource @Lazy // 延迟加载,解决循环依赖的问题 private MpServiceFactory mpServiceFactory; @Resource private Validator validator; @Override public PageResult getMessagePage(MpMessagePageReqVO pageReqVO) { return mpMessageMapper.selectPage(pageReqVO); } @Override public void receiveMessage(String appId, WxMpXmlMessage wxMessage) { // 获得关联信息 MpAccountDO account = mpAccountService.getAccountFromCache(appId); Assert.notNull(account, "公众号账号({}) 不存在", appId); // 订阅事件不记录,因为此时公众号粉丝表中还没有此粉丝的数据 // TODO @yshop:这个修复,后续看看还有啥问题 if (ObjUtil.equal(wxMessage.getEvent(), WxConsts.EventType.SUBSCRIBE)) { return; } MpUserDO user = mpUserService.getUser(appId, wxMessage.getFromUser()); Assert.notNull(user, "公众号粉丝({}/{}) 不存在", appId, wxMessage.getFromUser()); // 记录消息 MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user) .setSendFrom(MpMessageSendFromEnum.USER_TO_MP.getFrom()); downloadMessageMedia(message); mpMessageMapper.insert(message); } @Override public WxMpXmlOutMessage sendOutMessage(MpMessageSendOutReqBO sendReqBO) { // 校验消息格式 MpUtils.validateMessage(validator, sendReqBO.getType(), sendReqBO); // 获得关联信息 MpAccountDO account = mpAccountService.getAccountFromCache(sendReqBO.getAppId()); Assert.notNull(account, "公众号账号({}) 不存在", sendReqBO.getAppId()); MpUserDO user = mpUserService.getUser(sendReqBO.getAppId(), sendReqBO.getOpenid()); Assert.notNull(user, "公众号粉丝({}/{}) 不存在", sendReqBO.getAppId(), sendReqBO.getOpenid()); // 记录消息 MpMessageDO message = MpMessageConvert.INSTANCE.convert(sendReqBO, account, user). setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom()); downloadMessageMedia(message); mpMessageMapper.insert(message); // 转换返回 WxMpXmlOutMessage 对象 return MpMessageConvert.INSTANCE.convert02(message, account); } @Override public MpMessageDO sendKefuMessage(MpMessageSendReqVO sendReqVO) { // 校验消息格式 MpUtils.validateMessage(validator, sendReqVO.getType(), sendReqVO); // 获得关联信息 MpUserDO user = mpUserService.getRequiredUser(sendReqVO.getUserId()); MpAccountDO account = mpAccountService.getRequiredAccount(user.getAccountId()); // 发送客服消息 WxMpKefuMessage wxMessage = MpMessageConvert.INSTANCE.convert(sendReqVO, user); WxMpService mpService = mpServiceFactory.getRequiredMpService(user.getAppId()); try { mpService.getKefuService().sendKefuMessageWithResponse(wxMessage); } catch (WxErrorException e) { throw exception(MESSAGE_SEND_FAIL, e.getError().getErrorMsg()); } // 记录消息 MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user) .setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom()); downloadMessageMedia(message); mpMessageMapper.insert(message); return message; } /** * 下载消息使用到的媒体文件,并上传到文件服务 * * @param message 消息 */ private void downloadMessageMedia(MpMessageDO message) { if (StrUtil.isNotEmpty(message.getMediaId())) { message.setMediaUrl(mpMaterialService.downloadMaterialUrl(message.getAccountId(), message.getMediaId(), MpUtils.getMediaFileType(message.getType()))); } if (StrUtil.isNotEmpty(message.getThumbMediaId())) { message.setThumbMediaUrl(mpMaterialService.downloadMaterialUrl(message.getAccountId(), message.getThumbMediaId(), WxConsts.MediaFileType.THUMB)); } } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/message/bo/MpMessageSendOutReqBO.java ================================================ package co.yixiang.yshop.module.mp.service.message.bo; import co.yixiang.yshop.module.mp.dal.dataobject.message.MpMessageDO; import co.yixiang.yshop.module.mp.framework.mp.core.util.MpUtils.*; import lombok.Data; import me.chanjar.weixin.common.api.WxConsts; import org.hibernate.validator.constraints.URL; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.util.List; /** * 公众号消息发送 Request BO * * 为什么要有该 BO 呢?在自动回复、客服消息、菜单回复消息等场景,都涉及到 MP 给粉丝发送消息,所以使用该 BO 统一承接 * * @author yshop */ @Data public class MpMessageSendOutReqBO { /** * 公众号 appId */ @NotEmpty(message = "公众号 appId 不能为空") private String appId; /** * 公众号粉丝 openid */ @NotEmpty(message = "公众号粉丝 openid 不能为空") private String openid; // ========== 消息内容 ========== /** * 消息类型 * * 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS、MUSIC */ @NotEmpty(message = "消息类型不能为空") public String type; /** * 消息内容 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT */ @NotEmpty(message = "消息内容不能为空", groups = TextMessageGroup.class) private String content; /** * 媒体 id * * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO */ @NotEmpty(message = "消息 mediaId 不能为空", groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class}) private String mediaId; /** * 缩略图的媒体 id * * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC */ @NotEmpty(message = "消息 thumbMediaId 不能为空", groups = {MusicMessageGroup.class}) private String thumbMediaId; /** * 标题 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO */ @NotEmpty(message = "消息标题不能为空", groups = VideoMessageGroup.class) private String title; /** * 描述 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO */ @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class) private String description; /** * 图文消息 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS */ @Valid @NotNull(message = "图文消息不能为空", groups = NewsMessageGroup.class) private List articles; /** * 音乐链接 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC */ @NotEmpty(message = "音乐链接不能为空", groups = MusicMessageGroup.class) @URL(message = "高质量音乐链接格式不正确", groups = MusicMessageGroup.class) private String musicUrl; /** * 高质量音乐链接 * * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC */ @NotEmpty(message = "高质量音乐链接不能为空", groups = MusicMessageGroup.class) @URL(message = "高质量音乐链接格式不正确", groups = MusicMessageGroup.class) private String hqMusicUrl; } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/statistics/MpStatisticsService.java ================================================ package co.yixiang.yshop.module.mp.service.statistics; import me.chanjar.weixin.mp.bean.datacube.WxDataCubeInterfaceResult; import me.chanjar.weixin.mp.bean.datacube.WxDataCubeMsgResult; import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserCumulate; import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserSummary; import java.time.LocalDateTime; import java.util.List; /** * 公众号统计 Service 接口 * * @author yshop */ public interface MpStatisticsService { /** * 获取粉丝增减数据 * * @param accountId 公众号账号编号 * @param date 时间区间 * @return 粉丝增减数据 */ List getUserSummary(Long accountId, LocalDateTime[] date); /** * 获取粉丝累计数据 * * @param accountId 公众号账号编号 * @param date 时间区间 * @return 粉丝累计数据 */ List getUserCumulate(Long accountId, LocalDateTime[] date); /** * 获取消息发送概况数据 * * @param accountId 公众号账号编号 * @param date 时间区间 * @return 消息发送概况数据 */ List getUpstreamMessage(Long accountId, LocalDateTime[] date); /** * 获取接口分析数据 * * @param accountId 公众号账号编号 * @param date 时间区间 * @return 接口分析数据 */ List getInterfaceSummary(Long accountId, LocalDateTime[] date); } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/statistics/MpStatisticsServiceImpl.java ================================================ package co.yixiang.yshop.module.mp.service.statistics; import cn.hutool.core.date.DateUtil; import co.yixiang.yshop.module.mp.framework.mp.core.MpServiceFactory; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.datacube.WxDataCubeInterfaceResult; import me.chanjar.weixin.mp.bean.datacube.WxDataCubeMsgResult; import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserCumulate; import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserSummary; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import java.time.LocalDateTime; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.mp.enums.ErrorCodeConstants.*; /** * 公众号统计 Service 实现类 * * @author yshop */ @Service public class MpStatisticsServiceImpl implements MpStatisticsService { @Resource @Lazy // 延迟加载,解决循环依赖的问题 private MpServiceFactory mpServiceFactory; @Override public List getUserSummary(Long accountId, LocalDateTime[] date) { WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); try { return mpService.getDataCubeService().getUserSummary( DateUtil.date(date[0]), DateUtil.date(date[1])); } catch (WxErrorException e) { throw exception(STATISTICS_GET_USER_SUMMARY_FAIL, e.getError().getErrorMsg()); } } @Override public List getUserCumulate(Long accountId, LocalDateTime[] date) { WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); try { return mpService.getDataCubeService().getUserCumulate( DateUtil.date(date[0]), DateUtil.date(date[1])); } catch (WxErrorException e) { throw exception(STATISTICS_GET_USER_CUMULATE_FAIL, e.getError().getErrorMsg()); } } @Override public List getUpstreamMessage(Long accountId, LocalDateTime[] date) { WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); try { return mpService.getDataCubeService().getUpstreamMsg( DateUtil.date(date[0]), DateUtil.date(date[1])); } catch (WxErrorException e) { throw exception(STATISTICS_GET_UPSTREAM_MESSAGE_FAIL, e.getError().getErrorMsg()); } } @Override public List getInterfaceSummary(Long accountId, LocalDateTime[] date) { WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); try { return mpService.getDataCubeService().getInterfaceSummary( DateUtil.date(date[0]), DateUtil.date(date[1])); } catch (WxErrorException e) { throw exception(STATISTICS_GET_INTERFACE_SUMMARY_FAIL, e.getError().getErrorMsg()); } } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/tag/MpTagService.java ================================================ package co.yixiang.yshop.module.mp.service.tag; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.mp.controller.admin.tag.vo.MpTagCreateReqVO; import co.yixiang.yshop.module.mp.controller.admin.tag.vo.MpTagPageReqVO; import co.yixiang.yshop.module.mp.controller.admin.tag.vo.MpTagUpdateReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.tag.MpTagDO; import jakarta.validation.Valid; import java.util.List; /** * 公众号标签 Service 接口 * * @author fengdan */ public interface MpTagService { /** * 创建公众号标签 * * @param createReqVO 创建标签信息 * @return 标签编号 */ Long createTag(@Valid MpTagCreateReqVO createReqVO); /** * 更新公众号标签 * * @param updateReqVO 更新标签信息 */ void updateTag(@Valid MpTagUpdateReqVO updateReqVO); /** * 删除公众号标签 * * @param id 编号 */ void deleteTag(Long id); /** * 获得公众号标签分页 * * @param pageReqVO 分页查询 * @return 公众号标签分页 */ PageResult getTagPage(MpTagPageReqVO pageReqVO); /** * 获得公众号标签详情 * @param id id查询 * @return 公众号标签详情 */ MpTagDO get(Long id); List getTagList(); /** * 同步公众号标签 * * @param accountId 公众号账号的编号 */ void syncTag(Long accountId); } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/tag/MpTagServiceImpl.java ================================================ package co.yixiang.yshop.module.mp.service.tag; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.mp.controller.admin.tag.vo.MpTagCreateReqVO; import co.yixiang.yshop.module.mp.controller.admin.tag.vo.MpTagPageReqVO; import co.yixiang.yshop.module.mp.controller.admin.tag.vo.MpTagUpdateReqVO; import co.yixiang.yshop.module.mp.convert.tag.MpTagConvert; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import co.yixiang.yshop.module.mp.dal.dataobject.tag.MpTagDO; import co.yixiang.yshop.module.mp.dal.mysql.tag.MpTagMapper; import co.yixiang.yshop.module.mp.framework.mp.core.MpServiceFactory; import co.yixiang.yshop.module.mp.service.account.MpAccountService; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.tag.WxUserTag; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.List; import java.util.Map; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertList; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertMap; import static co.yixiang.yshop.module.mp.enums.ErrorCodeConstants.*; /** * 公众号标签 Service 实现类 * * @author fengdan */ @Slf4j @Service @Validated public class MpTagServiceImpl implements MpTagService { @Resource private MpTagMapper mpTagMapper; @Resource private MpAccountService mpAccountService; @Resource @Lazy // 延迟加载,为了解决延迟加载 private MpServiceFactory mpServiceFactory; @Override public Long createTag(MpTagCreateReqVO createReqVO) { // 获得公众号账号 MpAccountDO account = mpAccountService.getRequiredAccount(createReqVO.getAccountId()); // 第一步,新增标签到公众号平台。标签名的唯一,交给公众号平台 WxMpService mpService = mpServiceFactory.getRequiredMpService(createReqVO.getAccountId()); WxUserTag wxTag; try { wxTag = mpService.getUserTagService().tagCreate(createReqVO.getName()); } catch (WxErrorException e) { throw exception(TAG_CREATE_FAIL, e.getError().getErrorMsg()); } // 第二步,新增标签到数据库 MpTagDO tag = MpTagConvert.INSTANCE.convert(wxTag, account); mpTagMapper.insert(tag); return tag.getId(); } @Override public void updateTag(MpTagUpdateReqVO updateReqVO) { // 校验标签存在 MpTagDO tag = validateTagExists(updateReqVO.getId()); // 第一步,更新标签到公众号平台。标签名的唯一,交给公众号平台 WxMpService mpService = mpServiceFactory.getRequiredMpService(tag.getAccountId()); try { mpService.getUserTagService().tagUpdate(tag.getTagId(), updateReqVO.getName()); } catch (WxErrorException e) { throw exception(TAG_UPDATE_FAIL, e.getError().getErrorMsg()); } // 第二步,更新标签到数据库 mpTagMapper.updateById(new MpTagDO().setId(tag.getId()).setName(updateReqVO.getName())); } @Override public void deleteTag(Long id) { // 校验标签存在 MpTagDO tag = validateTagExists(id); // 第一步,删除标签到公众号平台。 WxMpService mpService = mpServiceFactory.getRequiredMpService(tag.getAccountId()); try { mpService.getUserTagService().tagDelete(tag.getTagId()); } catch (WxErrorException e) { throw exception(TAG_DELETE_FAIL, e.getError().getErrorMsg()); } // 第二步,删除标签到数据库 mpTagMapper.deleteById(tag.getId()); } private MpTagDO validateTagExists(Long id) { MpTagDO tag = mpTagMapper.selectById(id); if (tag == null) { throw exception(TAG_NOT_EXISTS); } return tag; } @Override public PageResult getTagPage(MpTagPageReqVO pageReqVO) { return mpTagMapper.selectPage(pageReqVO); } @Override public MpTagDO get(Long id) { return mpTagMapper.selectById(id); } @Override public List getTagList() { return mpTagMapper.selectList(); } @Override @Transactional(rollbackFor = Exception.class) public void syncTag(Long accountId) { MpAccountDO account = mpAccountService.getRequiredAccount(accountId); // 第一步,从公众号平台获取最新的标签列表 WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); List wxTags; try { wxTags = mpService.getUserTagService().tagGet(); } catch (WxErrorException e) { throw exception(TAG_GET_FAIL, e.getError().getErrorMsg()); } // 第二步,合并更新回自己的数据库;由于标签只有 100 个,所以直接 for 循环操作 Map tagMap = convertMap(mpTagMapper.selectListByAccountId(accountId), MpTagDO::getTagId); wxTags.forEach(wxTag -> { MpTagDO tag = tagMap.remove(wxTag.getId()); // 情况一,不存在,新增 if (tag == null) { tag = MpTagConvert.INSTANCE.convert(wxTag, account); mpTagMapper.insert(tag); return; } // 情况二,存在,则更新 mpTagMapper.updateById(new MpTagDO().setId(tag.getId()) .setName(wxTag.getName()).setCount(wxTag.getCount())); }); // 情况三,部分标签已经不存在了,删除 if (CollUtil.isNotEmpty(tagMap)) { mpTagMapper.deleteBatchIds(convertList(tagMap.values(), MpTagDO::getId)); } } } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/user/MpUserService.java ================================================ package co.yixiang.yshop.module.mp.service.user; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.mp.controller.admin.user.vo.MpUserPageReqVO; import co.yixiang.yshop.module.mp.controller.admin.user.vo.MpUserUpdateReqVO; import co.yixiang.yshop.module.mp.dal.dataobject.user.MpUserDO; import me.chanjar.weixin.mp.bean.result.WxMpUser; import java.util.Collection; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.mp.enums.ErrorCodeConstants.USER_NOT_EXISTS; /** * 公众号粉丝 Service 接口 * * @author yshop */ public interface MpUserService { /** * 获得公众号粉丝 * * @param id 编号 * @return 公众号粉丝 */ MpUserDO getUser(Long id); /** * 使用 appId + openId,获得公众号粉丝 * * @param appId 公众号 appId * @param openId 公众号 openId * @return 公众号粉丝 */ MpUserDO getUser(String appId, String openId); /** * 获得公众号粉丝 * * @param id 编号 * @return 公众号粉丝 */ default MpUserDO getRequiredUser(Long id) { MpUserDO user = getUser(id); if (user == null) { throw exception(USER_NOT_EXISTS); } return user; } /** * 获得公众号粉丝列表 * * @param ids 编号 * @return 公众号粉丝列表 */ List getUserList(Collection ids); /** * 获得公众号粉丝分页 * * @param pageReqVO 分页查询 * @return 公众号粉丝分页 */ PageResult getUserPage(MpUserPageReqVO pageReqVO); /** * 保存公众号粉丝 * * 新增或更新,根据是否存在数据库中 * * @param appId 公众号 appId * @param wxMpUser 公众号粉丝的信息 * @return 公众号粉丝 */ MpUserDO saveUser(String appId, WxMpUser wxMpUser); /** * 同步一个公众号粉丝 * * @param accountId 公众号账号的编号 */ void syncUser(Long accountId); /** * 更新公众号粉丝,取消关注 * * @param appId 公众号 appId * @param openId 公众号粉丝的 openid */ void updateUserUnsubscribe(String appId, String openId); /** * 更新公众号粉丝 * * @param updateReqVO 更新信息 */ void updateUser(MpUserUpdateReqVO updateReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-mp/yshop-module-mp-biz/src/main/java/co/yixiang/yshop/module/mp/service/user/MpUserServiceImpl.java ================================================ package co.yixiang.yshop.module.mp.service.user; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.module.mp.controller.admin.user.vo.MpUserPageReqVO; import co.yixiang.yshop.module.mp.controller.admin.user.vo.MpUserUpdateReqVO; import co.yixiang.yshop.module.mp.convert.user.MpUserConvert; import co.yixiang.yshop.module.mp.dal.dataobject.account.MpAccountDO; import co.yixiang.yshop.module.mp.dal.dataobject.user.MpUserDO; import co.yixiang.yshop.module.mp.dal.mysql.user.MpUserMapper; import co.yixiang.yshop.module.mp.framework.mp.core.MpServiceFactory; import co.yixiang.yshop.module.mp.service.account.MpAccountService; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.result.WxMpUser; import me.chanjar.weixin.mp.bean.result.WxMpUserList; import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.mp.enums.ErrorCodeConstants.USER_NOT_EXISTS; import static co.yixiang.yshop.module.mp.enums.ErrorCodeConstants.USER_UPDATE_TAG_FAIL; /** * 微信公众号粉丝 Service 实现类 * * @author yshop */ @Service @Validated @Slf4j public class MpUserServiceImpl implements MpUserService { @Resource @Lazy // 延迟加载,解决循环依赖的问题 private MpAccountService mpAccountService; @Resource @Lazy // 延迟加载,解决循环依赖的问题 private MpServiceFactory mpServiceFactory; @Resource private MpUserMapper mpUserMapper; @Override public MpUserDO getUser(Long id) { return mpUserMapper.selectById(id); } @Override public MpUserDO getUser(String appId, String openId) { return mpUserMapper.selectByAppIdAndOpenid(appId, openId); } @Override public List getUserList(Collection ids) { return mpUserMapper.selectBatchIds(ids); } @Override public PageResult getUserPage(MpUserPageReqVO pageReqVO) { return mpUserMapper.selectPage(pageReqVO); } @Override public MpUserDO saveUser(String appId, WxMpUser wxMpUser) { // 构建保存的 MpUserDO 对象 MpAccountDO account = mpAccountService.getAccountFromCache(appId); MpUserDO user = MpUserConvert.INSTANCE.convert(account, wxMpUser); // 根据情况,插入或更新 MpUserDO dbUser = mpUserMapper.selectByAppIdAndOpenid(appId, wxMpUser.getOpenId()); if (dbUser == null) { mpUserMapper.insert(user); } else { user.setId(dbUser.getId()); mpUserMapper.updateById(user); } return user; } @Override @Async public void syncUser(Long accountId) { MpAccountDO account = mpAccountService.getRequiredAccount(accountId); // for 循环,避免递归出意外问题,导致死循环 String nextOpenid = null; for (int i = 0; i < Short.MAX_VALUE; i++) { log.info("[syncUser][第({}) 次加载公众号粉丝列表,nextOpenid({})]", i, nextOpenid); try { nextOpenid = syncUser0(account, nextOpenid); } catch (WxErrorException e) { log.error("[syncUser][第({}) 次同步粉丝异常]", i, e); break; } // 如果 nextOpenid 为空,表示已经同步完毕 if (StrUtil.isEmpty(nextOpenid)) { break; } } } private String syncUser0(MpAccountDO account, String nextOpenid) throws WxErrorException { // 第一步,从公众号流式加载粉丝 WxMpService mpService = mpServiceFactory.getRequiredMpService(account.getId()); WxMpUserList wxUserList = mpService.getUserService().userList(nextOpenid); if (CollUtil.isEmpty(wxUserList.getOpenids())) { return null; } // 第二步,分批加载粉丝信息 List> openidsList = CollUtil.split(wxUserList.getOpenids(), 100); for (List openids : openidsList) { log.info("[syncUser][批量加载粉丝信息,openids({})]", openids); List wxUsers = mpService.getUserService().userInfoList(openids); batchSaveUser(account, wxUsers); } // 返回下一次的 nextOpenId return wxUserList.getNextOpenid(); } private void batchSaveUser(MpAccountDO account, List wxUsers) { if (CollUtil.isEmpty(wxUsers)) { return; } // 1. 获得数据库已保存的粉丝列表 List dbUsers = mpUserMapper.selectListByAppIdAndOpenid(account.getAppId(), CollectionUtils.convertList(wxUsers, WxMpUser::getOpenId)); Map openId2Users = CollectionUtils.convertMap(dbUsers, MpUserDO::getOpenid); // 2.1 根据情况,插入或更新 List users = MpUserConvert.INSTANCE.convertList(account, wxUsers); List newUsers = new ArrayList<>(); for (MpUserDO user : users) { MpUserDO dbUser = openId2Users.get(user.getOpenid()); if (dbUser == null) { // 新增:稍后批量插入 newUsers.add(user); } else { // 更新:直接执行更新 user.setId(dbUser.getId()); mpUserMapper.updateById(user); } } // 2.2 批量插入 if (CollUtil.isNotEmpty(newUsers)) { mpUserMapper.insertBatch(newUsers); } } @Override public void updateUserUnsubscribe(String appId, String openid) { MpUserDO dbUser = mpUserMapper.selectByAppIdAndOpenid(appId, openid); if (dbUser == null) { log.error("[updateUserUnsubscribe][微信公众号粉丝 appId({}) openid({}) 不存在]", appId, openid); return; } mpUserMapper.updateById(new MpUserDO().setId(dbUser.getId()).setSubscribeStatus(CommonStatusEnum.DISABLE.getStatus()) .setUnsubscribeTime(LocalDateTime.now())); } @Override public void updateUser(MpUserUpdateReqVO updateReqVO) { // 校验存在 MpUserDO user = validateUserExists(updateReqVO.getId()); // 第一步,更新标签到公众号 updateUserTag(user.getAppId(), user.getOpenid(), updateReqVO.getTagIds()); // 第二步,更新基本信息到数据库 MpUserDO updateObj = MpUserConvert.INSTANCE.convert(updateReqVO).setId(user.getId()); mpUserMapper.updateById(updateObj); } private MpUserDO validateUserExists(Long id) { MpUserDO user = mpUserMapper.selectById(id); if (user == null) { throw exception(USER_NOT_EXISTS); } return user; } private void updateUserTag(String appId, String openid, List tagIds) { WxMpService mpService = mpServiceFactory.getRequiredMpService(appId); try { // 第一步,先取消原来的标签 List oldTagIds = mpService.getUserTagService().userTagList(openid); for (Long tagId : oldTagIds) { mpService.getUserTagService().batchUntagging(tagId, new String[]{openid}); } // 第二步,再设置新的标签 if (CollUtil.isEmpty(tagIds)) { return; } for (Long tagId: tagIds) { mpService.getUserTagService().batchTagging(tagId, new String[]{openid}); } } catch (WxErrorException e) { throw exception(USER_UPDATE_TAG_FAIL, e.getError().getErrorMsg()); } } } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/pom.xml ================================================ co.yixiang.boot yshop ${revision} 4.0.0 yshop-module-pay-api yshop-module-pay-biz yshop-module-pay pom ${project.artifactId} pay 模块 ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-api/pom.xml ================================================ yshop-module-pay co.yixiang.boot ${revision} 4.0.0 yshop-module-pay-api jar ${project.artifactId} pay 模块 API,暴露给其它模块调用 co.yixiang.boot yshop-common org.springframework.boot spring-boot-starter-validation true co.yixiang.boot yshop-spring-boot-starter-mybatis co.yixiang.boot yshop-spring-boot-starter-mq com.egzosn pay-java-wx com.egzosn pay-java-ali com.egzosn pay-spring-boot-starter com.egzosn pay-java-web-support ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-api/src/main/java/co/yixiang/yshop/module/pay/config/MerchantPayServiceConfigurer.java ================================================ package co.yixiang.yshop.module.pay.config; import co.yixiang.yshop.module.pay.config.handlers.AliPayMessageHandler; import co.yixiang.yshop.module.pay.config.handlers.WxPayMessageHandler; import com.egzosn.pay.spring.boot.core.PayServiceConfigurer; import com.egzosn.pay.spring.boot.core.configurers.MerchantDetailsServiceConfigurer; import com.egzosn.pay.spring.boot.core.configurers.PayMessageConfigurer; import com.egzosn.pay.spring.boot.core.merchant.PaymentPlatform; import com.egzosn.pay.spring.boot.core.provider.merchant.platform.AliPaymentPlatform; import com.egzosn.pay.spring.boot.core.provider.merchant.platform.PaymentPlatforms; import com.egzosn.pay.spring.boot.core.provider.merchant.platform.WxPaymentPlatform; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcTemplate; import jakarta.annotation.Resource; /** * 支付服务配置 * * @author hupeng * @date 2023/7/15 */ @Configuration public class MerchantPayServiceConfigurer implements PayServiceConfigurer { @Resource private JdbcTemplate jdbcTemplate; @Resource private AliPayMessageHandler aliPayMessageHandler; @Resource private WxPayMessageHandler wxPayMessageHandler; /** * 商户配置 * * @param merchants 商户配置 */ @Override public void configure(MerchantDetailsServiceConfigurer merchants) { merchants.jdbc() //是否开启缓存,默认不开启,这里开启缓存 .cache(false) .template(jdbcTemplate); } /** * 商户配置 * * @param configurer 支付消息配置 */ @Override public void configure(PayMessageConfigurer configurer) { PaymentPlatform aliPaymentPlatform = PaymentPlatforms.getPaymentPlatform(AliPaymentPlatform.PLATFORM_NAME); PaymentPlatform wxPaymentPlatform = PaymentPlatforms.getPaymentPlatform(WxPaymentPlatform.PLATFORM_NAME); configurer.addHandler(aliPaymentPlatform, aliPayMessageHandler); configurer.addHandler(wxPaymentPlatform, wxPayMessageHandler); //configurer.addInterceptor(wxPaymentPlatform, spring.getBean(AliPayMessageInterceptor.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-api/src/main/java/co/yixiang/yshop/module/pay/config/PayAutoConfiguration.java ================================================ package co.yixiang.yshop.module.pay.config; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.Configuration; //@Configuration //@EnableAutoConfiguration public class PayAutoConfiguration { } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-api/src/main/java/co/yixiang/yshop/module/pay/config/handlers/AliPayMessageHandler.java ================================================ package co.yixiang.yshop.module.pay.config.handlers; import co.yixiang.yshop.module.pay.mq.producer.PayNoticeProducer; import com.egzosn.pay.ali.api.AliPayService; import com.egzosn.pay.ali.bean.AliPayMessage; import com.egzosn.pay.common.api.PayMessageHandler; import com.egzosn.pay.common.bean.PayOutMessage; import com.egzosn.pay.common.exception.PayErrorException; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; import java.util.Map; /** * 支付宝支付回调处理器 * @author hupeng * @date 2023/7/15 */ @Component public class AliPayMessageHandler implements PayMessageHandler { @Resource private PayNoticeProducer payNoticeProducer; /** * 处理支付回调消息的处理器接口 * * @param payMessage 支付消息 * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 * @param payService 支付服务 * @return xml, text格式的消息,如果在异步规则里处理的话,可以返回null * @throws PayErrorException 支付错误异常 */ @Override public PayOutMessage handle(AliPayMessage payMessage, Map context, AliPayService payService) throws PayErrorException { Map message = payMessage.getPayMessage(); //交易状态 String trade_status = (String) message.get("trade_status"); //交易完成 if ("TRADE_SUCCESS".equals(trade_status) || "TRADE_FINISHED".equals(trade_status)) { String orderId = (String) payMessage.getPayMessage().get("out_trade_no"); //消息队列处理 payNoticeProducer.sendPayNoticeMessage(orderId,"alipay"); return payService.getPayOutMessage("success", "成功"); } return payService.getPayOutMessage("fail", "失败"); } } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-api/src/main/java/co/yixiang/yshop/module/pay/config/handlers/WxPayMessageHandler.java ================================================ package co.yixiang.yshop.module.pay.config.handlers; import co.yixiang.yshop.module.pay.mq.producer.PayNoticeProducer; import com.egzosn.pay.common.api.PayMessageHandler; import com.egzosn.pay.common.api.PayService; import com.egzosn.pay.common.bean.PayOutMessage; import com.egzosn.pay.common.exception.PayErrorException; import com.egzosn.pay.wx.bean.WxPayMessage; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; import java.util.Map; /** * 微信支付回调处理器 * @author hupeng * @date 2023/7/15 */ @Component @Slf4j public class WxPayMessageHandler implements PayMessageHandler { @Resource private PayNoticeProducer payNoticeProducer; @Override public PayOutMessage handle(WxPayMessage payMessage, Map context, PayService payService) throws PayErrorException { log.info("======pay notice ========"); //交易状态 if ("SUCCESS".equals(payMessage.getPayMessage().get("result_code"))){ String orderId = (String)payMessage.getPayMessage().get("out_trade_no"); //消息队列处理 payNoticeProducer.sendPayNoticeMessage(orderId,"weixin"); return payService.getPayOutMessage("SUCCESS", "OK"); } return payService.getPayOutMessage("FAIL", "失败"); } } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-api/src/main/java/co/yixiang/yshop/module/pay/config/interceptor/AliPayMessageInterceptor.java ================================================ package co.yixiang.yshop.module.pay.config.interceptor; import com.egzosn.pay.ali.api.AliPayService; import com.egzosn.pay.ali.bean.AliPayMessage; import com.egzosn.pay.common.api.PayMessageHandler; import com.egzosn.pay.common.api.PayMessageInterceptor; import com.egzosn.pay.common.exception.PayErrorException; import java.util.Map; /** * 支付宝回调信息拦截器 * @author hupeng * @date 2023/7/15 */ //使用 自己开启 //@Component public class AliPayMessageInterceptor implements PayMessageInterceptor { /** * 拦截支付消息 * * @param payMessage 支付回调消息 * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 * @param payService 支付服务 * @return true代表OK,false代表不OK并直接中断对应的支付处理器 * @see PayMessageHandler 支付处理器 * @throws PayErrorException PayErrorException* */ @Override public boolean intercept(AliPayMessage payMessage, Map context, AliPayService payService) throws PayErrorException { //这里进行拦截器处理,自行实现 String outTradeNo = payMessage.getOutTradeNo(); // todo return true; } } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-api/src/main/java/co/yixiang/yshop/module/pay/config/interceptor/WxPayMessageInterceptor.java ================================================ package co.yixiang.yshop.module.pay.config.interceptor; import com.egzosn.pay.ali.api.AliPayService; import com.egzosn.pay.ali.bean.AliPayMessage; import com.egzosn.pay.common.api.PayMessageHandler; import com.egzosn.pay.common.api.PayMessageInterceptor; import com.egzosn.pay.common.exception.PayErrorException; import com.egzosn.pay.wx.api.WxPayService; import com.egzosn.pay.wx.bean.WxPayMessage; import org.springframework.stereotype.Component; import java.util.Map; /** * 支付宝回调信息拦截器 * @author hupeng * @date 2023/7/15 */ //使用 自己开启 //@Component public class WxPayMessageInterceptor implements PayMessageInterceptor { // /** // * 拦截支付消息 // * // * @param payMessage 支付回调消息 // * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 // * @param payService 支付服务 // * @return true代表OK,false代表不OK并直接中断对应的支付处理器 // * @see PayMessageHandler 支付处理器 // * @throws PayErrorException PayErrorException* // */ // @Override // public boolean intercept(AliPayMessage payMessage, Map context, AliPayService payService) throws PayErrorException { // // //这里进行拦截器处理,自行实现 // String outTradeNo = payMessage.getOutTradeNo(); // // todo // // // // return true; // } @Override public boolean intercept(WxPayMessage wxPayMessage, Map map, WxPayService wxPayService) throws PayErrorException { // wxPayService. return false; } } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-api/src/main/java/co/yixiang/yshop/module/pay/enums/ErrorCodeConstants.java ================================================ package co.yixiang.yshop.module.pay.enums; import co.yixiang.yshop.framework.common.exception.ErrorCode; public interface ErrorCodeConstants { ErrorCode MERCHANT_DETAILS_NOT_EXISTS = new ErrorCode(1008009000, "支付服务商配置不存在"); } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-api/src/main/java/co/yixiang/yshop/module/pay/mq/message/PayNoticeMessage.java ================================================ package co.yixiang.yshop.module.pay.mq.message; import co.yixiang.yshop.framework.mq.redis.core.stream.AbstractRedisStreamMessage; import lombok.Data; import jakarta.validation.constraints.NotNull; @Data public class PayNoticeMessage extends AbstractRedisStreamMessage { /** * 订单编号 */ @NotNull(message = "订单编号编号不能为空") private String orderId; //支付类型 private String payType; @Override public String getStreamKey() { return "order.pay.notice"; } } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-api/src/main/java/co/yixiang/yshop/module/pay/mq/producer/PayNoticeProducer.java ================================================ package co.yixiang.yshop.module.pay.mq.producer; import co.yixiang.yshop.framework.mq.redis.core.RedisMQTemplate; import co.yixiang.yshop.module.pay.mq.message.PayNoticeMessage; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; @Slf4j @Component public class PayNoticeProducer { @Resource private RedisMQTemplate redisMQTemplate; /** * 发送消息 * @param orderId 订单编号 */ public void sendPayNoticeMessage(String orderId,String payType) { PayNoticeMessage payNoticeMessage = new PayNoticeMessage().setOrderId(orderId).setPayType(payType); redisMQTemplate.send(payNoticeMessage); } } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-biz/pom.xml ================================================ yshop-module-pay co.yixiang.boot ${revision} 4.0.0 yshop-module-pay-biz jar ${project.artifactId} pay 模块,我们放支付业务,提供业务的支付能力。 例如说:商户、应用、支付、退款等等 co.yixiang.boot yshop-module-pay-api ${revision} co.yixiang.boot yshop-spring-boot-starter-biz-tenant co.yixiang.boot yshop-spring-boot-starter-security co.yixiang.boot yshop-spring-boot-starter-redis co.yixiang.boot yshop-spring-boot-starter-mq co.yixiang.boot yshop-spring-boot-starter-test test co.yixiang.boot yshop-spring-boot-starter-excel ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-biz/src/main/java/co/yixiang/yshop/module/pay/controller/admin/merchantdetails/MerchantDetailsController.java ================================================ package co.yixiang.yshop.module.pay.controller.admin.merchantdetails; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import org.springframework.security.access.prepost.PreAuthorize; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.*; import java.util.*; import java.io.IOException; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.pojo.CommonResult; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.pay.controller.admin.merchantdetails.vo.*; import co.yixiang.yshop.module.pay.dal.dataobject.merchantdetails.MerchantDetailsDO; import co.yixiang.yshop.module.pay.convert.merchantdetails.MerchantDetailsConvert; import co.yixiang.yshop.module.pay.service.merchantdetails.MerchantDetailsService; @Tag(name = "管理后台 - 支付服务商配置") @RestController @RequestMapping("/pay/merchant-details") @Validated public class MerchantDetailsController { @Resource private MerchantDetailsService merchantDetailsService; @PostMapping("/create") @Operation(summary = "创建支付服务商配置") @PreAuthorize("@ss.hasPermission('pay:merchant-details:create')") public CommonResult createMerchantDetails(@Valid @RequestBody MerchantDetailsCreateReqVO createReqVO) { return success(merchantDetailsService.createMerchantDetails(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新支付服务商配置") @PreAuthorize("@ss.hasPermission('pay:merchant-details:update')") public CommonResult updateMerchantDetails(@Valid @RequestBody MerchantDetailsUpdateReqVO updateReqVO) { merchantDetailsService.updateMerchantDetails(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除支付服务商配置") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('pay:merchant-details:delete')") public CommonResult deleteMerchantDetails(@RequestParam("id") String id) { merchantDetailsService.deleteMerchantDetails(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得支付服务商配置") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('pay:merchant-details:query')") public CommonResult getMerchantDetails(@RequestParam("id") String id) { MerchantDetailsDO merchantDetails = merchantDetailsService.getMerchantDetails(id); return success(MerchantDetailsConvert.INSTANCE.convert(merchantDetails)); } @GetMapping("/list") @Operation(summary = "获得支付服务商配置列表") @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") @PreAuthorize("@ss.hasPermission('pay:merchant-details:query')") public CommonResult> getMerchantDetailsList(@RequestParam("ids") Collection ids) { List list = merchantDetailsService.getMerchantDetailsList(ids); return success(MerchantDetailsConvert.INSTANCE.convertList(list)); } @GetMapping("/page") @Operation(summary = "获得支付服务商配置分页") @PreAuthorize("@ss.hasPermission('pay:merchant-details:query')") public CommonResult> getMerchantDetailsPage(@Valid MerchantDetailsPageReqVO pageVO) { PageResult pageResult = merchantDetailsService.getMerchantDetailsPage(pageVO); return success(MerchantDetailsConvert.INSTANCE.convertPage(pageResult)); } @GetMapping("/export-excel") @Operation(summary = "导出支付服务商配置 Excel") @PreAuthorize("@ss.hasPermission('pay:merchant-details:export')") public void exportMerchantDetailsExcel(@Valid MerchantDetailsExportReqVO exportReqVO, HttpServletResponse response) throws IOException { List list = merchantDetailsService.getMerchantDetailsList(exportReqVO); // 导出 Excel List datas = MerchantDetailsConvert.INSTANCE.convertList02(list); ExcelUtils.write(response, "支付服务商配置.xls", "数据", MerchantDetailsExcelVO.class, datas); } } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-biz/src/main/java/co/yixiang/yshop/module/pay/controller/admin/merchantdetails/vo/MerchantDetailsBaseVO.java ================================================ package co.yixiang.yshop.module.pay.controller.admin.merchantdetails.vo; import co.yixiang.yshop.framework.desensitize.core.slider.annotation.SliderDesensitize; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import jakarta.validation.constraints.*; /** * 支付服务商配置 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class MerchantDetailsBaseVO { private String detailsId; @Schema(description = "支付类型(支付渠道) 详情查看com.egzosn.pay.spring.boot.core.merchant.PaymentPlatform对应子类,aliPay 支付宝, wxPay微信..等等", required = true, example = "2") @NotNull(message = "支付类型(支付渠道)等不能为空") private String payType; //@SliderDesensitize(prefixKeep=3 ,suffixKeep=4) @Schema(description = "应用id", example = "1718") private String appid; @Schema(description = "商户id,商户号,合作伙伴id等等", example = "21574") //@SliderDesensitize(prefixKeep=2 ,suffixKeep=2) private String mchId; @Schema(description = "当前面私钥公钥为证书类型的时候,这里必填,可选值:PATH,STR,INPUT_STREAM,CLASS_PATH,URL", example = "1") private String certStoreType; @Schema(description = "私钥或私钥证书") //@SliderDesensitize(prefixKeep=4 ,suffixKeep=4) private String keyPrivate; @Schema(description = "公钥或公钥证书") private String keyPublic; @Schema(description = "key证书,附加证书使用,如SSL证书,或者银联根级证书方面") private String keyCert; @Schema(description = "私钥证书或key证书的密码") private String keyCertPwd; @Schema(description = "异步回调", example = "https://www.yixiang.co") private String notifyUrl; @Schema(description = "同步回调地址,大部分用于付款成功后页面转跳", example = "https://www.yixiang.co") private String returnUrl; @Schema(description = "签名方式,目前已实现多种签名方式详情查看com.egzosn.pay.common.util.sign.encrypt。MD5,RSA等等", required = true, example = "1") @NotNull(message = "签名方式不能为空") private String signType; @Schema(description = "收款账号,暂时只有支付宝部分使用,可根据开发者自行使用") private String seller; @Schema(description = "子appid", example = "13761") private String subAppId; @Schema(description = "子商户id", example = "10127") private String subMchId; @Schema(description = "编码类型,大部分为utf-8", required = true) //@NotNull(message = "编码类型,大部分为utf-8不能为空") private String inputCharset; @Schema(description = "是否为测试环境: 0 否,1 测试环境", required = true) @NotNull(message = "是否为测试环境: 0 否,1 测试环境不能为空") private Integer isTest; } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-biz/src/main/java/co/yixiang/yshop/module/pay/controller/admin/merchantdetails/vo/MerchantDetailsCreateReqVO.java ================================================ package co.yixiang.yshop.module.pay.controller.admin.merchantdetails.vo; import lombok.*; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "管理后台 - 支付服务商配置创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MerchantDetailsCreateReqVO extends MerchantDetailsBaseVO { } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-biz/src/main/java/co/yixiang/yshop/module/pay/controller/admin/merchantdetails/vo/MerchantDetailsExcelVO.java ================================================ package co.yixiang.yshop.module.pay.controller.admin.merchantdetails.vo; import lombok.*; import com.alibaba.excel.annotation.ExcelProperty; /** * 支付服务商配置 Excel VO * * @author yshop */ @Data public class MerchantDetailsExcelVO { @ExcelProperty("列表id") private String detailsId; @ExcelProperty("支付类型(支付渠道) 详情查看com.egzosn.pay.spring.boot.core.merchant.PaymentPlatform对应子类,aliPay 支付宝, wxPay微信..等等") private String payType; @ExcelProperty("应用id") private String appid; @ExcelProperty("商户id,商户号,合作伙伴id等等") private String mchId; @ExcelProperty("当前面私钥公钥为证书类型的时候,这里必填,可选值:PATH,STR,INPUT_STREAM,CLASS_PATH,URL") private String certStoreType; @ExcelProperty("私钥或私钥证书") private String keyPrivate; @ExcelProperty("公钥或公钥证书") private String keyPublic; @ExcelProperty("key证书,附加证书使用,如SSL证书,或者银联根级证书方面") private String keyCert; @ExcelProperty("私钥证书或key证书的密码") private String keyCertPwd; @ExcelProperty("异步回调") private String notifyUrl; @ExcelProperty("同步回调地址,大部分用于付款成功后页面转跳") private String returnUrl; @ExcelProperty("签名方式,目前已实现多种签名方式详情查看com.egzosn.pay.common.util.sign.encrypt。MD5,RSA等等") private String signType; @ExcelProperty("收款账号,暂时只有支付宝部分使用,可根据开发者自行使用") private String seller; @ExcelProperty("子appid") private String subAppId; @ExcelProperty("子商户id") private String subMchId; @ExcelProperty("编码类型,大部分为utf-8") private String inputCharset; @ExcelProperty("是否为测试环境: 0 否,1 测试环境") private Integer isTest; } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-biz/src/main/java/co/yixiang/yshop/module/pay/controller/admin/merchantdetails/vo/MerchantDetailsExportReqVO.java ================================================ package co.yixiang.yshop.module.pay.controller.admin.merchantdetails.vo; import lombok.*; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "管理后台 - 支付服务商配置 Excel 导出 Request VO,参数和 MerchantDetailsPageReqVO 是一致的") @Data public class MerchantDetailsExportReqVO { @Schema(description = "支付类型(支付渠道) 详情查看com.egzosn.pay.spring.boot.core.merchant.PaymentPlatform对应子类,aliPay 支付宝, wxPay微信..等等", example = "2") private String payType; @Schema(description = "应用id", example = "1718") private String appid; @Schema(description = "商户id,商户号,合作伙伴id等等", example = "21574") private String mchId; @Schema(description = "当前面私钥公钥为证书类型的时候,这里必填,可选值:PATH,STR,INPUT_STREAM,CLASS_PATH,URL", example = "1") private String certStoreType; @Schema(description = "私钥或私钥证书") private String keyPrivate; @Schema(description = "公钥或公钥证书") private String keyPublic; @Schema(description = "key证书,附加证书使用,如SSL证书,或者银联根级证书方面") private String keyCert; @Schema(description = "私钥证书或key证书的密码") private String keyCertPwd; @Schema(description = "异步回调", example = "https://www.yixiang.co") private String notifyUrl; @Schema(description = "同步回调地址,大部分用于付款成功后页面转跳", example = "https://www.yixiang.co") private String returnUrl; @Schema(description = "签名方式,目前已实现多种签名方式详情查看com.egzosn.pay.common.util.sign.encrypt。MD5,RSA等等", example = "1") private String signType; @Schema(description = "收款账号,暂时只有支付宝部分使用,可根据开发者自行使用") private String seller; @Schema(description = "子appid", example = "13761") private String subAppId; @Schema(description = "子商户id", example = "10127") private String subMchId; @Schema(description = "编码类型,大部分为utf-8") private String inputCharset; @Schema(description = "是否为测试环境: 0 否,1 测试环境") private Boolean isTest; } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-biz/src/main/java/co/yixiang/yshop/module/pay/controller/admin/merchantdetails/vo/MerchantDetailsPageReqVO.java ================================================ package co.yixiang.yshop.module.pay.controller.admin.merchantdetails.vo; import lombok.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; @Schema(description = "管理后台 - 支付服务商配置分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MerchantDetailsPageReqVO extends PageParam { @Schema(description = "支付类型(支付渠道) 详情查看com.egzosn.pay.spring.boot.core.merchant.PaymentPlatform对应子类,aliPay 支付宝, wxPay微信..等等", example = "2") private String payType; @Schema(description = "应用id", example = "1718") private String appid; @Schema(description = "商户id,商户号,合作伙伴id等等", example = "21574") private String mchId; @Schema(description = "当前面私钥公钥为证书类型的时候,这里必填,可选值:PATH,STR,INPUT_STREAM,CLASS_PATH,URL", example = "1") private String certStoreType; @Schema(description = "私钥或私钥证书") private String keyPrivate; @Schema(description = "公钥或公钥证书") private String keyPublic; @Schema(description = "key证书,附加证书使用,如SSL证书,或者银联根级证书方面") private String keyCert; @Schema(description = "私钥证书或key证书的密码") private String keyCertPwd; @Schema(description = "异步回调", example = "https://www.yixiang.co") private String notifyUrl; @Schema(description = "同步回调地址,大部分用于付款成功后页面转跳", example = "https://www.yixiang.co") private String returnUrl; @Schema(description = "签名方式,目前已实现多种签名方式详情查看com.egzosn.pay.common.util.sign.encrypt。MD5,RSA等等", example = "1") private String signType; @Schema(description = "收款账号,暂时只有支付宝部分使用,可根据开发者自行使用") private String seller; @Schema(description = "子appid", example = "13761") private String subAppId; @Schema(description = "子商户id", example = "10127") private String subMchId; @Schema(description = "编码类型,大部分为utf-8") private String inputCharset; @Schema(description = "是否为测试环境: 0 否,1 测试环境") private Boolean isTest; } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-biz/src/main/java/co/yixiang/yshop/module/pay/controller/admin/merchantdetails/vo/MerchantDetailsRespVO.java ================================================ package co.yixiang.yshop.module.pay.controller.admin.merchantdetails.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; @Schema(description = "管理后台 - 支付服务商配置 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MerchantDetailsRespVO extends MerchantDetailsBaseVO { @Schema(description = "列表id", required = true, example = "17552") private String detailsId; } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-biz/src/main/java/co/yixiang/yshop/module/pay/controller/admin/merchantdetails/vo/MerchantDetailsUpdateReqVO.java ================================================ package co.yixiang.yshop.module.pay.controller.admin.merchantdetails.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 支付服务商配置更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MerchantDetailsUpdateReqVO extends MerchantDetailsBaseVO { @Schema(description = "列表id", required = true, example = "17552") @NotNull(message = "列表id不能为空") private String detailsId; } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-biz/src/main/java/co/yixiang/yshop/module/pay/convert/merchantdetails/MerchantDetailsConvert.java ================================================ package co.yixiang.yshop.module.pay.convert.merchantdetails; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import co.yixiang.yshop.module.pay.controller.admin.merchantdetails.vo.*; import co.yixiang.yshop.module.pay.dal.dataobject.merchantdetails.MerchantDetailsDO; /** * 支付服务商配置 Convert * * @author yshop */ @Mapper public interface MerchantDetailsConvert { MerchantDetailsConvert INSTANCE = Mappers.getMapper(MerchantDetailsConvert.class); MerchantDetailsDO convert(MerchantDetailsCreateReqVO bean); MerchantDetailsDO convert(MerchantDetailsUpdateReqVO bean); MerchantDetailsRespVO convert(MerchantDetailsDO bean); List convertList(List list); PageResult convertPage(PageResult page); List convertList02(List list); } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-biz/src/main/java/co/yixiang/yshop/module/pay/dal/dataobject/merchantdetails/MerchantDetailsDO.java ================================================ package co.yixiang.yshop.module.pay.dal.dataobject.merchantdetails; import lombok.*; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; /** * 支付服务商配置 DO * * @author yshop */ @TableName("merchant_details") @KeySequence("merchant_details_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class MerchantDetailsDO extends BaseDO { /** * 列表id */ @TableId(type = IdType.INPUT) private String detailsId; /** * 支付类型(支付渠道) 详情查看com.egzosn.pay.spring.boot.core.merchant.PaymentPlatform对应子类,aliPay 支付宝, wxPay微信..等等 */ private String payType; /** * 应用id */ private String appid; /** * 商户id,商户号,合作伙伴id等等 */ private String mchId; /** * 当前面私钥公钥为证书类型的时候,这里必填,可选值:PATH,STR,INPUT_STREAM,CLASS_PATH,URL */ private String certStoreType; /** * 私钥或私钥证书 */ private String keyPrivate; /** * 公钥或公钥证书 */ private String keyPublic; /** * key证书,附加证书使用,如SSL证书,或者银联根级证书方面 */ private String keyCert; /** * 私钥证书或key证书的密码 */ private String keyCertPwd; /** * 异步回调 */ private String notifyUrl; /** * 同步回调地址,大部分用于付款成功后页面转跳 */ private String returnUrl; /** * 签名方式,目前已实现多种签名方式详情查看com.egzosn.pay.common.util.sign.encrypt。MD5,RSA等等 */ private String signType; /** * 收款账号,暂时只有支付宝部分使用,可根据开发者自行使用 */ private String seller; /** * 子appid */ private String subAppId; /** * 子商户id */ private String subMchId; /** * 编码类型,大部分为utf-8 */ private String inputCharset; /** * 是否为测试环境: 0 否,1 测试环境 */ private Integer isTest; } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-biz/src/main/java/co/yixiang/yshop/module/pay/dal/mysql/merchantdetails/MerchantDetailsMapper.java ================================================ package co.yixiang.yshop.module.pay.dal.mysql.merchantdetails; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.pay.dal.dataobject.merchantdetails.MerchantDetailsDO; import org.apache.ibatis.annotations.Mapper; import co.yixiang.yshop.module.pay.controller.admin.merchantdetails.vo.*; /** * 支付服务商配置 Mapper * * @author yshop */ @Mapper public interface MerchantDetailsMapper extends BaseMapperX { default PageResult selectPage(MerchantDetailsPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(MerchantDetailsDO::getPayType, reqVO.getPayType()) .eqIfPresent(MerchantDetailsDO::getAppid, reqVO.getAppid()) .eqIfPresent(MerchantDetailsDO::getMchId, reqVO.getMchId()) .eqIfPresent(MerchantDetailsDO::getCertStoreType, reqVO.getCertStoreType()) .eqIfPresent(MerchantDetailsDO::getKeyPrivate, reqVO.getKeyPrivate()) .eqIfPresent(MerchantDetailsDO::getKeyPublic, reqVO.getKeyPublic()) .eqIfPresent(MerchantDetailsDO::getKeyCert, reqVO.getKeyCert()) .eqIfPresent(MerchantDetailsDO::getKeyCertPwd, reqVO.getKeyCertPwd()) .eqIfPresent(MerchantDetailsDO::getNotifyUrl, reqVO.getNotifyUrl()) .eqIfPresent(MerchantDetailsDO::getReturnUrl, reqVO.getReturnUrl()) .eqIfPresent(MerchantDetailsDO::getSignType, reqVO.getSignType()) .eqIfPresent(MerchantDetailsDO::getSeller, reqVO.getSeller()) .eqIfPresent(MerchantDetailsDO::getSubAppId, reqVO.getSubAppId()) .eqIfPresent(MerchantDetailsDO::getSubMchId, reqVO.getSubMchId()) .eqIfPresent(MerchantDetailsDO::getInputCharset, reqVO.getInputCharset()) .eqIfPresent(MerchantDetailsDO::getIsTest, reqVO.getIsTest()) .orderByDesc(MerchantDetailsDO::getDetailsId)); } default List selectList(MerchantDetailsExportReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .eqIfPresent(MerchantDetailsDO::getPayType, reqVO.getPayType()) .eqIfPresent(MerchantDetailsDO::getAppid, reqVO.getAppid()) .eqIfPresent(MerchantDetailsDO::getMchId, reqVO.getMchId()) .eqIfPresent(MerchantDetailsDO::getCertStoreType, reqVO.getCertStoreType()) .eqIfPresent(MerchantDetailsDO::getKeyPrivate, reqVO.getKeyPrivate()) .eqIfPresent(MerchantDetailsDO::getKeyPublic, reqVO.getKeyPublic()) .eqIfPresent(MerchantDetailsDO::getKeyCert, reqVO.getKeyCert()) .eqIfPresent(MerchantDetailsDO::getKeyCertPwd, reqVO.getKeyCertPwd()) .eqIfPresent(MerchantDetailsDO::getNotifyUrl, reqVO.getNotifyUrl()) .eqIfPresent(MerchantDetailsDO::getReturnUrl, reqVO.getReturnUrl()) .eqIfPresent(MerchantDetailsDO::getSignType, reqVO.getSignType()) .eqIfPresent(MerchantDetailsDO::getSeller, reqVO.getSeller()) .eqIfPresent(MerchantDetailsDO::getSubAppId, reqVO.getSubAppId()) .eqIfPresent(MerchantDetailsDO::getSubMchId, reqVO.getSubMchId()) .eqIfPresent(MerchantDetailsDO::getInputCharset, reqVO.getInputCharset()) .eqIfPresent(MerchantDetailsDO::getIsTest, reqVO.getIsTest()) .orderByDesc(MerchantDetailsDO::getDetailsId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-biz/src/main/java/co/yixiang/yshop/module/pay/http/HttpRequestNoticeNewParams.java ================================================ package co.yixiang.yshop.module.pay.http; import com.egzosn.pay.common.bean.NoticeRequest; import java.io.IOException; import java.io.InputStream; import java.util.Enumeration; import java.util.Map; import jakarta.servlet.http.HttpServletRequest; public class HttpRequestNoticeNewParams implements NoticeRequest { private final HttpServletRequest httpServletRequest; public HttpRequestNoticeNewParams(HttpServletRequest httpServletRequest) { this.httpServletRequest = httpServletRequest; } /** * 根据请求头名称获取请求头信息 * * @param name 名称 * @return 请求头值 */ @Override public String getHeader(String name) { return httpServletRequest.getHeader(name); } /** * 根据请求头名称获取请求头信息 * * @param name 名称 * @return 请求头值 */ @Override public Enumeration getHeaders(String name) { return httpServletRequest.getHeaders(name); } /** * 获取所有的请求头名称 * * @return 请求头名称 */ @Override public Enumeration getHeaderNames() { return httpServletRequest.getHeaderNames(); } /** * 输入流 * * @return 输入流 * @throws IOException IOException */ @Override public InputStream getInputStream() throws IOException { return httpServletRequest.getInputStream(); } /** * 获取所有的请求参数 * * @return 请求参数 */ @Override public Map getParameterMap() { return httpServletRequest.getParameterMap(); } } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-biz/src/main/java/co/yixiang/yshop/module/pay/service/merchantdetails/MerchantDetailsService.java ================================================ package co.yixiang.yshop.module.pay.service.merchantdetails; import java.util.*; import jakarta.validation.*; import co.yixiang.yshop.module.pay.controller.admin.merchantdetails.vo.*; import co.yixiang.yshop.module.pay.dal.dataobject.merchantdetails.MerchantDetailsDO; import co.yixiang.yshop.framework.common.pojo.PageResult; /** * 支付服务商配置 Service 接口 * * @author yshop */ public interface MerchantDetailsService { /** * 创建支付服务商配置 * * @param createReqVO 创建信息 * @return 编号 */ String createMerchantDetails(@Valid MerchantDetailsCreateReqVO createReqVO); /** * 更新支付服务商配置 * * @param updateReqVO 更新信息 */ void updateMerchantDetails(@Valid MerchantDetailsUpdateReqVO updateReqVO); /** * 删除支付服务商配置 * * @param id 编号 */ void deleteMerchantDetails(String id); /** * 获得支付服务商配置 * * @param id 编号 * @return 支付服务商配置 */ MerchantDetailsDO getMerchantDetails(String id); /** * 获得支付服务商配置列表 * * @param ids 编号 * @return 支付服务商配置列表 */ List getMerchantDetailsList(Collection ids); /** * 获得支付服务商配置分页 * * @param pageReqVO 分页查询 * @return 支付服务商配置分页 */ PageResult getMerchantDetailsPage(MerchantDetailsPageReqVO pageReqVO); /** * 获得支付服务商配置列表, 用于 Excel 导出 * * @param exportReqVO 查询条件 * @return 支付服务商配置列表 */ List getMerchantDetailsList(MerchantDetailsExportReqVO exportReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-biz/src/main/java/co/yixiang/yshop/module/pay/service/merchantdetails/MerchantDetailsServiceImpl.java ================================================ package co.yixiang.yshop.module.pay.service.merchantdetails; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import java.util.*; import co.yixiang.yshop.module.pay.controller.admin.merchantdetails.vo.*; import co.yixiang.yshop.module.pay.dal.dataobject.merchantdetails.MerchantDetailsDO; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.pay.convert.merchantdetails.MerchantDetailsConvert; import co.yixiang.yshop.module.pay.dal.mysql.merchantdetails.MerchantDetailsMapper; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.pay.enums.ErrorCodeConstants.*; /** * 支付服务商配置 Service 实现类 * * @author yshop */ @Service @Validated public class MerchantDetailsServiceImpl implements MerchantDetailsService { @Resource private MerchantDetailsMapper merchantDetailsMapper; @Override public String createMerchantDetails(MerchantDetailsCreateReqVO createReqVO) { // 插入 MerchantDetailsDO merchantDetails = MerchantDetailsConvert.INSTANCE.convert(createReqVO); merchantDetailsMapper.insert(merchantDetails); // 返回 return merchantDetails.getDetailsId(); } @Override public void updateMerchantDetails(MerchantDetailsUpdateReqVO updateReqVO) { // 校验存在 validateMerchantDetailsExists(updateReqVO.getDetailsId()); // 更新 MerchantDetailsDO updateObj = MerchantDetailsConvert.INSTANCE.convert(updateReqVO); merchantDetailsMapper.updateById(updateObj); } @Override public void deleteMerchantDetails(String id) { // 校验存在 validateMerchantDetailsExists(id); // 删除 merchantDetailsMapper.deleteById(id); } private void validateMerchantDetailsExists(String id) { if (merchantDetailsMapper.selectById(id) == null) { throw exception(MERCHANT_DETAILS_NOT_EXISTS); } } @Override public MerchantDetailsDO getMerchantDetails(String id) { return merchantDetailsMapper.selectById(id); } @Override public List getMerchantDetailsList(Collection ids) { return merchantDetailsMapper.selectBatchIds(ids); } @Override public PageResult getMerchantDetailsPage(MerchantDetailsPageReqVO pageReqVO) { return merchantDetailsMapper.selectPage(pageReqVO); } @Override public List getMerchantDetailsList(MerchantDetailsExportReqVO exportReqVO) { return merchantDetailsMapper.selectList(exportReqVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-pay/yshop-module-pay-biz/src/main/resources/mapper/merchantdetails/MerchantDetailsMapper.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-score/pom.xml ================================================ co.yixiang.boot yshop ${revision} 4.0.0 yshop-module-score pom yshop-module-score-api yshop-module-score-biz ${project.artifactId} score 模块 ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-api/pom.xml ================================================ yshop-module-score co.yixiang.boot ${revision} 4.0.0 yshop-module-score-api jar ${project.artifactId} score 模块 API,暴露给其它模块调用 co.yixiang.boot yshop-common org.springframework.boot spring-boot-starter-validation true co.yixiang.boot yshop-spring-boot-starter-mybatis co.yixiang.boot yshop-spring-boot-starter-mq com.egzosn pay-java-wx com.egzosn pay-java-ali com.egzosn pay-spring-boot-starter com.egzosn pay-java-web-support ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-api/src/main/java/co/yixiang/yshop/module/score/enums/ErrorCodeConstants.java ================================================ package co.yixiang.yshop.module.score.enums; import co.yixiang.yshop.framework.common.exception.ErrorCode; public interface ErrorCodeConstants { ErrorCode ORDER_NOT_EXISTS = new ErrorCode(1008018000, "积分商城订单不存在"); ErrorCode PRODUCT_NOT_EXISTS = new ErrorCode(1008018001, "积分产品不存在"); ErrorCode SCORE_NOT = new ErrorCode(1008018002, "积分不足"); ErrorCode PRODUCT_NOT_STOCK = new ErrorCode(1008018002, "库存不足"); } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-api/src/main/java/co/yixiang/yshop/module/score/enums/OrderStatusEnum.java ================================================ package co.yixiang.yshop.module.score.enums; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.stream.Stream; /** * @author hupeng * 订单相关枚举 */ @Getter @AllArgsConstructor public enum OrderStatusEnum { STATUS__1(-1,"全部订单"), STATUS_0(0,"待发货"), STATUS_1(1,"待收货"), STATUS_2(2,"已完成"), STATUS_6(6,"已删除"); private Integer value; private String desc; public static OrderStatusEnum toType(int value) { return Stream.of(OrderStatusEnum.values()) .filter(p -> p.value == value) .findAny() .orElse(null); } } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/pom.xml ================================================ yshop-module-score co.yixiang.boot ${revision} 4.0.0 yshop-module-score-biz jar ${project.artifactId} score 模块 co.yixiang.boot yshop-module-score-api ${revision} co.yixiang.boot yshop-module-member-biz ${revision} co.yixiang.boot yshop-spring-boot-starter-biz-tenant co.yixiang.boot yshop-spring-boot-starter-security co.yixiang.boot yshop-spring-boot-starter-redis co.yixiang.boot yshop-spring-boot-starter-mq co.yixiang.boot yshop-spring-boot-starter-test test co.yixiang.boot yshop-spring-boot-starter-excel ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/admin/scoreorder/ScoreOrderController.java ================================================ package co.yixiang.yshop.module.score.controller.admin.scoreorder; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import org.springframework.security.access.prepost.PreAuthorize; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.constraints.*; import jakarta.validation.*; import java.util.*; import java.io.IOException; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.pojo.CommonResult; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.score.controller.admin.scoreorder.vo.*; import co.yixiang.yshop.module.score.dal.dataobject.scoreorder.ScoreOrderDO; import co.yixiang.yshop.module.score.convert.scoreorder.ScoreOrderConvert; import co.yixiang.yshop.module.score.service.scoreorder.ScoreOrderService; @Tag(name = "管理后台 - 积分商城订单") @RestController @RequestMapping("/score/order") @Validated public class ScoreOrderController { @Resource private ScoreOrderService orderService; @PutMapping("/update") @Operation(summary = "更新积分商城订单") @PreAuthorize("@ss.hasPermission('score:order:update')") public CommonResult updateOrder(@Valid @RequestBody ScoreOrderUpdateReqVO updateReqVO) { orderService.updateOrder(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除积分商城订单") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('score:order:delete')") public CommonResult deleteOrder(@RequestParam("id") Long id) { orderService.deleteOrder(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得积分商城订单") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('score:order:query')") public CommonResult getOrder(@RequestParam("id") Long id) { return success(orderService.getOrder(id)); } @GetMapping("/list") @Operation(summary = "获得积分商城订单列表") @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") @PreAuthorize("@ss.hasPermission('score:order:query')") public CommonResult> getOrderList(@RequestParam("ids") Collection ids) { List list = orderService.getOrderList(ids); return success(ScoreOrderConvert.INSTANCE.convertList(list)); } @GetMapping("/page") @Operation(summary = "获得积分商城订单分页") @PreAuthorize("@ss.hasPermission('score:order:query')") public CommonResult> getOrderPage(@Valid ScoreOrderPageReqVO pageVO) { return success(orderService.getOrderPage(pageVO)); } @GetMapping("/export-excel") @Operation(summary = "导出积分商城订单 Excel") @PreAuthorize("@ss.hasPermission('score:order:export')") public void exportOrderExcel(@Valid ScoreOrderExportReqVO exportReqVO, HttpServletResponse response) throws IOException { List list = orderService.getOrderList(exportReqVO); // 导出 Excel List datas = ScoreOrderConvert.INSTANCE.convertList02(list); ExcelUtils.write(response, "积分商城订单.xls", "数据", ScoreOrderExcelVO.class, datas); } @GetMapping("/take") @Operation(summary = "收货") @PreAuthorize("@ss.hasPermission('score:order:update')") public CommonResult take(@RequestParam("id") Long id) { orderService.takeOrder(id); return success(true); } } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/admin/scoreorder/vo/ScoreOrderBaseVO.java ================================================ package co.yixiang.yshop.module.score.controller.admin.scoreorder.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import jakarta.validation.constraints.*; /** * 积分商城订单 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class ScoreOrderBaseVO { @Schema(description = "用户id", required = true, example = "28397") @NotNull(message = "用户id不能为空") private Long uid; @Schema(description = "订单编号", required = true, example = "28397") private String orderId; @Schema(description = "商品id", required = true, example = "31639") @NotNull(message = "商品id不能为空") private Long productId; @Schema(description = "数量", required = true) @NotNull(message = "数量不能为空") private Integer number; @Schema(description = "单个商品积分", required = true) @NotNull(message = "单个商品积分不能为空") private Integer score; @Schema(description = "总消耗积分", required = true) @NotNull(message = "总消耗积分不能为空") private Integer totalScore; @Schema(description = "下单ip") private String ip; @Schema(description = "快递公司编码", required = true) private String expressSn; @Schema(description = "快递编号", required = true) @NotNull(message = "快递编号不能为空") private String expressNumber; @Schema(description = "快递公司", required = true) @NotNull(message = "快递公司不能为空") private String expressCompany; @Schema(description = "收货名称", example = "王五") private String customerName; @Schema(description = "收货电话") private String customerPhone; @Schema(description = "收货地址") private String customerAddress; @Schema(description = "订单状态:0=取消订单,1=正常啊", required = true, example = "1") @NotNull(message = "订单状态:0=取消订单,1=正常啊不能为空") private Boolean status; @Schema(description = "已支付:0=否", required = true, example = "12822") @NotNull(message = "已支付:0=否不能为空") private Integer havePaid; @Schema(description = "已发货:0=否", required = true) @NotNull(message = "已发货:0=否不能为空") private Integer haveDelivered; @Schema(description = "已收货:0=否", required = true) @NotNull(message = "已收货:0=否不能为空") private Integer haveReceived; } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/admin/scoreorder/vo/ScoreOrderCreateReqVO.java ================================================ package co.yixiang.yshop.module.score.controller.admin.scoreorder.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 积分商城订单创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ScoreOrderCreateReqVO extends ScoreOrderBaseVO { } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/admin/scoreorder/vo/ScoreOrderExcelVO.java ================================================ package co.yixiang.yshop.module.score.controller.admin.scoreorder.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.alibaba.excel.annotation.ExcelProperty; /** * 积分商城订单 Excel VO * * @author yshop */ @Data public class ScoreOrderExcelVO { @ExcelProperty("id") private Long id; @ExcelProperty("用户id") private Integer userId; @ExcelProperty("商品id") private Integer productId; @ExcelProperty("数量") private Integer number; @ExcelProperty("单个商品积分") private Integer score; @ExcelProperty("总消耗积分") private Integer totalScore; @ExcelProperty("下单ip") private String ip; @ExcelProperty("快递编号") private String expressNumber; @ExcelProperty("快递公司") private String expressCompany; @ExcelProperty("收货名称") private String customerName; @ExcelProperty("收货电话") private String customerPhone; @ExcelProperty("收货地址") private String customerAddress; @ExcelProperty("订单状态:0=取消订单,1=正常啊") private Boolean status; @ExcelProperty("已支付:0=否") private Integer havePaid; @ExcelProperty("已发货:0=否") private Integer haveDelivered; @ExcelProperty("已收货:0=否") private Integer haveReceived; @ExcelProperty("添加时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/admin/scoreorder/vo/ScoreOrderExportReqVO.java ================================================ package co.yixiang.yshop.module.score.controller.admin.scoreorder.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import java.time.LocalDateTime; import org.springframework.format.annotation.DateTimeFormat; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 积分商城订单 Excel 导出 Request VO,参数和 ScoreOrderPageReqVO 是一致的") @Data public class ScoreOrderExportReqVO { @Schema(description = "用户id", example = "28397") private Integer userId; @Schema(description = "商品id", example = "31639") private Integer productId; @Schema(description = "数量") private Integer number; @Schema(description = "单个商品积分") private Integer score; @Schema(description = "总消耗积分") private Integer totalScore; @Schema(description = "下单ip") private String ip; @Schema(description = "快递编号") private String expressNumber; @Schema(description = "快递公司") private String expressCompany; @Schema(description = "收货名称", example = "王五") private String customerName; @Schema(description = "收货电话") private String customerPhone; @Schema(description = "收货地址") private String customerAddress; @Schema(description = "订单状态:0=取消订单,1=正常啊", example = "1") private Boolean status; @Schema(description = "已支付:0=否", example = "12822") private Integer havePaid; @Schema(description = "已发货:0=否") private Integer haveDelivered; @Schema(description = "已收货:0=否") private Integer haveReceived; @Schema(description = "添加时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/admin/scoreorder/vo/ScoreOrderPageReqVO.java ================================================ package co.yixiang.yshop.module.score.controller.admin.scoreorder.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 积分商城订单分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ScoreOrderPageReqVO extends PageParam { @Schema(description = "用户id", example = "28397") private Long uid; @Schema(description = "订单编号", required = true, example = "28397") private String orderId; @Schema(description = "商品id", example = "31639") private Long productId; @Schema(description = "数量") private Integer number; @Schema(description = "单个商品积分") private Integer score; @Schema(description = "总消耗积分") private Integer totalScore; @Schema(description = "下单ip") private String ip; @Schema(description = "快递编号") private String expressNumber; @Schema(description = "快递公司") private String expressCompany; @Schema(description = "收货名称", example = "王五") private String customerName; @Schema(description = "收货电话") private String customerPhone; @Schema(description = "收货地址") private String customerAddress; @Schema(description = "订单状态:0=取消订单,1=正常啊", example = "1") private Boolean status; @Schema(description = "已支付:0=否", example = "12822") private Integer havePaid; @Schema(description = "已发货:0=否") private Integer haveDelivered; @Schema(description = "已收货:0=否") private Integer haveReceived; @Schema(description = "添加时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; // "状态,-1全部 默认为0待发货 1待收货 2已完成" private Integer type; } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/admin/scoreorder/vo/ScoreOrderRespVO.java ================================================ package co.yixiang.yshop.module.score.controller.admin.scoreorder.vo; import co.yixiang.yshop.module.member.controller.admin.user.vo.UserRespVO; import co.yixiang.yshop.module.score.controller.admin.scoreproduct.vo.ScoreProductRespVO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; @Schema(description = "管理后台 - 积分商城订单 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ScoreOrderRespVO extends ScoreOrderBaseVO { @Schema(description = "id", required = true, example = "19735") private Long id; @Schema(description = "添加时间") private LocalDateTime createTime; @Schema(description = "商品明细") private ScoreProductRespVO scoreProductRespVO; @Schema(description = "用户信息", required = true) private UserRespVO userRespVO; } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/admin/scoreorder/vo/ScoreOrderUpdateReqVO.java ================================================ package co.yixiang.yshop.module.score.controller.admin.scoreorder.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 积分商城订单更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ScoreOrderUpdateReqVO extends ScoreOrderBaseVO { @Schema(description = "id", required = true, example = "19735") @NotNull(message = "id不能为空") private Long id; } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/admin/scoreproduct/ScoreProductController.java ================================================ package co.yixiang.yshop.module.score.controller.admin.scoreproduct; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import org.springframework.security.access.prepost.PreAuthorize; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.constraints.*; import jakarta.validation.*; import java.util.*; import java.io.IOException; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.pojo.CommonResult; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.score.controller.admin.scoreproduct.vo.*; import co.yixiang.yshop.module.score.dal.dataobject.scoreproduct.ScoreProductDO; import co.yixiang.yshop.module.score.convert.scoreproduct.ScoreProductConvert; import co.yixiang.yshop.module.score.service.scoreproduct.ScoreProductService; @Tag(name = "管理后台 - 积分产品") @RestController @RequestMapping("/score/product") @Validated public class ScoreProductController { @Resource private ScoreProductService productService; @PostMapping("/create") @Operation(summary = "创建积分产品") @PreAuthorize("@ss.hasPermission('score:product:create')") public CommonResult createProduct(@Valid @RequestBody ScoreProductCreateReqVO createReqVO) { return success(productService.createProduct(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新积分产品") @PreAuthorize("@ss.hasPermission('score:product:update')") public CommonResult updateProduct(@Valid @RequestBody ScoreProductUpdateReqVO updateReqVO) { productService.updateProduct(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除积分产品") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('score:product:delete')") public CommonResult deleteProduct(@RequestParam("id") Long id) { productService.deleteProduct(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得积分产品") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('score:product:query')") public CommonResult getProduct(@RequestParam("id") Long id) { ScoreProductDO product = productService.getProduct(id); return success(ScoreProductConvert.INSTANCE.convert(product)); } @GetMapping("/list") @Operation(summary = "获得积分产品列表") @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") @PreAuthorize("@ss.hasPermission('score:product:query')") public CommonResult> getProductList(@RequestParam("ids") Collection ids) { List list = productService.getProductList(ids); return success(ScoreProductConvert.INSTANCE.convertList(list)); } @GetMapping("/page") @Operation(summary = "获得积分产品分页") @PreAuthorize("@ss.hasPermission('score:product:query')") public CommonResult> getProductPage(@Valid ScoreProductPageReqVO pageVO) { PageResult pageResult = productService.getProductPage(pageVO); return success(ScoreProductConvert.INSTANCE.convertPage(pageResult)); } @GetMapping("/export-excel") @Operation(summary = "导出积分产品 Excel") @PreAuthorize("@ss.hasPermission('score:product:export')") public void exportProductExcel(@Valid ScoreProductExportReqVO exportReqVO, HttpServletResponse response) throws IOException { List list = productService.getProductList(exportReqVO); // 导出 Excel List datas = ScoreProductConvert.INSTANCE.convertList02(list); ExcelUtils.write(response, "积分产品.xls", "数据", ScoreProductExcelVO.class, datas); } } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/admin/scoreproduct/vo/ScoreProductBaseVO.java ================================================ package co.yixiang.yshop.module.score.controller.admin.scoreproduct.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import jakarta.validation.constraints.*; /** * 积分产品 Base VO,提供给添加、修改、详细的子 VO 使用 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class ScoreProductBaseVO { @Schema(description = "产品标题", required = true) @NotNull(message = "产品标题不能为空") private String title; @Schema(description = "主图", required = true) @NotNull(message = "主图不能为空") private String image; @Schema(description = "组图", required = true) @NotNull(message = "组图不能为空") private List images; @Schema(description = "详情", required = true) @NotNull(message = "详情不能为空") private String desc; @Schema(description = "消耗积分", required = true) @NotNull(message = "消耗积分不能为空") private Integer score; @Schema(description = "权重", required = true) private Integer weigh; @Schema(description = "库存", required = true) @NotNull(message = "库存不能为空") private Integer stock; @Schema(description = "销售量", required = true) private Integer sales; @Schema(description = "是否上架:0=否,1=是", required = true) private Integer isSwitch; } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/admin/scoreproduct/vo/ScoreProductCreateReqVO.java ================================================ package co.yixiang.yshop.module.score.controller.admin.scoreproduct.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 积分产品创建 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ScoreProductCreateReqVO extends ScoreProductBaseVO { } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/admin/scoreproduct/vo/ScoreProductExcelVO.java ================================================ package co.yixiang.yshop.module.score.controller.admin.scoreproduct.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.alibaba.excel.annotation.ExcelProperty; /** * 积分产品 Excel VO * * @author yshop */ @Data public class ScoreProductExcelVO { @ExcelProperty("id") private Long id; @ExcelProperty("产品标题") private String title; @ExcelProperty("主图") private String image; @ExcelProperty("组图") private List images; @ExcelProperty("详情") private String desc; @ExcelProperty("消耗积分") private Integer score; @ExcelProperty("权重") private Integer weigh; @ExcelProperty("库存") private Integer stock; @ExcelProperty("销售量") private Integer sales; @ExcelProperty("是否上架:0=否,1=是") private Integer isSwitch; @ExcelProperty("添加时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/admin/scoreproduct/vo/ScoreProductExportReqVO.java ================================================ package co.yixiang.yshop.module.score.controller.admin.scoreproduct.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import java.time.LocalDateTime; import org.springframework.format.annotation.DateTimeFormat; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 积分产品 Excel 导出 Request VO,参数和 ScoreProductPageReqVO 是一致的") @Data public class ScoreProductExportReqVO { @Schema(description = "产品标题") private String title; @Schema(description = "主图") private String image; @Schema(description = "组图") private String images; @Schema(description = "详情") private String desc; @Schema(description = "消耗积分") private Integer score; @Schema(description = "权重") private Integer weigh; @Schema(description = "库存") private Integer stock; @Schema(description = "销售量") private Integer sales; @Schema(description = "是否上架:0=否,1=是") private Integer isSwitch; @Schema(description = "添加时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/admin/scoreproduct/vo/ScoreProductPageReqVO.java ================================================ package co.yixiang.yshop.module.score.controller.admin.scoreproduct.vo; import lombok.*; import java.util.*; import io.swagger.v3.oas.annotations.media.Schema; import co.yixiang.yshop.framework.common.pojo.PageParam; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 积分产品分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ScoreProductPageReqVO extends PageParam { @Schema(description = "产品标题") private String title; @Schema(description = "主图") private String image; @Schema(description = "组图") private List images; @Schema(description = "详情") private String desc; @Schema(description = "消耗积分") private Integer score; @Schema(description = "权重") private Integer weigh; @Schema(description = "库存") private Integer stock; @Schema(description = "销售量") private Integer sales; @Schema(description = "是否上架:0=否,1=是") private Integer isSwitch; @Schema(description = "添加时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/admin/scoreproduct/vo/ScoreProductRespVO.java ================================================ package co.yixiang.yshop.module.score.controller.admin.scoreproduct.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; @Schema(description = "管理后台 - 积分产品 Response VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ScoreProductRespVO extends ScoreProductBaseVO { @Schema(description = "id", required = true, example = "8624") private Long id; @Schema(description = "添加时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/admin/scoreproduct/vo/ScoreProductUpdateReqVO.java ================================================ package co.yixiang.yshop.module.score.controller.admin.scoreproduct.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import jakarta.validation.constraints.*; @Schema(description = "管理后台 - 积分产品更新 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class ScoreProductUpdateReqVO extends ScoreProductBaseVO { @Schema(description = "id", required = true, example = "8624") @NotNull(message = "id不能为空") private Long id; } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/app/order/AppScoreOrderController.java ================================================ package co.yixiang.yshop.module.score.controller.app.order; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.security.core.annotations.PreAuthenticated; import co.yixiang.yshop.module.score.controller.app.order.param.AppScoreOrderParam; import co.yixiang.yshop.module.score.controller.app.order.vo.AppScoreOrderVO; import co.yixiang.yshop.module.score.controller.app.product.param.AppScoreProductQueryParam; import co.yixiang.yshop.module.score.controller.app.product.vo.AppScoreProductVO; import co.yixiang.yshop.module.score.service.scoreorder.AppScoreOrderService; import co.yixiang.yshop.module.score.service.scoreproduct.AppScoreProductService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import jakarta.validation.Valid; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @Tag(name = "用户 APP - 积分订单") @RestController @RequestMapping("/score-order") @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class AppScoreOrderController { private final AppScoreOrderService appScoreOrderService; /** * 提交积分订单 */ @PreAuthenticated @PostMapping("/submit") @Operation(summary = "提交积分订单") public CommonResult submit(@RequestBody @Valid AppScoreOrderParam appScoreOrderParam){ Long uid = getLoginUserId(); appScoreOrderService.submit(uid,appScoreOrderParam); return success(true); } /** * 订单列表 */ @PreAuthenticated @GetMapping("/list") @Operation(summary = "订单列表") @Parameters({ @Parameter(name = "type", description = "状态,-1全部 默认为0待发货 1待收货 2已完成", required = true, example = "1"), @Parameter(name = "page", description = "页码,默认为1", required = true, example = "1"), @Parameter(name = "limit", description = "页大小,默认为10", required = true, example = "10 ") }) public CommonResult> orderList(@RequestParam(value = "type", defaultValue = "0") int type, @RequestParam(value = "page", defaultValue = "1") int page, @RequestParam(value = "limit", defaultValue = "10") int limit) { Long uid = getLoginUserId(); return success(appScoreOrderService.orderList(uid, type, page, limit)); } /** * 订单详情 */ @PreAuthenticated @GetMapping("/detail") @Operation(summary = "订单详情") @Parameters({ @Parameter(name = "id", description = "id", required = true, example = "1") }) public CommonResult orderList(@RequestParam(value = "id", defaultValue = "0") Long id) { Long uid = getLoginUserId(); return success(appScoreOrderService.orderDetail(uid, id)); } /** * 收货 */ @PreAuthenticated @GetMapping("/take") @Operation(summary = "收货") public CommonResult take(@RequestParam(value = "id", defaultValue = "0") Long id){ Long uid = getLoginUserId(); appScoreOrderService.take(uid,id); return success(true); } } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/app/order/param/AppScoreOrderParam.java ================================================ package co.yixiang.yshop.module.score.controller.app.order.param; import co.yixiang.yshop.framework.common.params.QueryParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import jakarta.validation.constraints.NotBlank; /** *

* 积分订单参数对象 *

* * @author hupeng * @date 2023-11-30 */ @Data @Schema(description = "用户 APP - 积分订单参数对象") public class AppScoreOrderParam { @Schema(description = "积分商品ID", required = true) @NotBlank(message = "参数有误") private String productId; @Schema(description = "地址ID", required = true) @NotBlank(message = "请选择地址") private String addressId; @Schema(description = "数量", required = true) private String num; } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/app/order/vo/AppScoreOrderVO.java ================================================ package co.yixiang.yshop.module.score.controller.app.order.vo; import co.yixiang.yshop.module.score.controller.app.product.vo.AppScoreProductVO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; /** * 积分商城订单 AppScoreOrderVO VO,提供给添加、修改、详细的子 VO 使用 */ @Data public class AppScoreOrderVO { private Long id; @Schema(description = "用户id", required = true, example = "28397") private Long uid; @Schema(description = "订单编号", required = true, example = "28397") private String orderId; @Schema(description = "商品id", required = true, example = "31639") private Long productId; @Schema(description = "数量", required = true) private Integer number; @Schema(description = "单个商品积分", required = true) private Integer score; @Schema(description = "总消耗积分", required = true) @NotNull(message = "总消耗积分不能为空") private Integer totalScore; @Schema(description = "下单ip") private String ip; @Schema(description = "快递公司编号", required = true) private String expressSn; @Schema(description = "快递编号", required = true) private String expressNumber; @Schema(description = "快递公司", required = true) private String expressCompany; @Schema(description = "收货名称", example = "王五") private String customerName; @Schema(description = "收货电话") private String customerPhone; @Schema(description = "收货地址") private String customerAddress; @Schema(description = "订单状态:0=取消订单,1=正常啊", required = true, example = "1") private Boolean status; @Schema(description = "已支付:0=否", required = true, example = "12822") private Integer havePaid; @Schema(description = "已发货:0=否", required = true) private Integer haveDelivered; @Schema(description = "已收货:0=否", required = true) private Integer haveReceived; @Schema(description = "订单状态字符串", required = true) private String statusText; private LocalDateTime createTime; private AppScoreProductVO product; } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/app/product/AppScoreProductController.java ================================================ package co.yixiang.yshop.module.score.controller.app.product; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.module.score.controller.app.product.param.AppScoreProductQueryParam; import co.yixiang.yshop.module.score.controller.app.product.vo.AppScoreProductVO; import co.yixiang.yshop.module.score.service.scoreproduct.AppScoreProductService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; 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.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "用户 APP - 积分商品") @RestController @RequestMapping("/score-product") @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class AppScoreProductController { private final AppScoreProductService appScoreProductService; /** * 获取积分商品列表 */ @GetMapping("/list") @Operation(summary = "获取积分商品列表") public CommonResult> goodsList(AppScoreProductQueryParam productQueryParam){ return success(appScoreProductService.getList(productQueryParam.getPage(),productQueryParam.getLimit())); } /** * 获取积分商品详情 */ @GetMapping("/detail") @Operation(summary = "获取积分商品详情") public CommonResult goodsDetail(@RequestParam(value = "id", defaultValue = "0") Long id){ return success(appScoreProductService.getDetail(id)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/app/product/param/AppScoreProductQueryParam.java ================================================ package co.yixiang.yshop.module.score.controller.app.product.param; import co.yixiang.yshop.framework.common.params.QueryParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; /** *

* 商品查询参数对象 *

* * @author hupeng * @date 2023-11-26 */ @Data @EqualsAndHashCode(callSuper = true) @Schema(description = "用户 APP - 积分商品表查询参数") public class AppScoreProductQueryParam extends QueryParam { private static final long serialVersionUID = 1L; @Schema(description = "关键字", required = true) private String keyword; } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/controller/app/product/vo/AppScoreProductVO.java ================================================ package co.yixiang.yshop.module.score.controller.app.product.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotNull; import java.util.List; /** * 积分产品 AppScoreProductVO VO * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 */ @Data public class AppScoreProductVO { @Schema(description = "id", required = true, example = "8624") private Long id; @Schema(description = "产品标题", required = true) private String title; @Schema(description = "主图", required = true) private String image; @Schema(description = "组图", required = true) private List images; @Schema(description = "详情", required = true) private String desc; @Schema(description = "消耗积分", required = true) private Integer score; @Schema(description = "库存", required = true) private Integer stock; @Schema(description = "销售量", required = true) private Integer sales; } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/convert/scoreorder/ScoreOrderConvert.java ================================================ package co.yixiang.yshop.module.score.convert.scoreorder; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.score.controller.app.order.vo.AppScoreOrderVO; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import co.yixiang.yshop.module.score.controller.admin.scoreorder.vo.*; import co.yixiang.yshop.module.score.dal.dataobject.scoreorder.ScoreOrderDO; /** * 积分商城订单 Convert * * @author yshop */ @Mapper public interface ScoreOrderConvert { ScoreOrderConvert INSTANCE = Mappers.getMapper(ScoreOrderConvert.class); ScoreOrderDO convert(ScoreOrderCreateReqVO bean); ScoreOrderDO convert(ScoreOrderUpdateReqVO bean); ScoreOrderRespVO convert(ScoreOrderDO bean); AppScoreOrderVO convert01(ScoreOrderDO bean); List convertList(List list); List convertList01(List list); PageResult convertPage(PageResult page); List convertList02(List list); } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/convert/scoreproduct/ScoreProductConvert.java ================================================ package co.yixiang.yshop.module.score.convert.scoreproduct; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.score.controller.app.product.vo.AppScoreProductVO; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import co.yixiang.yshop.module.score.controller.admin.scoreproduct.vo.*; import co.yixiang.yshop.module.score.dal.dataobject.scoreproduct.ScoreProductDO; /** * 积分产品 Convert * * @author yshop */ @Mapper public interface ScoreProductConvert { ScoreProductConvert INSTANCE = Mappers.getMapper(ScoreProductConvert.class); ScoreProductDO convert(ScoreProductCreateReqVO bean); ScoreProductDO convert(ScoreProductUpdateReqVO bean); ScoreProductRespVO convert(ScoreProductDO bean); AppScoreProductVO convert01(ScoreProductDO bean); List convertList(List list); List convertList01(List list); PageResult convertPage(PageResult page); List convertList02(List list); } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/dal/dataobject/scoreorder/ScoreOrderDO.java ================================================ package co.yixiang.yshop.module.score.dal.dataobject.scoreorder; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; /** * 积分商城订单 DO * * @author yshop */ @TableName("yshop_score_order") @KeySequence("yshop_score_order_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class ScoreOrderDO extends BaseDO { /** * id */ @TableId private Long id; /** * 用户id */ private Long uid; /** * 订单编号 */ private String orderId; /** * 商品id */ private Long productId; /** * 数量 */ private Integer number; /** * 单个商品积分 */ private Integer score; /** * 总消耗积分 */ private Integer totalScore; /** * 下单ip */ private String ip; /** * 快递公司编码 */ private String expressSn; /** * 快递编号 */ private String expressNumber; /** * 快递公司 */ private String expressCompany; /** * 收货名称 */ private String customerName; /** * 收货电话 */ private String customerPhone; /** * 收货地址 */ private String customerAddress; /** * 订单状态:0=取消订单,1=正常啊 */ private Boolean status; /** * 已支付:0=否 */ private Integer havePaid; /** * 已发货:0=否 */ private Integer haveDelivered; /** * 已收货:0=否 */ private Integer haveReceived; } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/dal/dataobject/scoreproduct/ScoreProductDO.java ================================================ package co.yixiang.yshop.module.score.dal.dataobject.scoreproduct; import co.yixiang.yshop.framework.mybatis.core.type.StringListTypeHandler; import lombok.*; import java.util.*; import java.time.LocalDateTime; import java.time.LocalDateTime; import com.baomidou.mybatisplus.annotation.*; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; /** * 积分产品 DO * * @author yshop */ @TableName(value = "yshop_score_product",autoResultMap = true) @KeySequence("yshop_score_product_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class ScoreProductDO extends BaseDO { /** * id */ @TableId private Long id; /** * 产品标题 */ private String title; /** * 主图 */ private String image; /** * 组图 */ @TableField(typeHandler = StringListTypeHandler.class) private List images; /** * 详情 */ @TableField(value = "`desc`") private String desc; /** * 消耗积分 */ private Integer score; /** * 权重 */ private Integer weigh; /** * 库存 */ private Integer stock; /** * 销售量 */ private Integer sales; /** * 是否上架:0=否,1=是 */ private Integer isSwitch; } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/dal/mysql/scoreorder/ScoreOrderMapper.java ================================================ package co.yixiang.yshop.module.score.dal.mysql.scoreorder; import java.util.*; import co.yixiang.yshop.framework.common.enums.OrderInfoEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.score.dal.dataobject.scoreorder.ScoreOrderDO; import co.yixiang.yshop.module.score.enums.OrderStatusEnum; import org.apache.ibatis.annotations.Mapper; import co.yixiang.yshop.module.score.controller.admin.scoreorder.vo.*; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; /** * 积分商城订单 Mapper * * @author yshop */ @Mapper public interface ScoreOrderMapper extends BaseMapperX { default PageResult selectPage(ScoreOrderPageReqVO reqVO) { LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX(); wrapper.eqIfPresent(ScoreOrderDO::getOrderId, reqVO.getOrderId()) .likeIfPresent(ScoreOrderDO::getCustomerName, reqVO.getCustomerName()) .eqIfPresent(ScoreOrderDO::getCustomerPhone, reqVO.getCustomerPhone()) .orderByDesc(ScoreOrderDO::getId); switch (OrderStatusEnum.toType(reqVO.getType())) { case STATUS__1: break; //待发货 case STATUS_0: wrapper.eq(ScoreOrderDO::getHavePaid, OrderInfoEnum.PAY_STATUS_1.getValue()) .eq(ScoreOrderDO::getHaveDelivered, OrderInfoEnum.STATUS_0.getValue()) .eq(ScoreOrderDO::getHaveReceived, OrderInfoEnum.STATUS_0.getValue()); break; //待收货 case STATUS_1: wrapper.eq(ScoreOrderDO::getHavePaid, OrderInfoEnum.PAY_STATUS_1.getValue()) .eq(ScoreOrderDO::getHaveDelivered, OrderInfoEnum.STATUS_1.getValue()) .eq(ScoreOrderDO::getHaveReceived, OrderInfoEnum.STATUS_0.getValue()); break; //已完成 case STATUS_2: wrapper.eq(ScoreOrderDO::getHavePaid, OrderInfoEnum.PAY_STATUS_1.getValue()) .eq(ScoreOrderDO::getHaveDelivered, OrderInfoEnum.STATUS_1.getValue()) .eq(ScoreOrderDO::getHaveReceived, OrderInfoEnum.STATUS_1.getValue()); break; default: } return selectPage(reqVO, wrapper); } default List selectList(ScoreOrderExportReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .eqIfPresent(ScoreOrderDO::getProductId, reqVO.getProductId()) .eqIfPresent(ScoreOrderDO::getNumber, reqVO.getNumber()) .eqIfPresent(ScoreOrderDO::getScore, reqVO.getScore()) .eqIfPresent(ScoreOrderDO::getTotalScore, reqVO.getTotalScore()) .eqIfPresent(ScoreOrderDO::getIp, reqVO.getIp()) .eqIfPresent(ScoreOrderDO::getExpressNumber, reqVO.getExpressNumber()) .eqIfPresent(ScoreOrderDO::getExpressCompany, reqVO.getExpressCompany()) .likeIfPresent(ScoreOrderDO::getCustomerName, reqVO.getCustomerName()) .eqIfPresent(ScoreOrderDO::getCustomerPhone, reqVO.getCustomerPhone()) .eqIfPresent(ScoreOrderDO::getCustomerAddress, reqVO.getCustomerAddress()) .eqIfPresent(ScoreOrderDO::getStatus, reqVO.getStatus()) .eqIfPresent(ScoreOrderDO::getHavePaid, reqVO.getHavePaid()) .eqIfPresent(ScoreOrderDO::getHaveDelivered, reqVO.getHaveDelivered()) .eqIfPresent(ScoreOrderDO::getHaveReceived, reqVO.getHaveReceived()) .betweenIfPresent(ScoreOrderDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(ScoreOrderDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/dal/mysql/scoreproduct/ScoreProductMapper.java ================================================ package co.yixiang.yshop.module.score.dal.mysql.scoreproduct; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.score.dal.dataobject.scoreproduct.ScoreProductDO; import org.apache.ibatis.annotations.Mapper; import co.yixiang.yshop.module.score.controller.admin.scoreproduct.vo.*; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; /** * 积分产品 Mapper * * @author yshop */ @Mapper public interface ScoreProductMapper extends BaseMapperX { default PageResult selectPage(ScoreProductPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(ScoreProductDO::getTitle, reqVO.getTitle()) .eqIfPresent(ScoreProductDO::getImage, reqVO.getImage()) .eqIfPresent(ScoreProductDO::getImages, reqVO.getImages()) .eqIfPresent(ScoreProductDO::getDesc, reqVO.getDesc()) .eqIfPresent(ScoreProductDO::getScore, reqVO.getScore()) .eqIfPresent(ScoreProductDO::getWeigh, reqVO.getWeigh()) .eqIfPresent(ScoreProductDO::getStock, reqVO.getStock()) .eqIfPresent(ScoreProductDO::getSales, reqVO.getSales()) .betweenIfPresent(ScoreProductDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(ScoreProductDO::getId)); } default List selectList(ScoreProductExportReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .eqIfPresent(ScoreProductDO::getTitle, reqVO.getTitle()) .eqIfPresent(ScoreProductDO::getImage, reqVO.getImage()) .eqIfPresent(ScoreProductDO::getImages, reqVO.getImages()) .eqIfPresent(ScoreProductDO::getDesc, reqVO.getDesc()) .eqIfPresent(ScoreProductDO::getScore, reqVO.getScore()) .eqIfPresent(ScoreProductDO::getWeigh, reqVO.getWeigh()) .eqIfPresent(ScoreProductDO::getStock, reqVO.getStock()) .eqIfPresent(ScoreProductDO::getSales, reqVO.getSales()) .betweenIfPresent(ScoreProductDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(ScoreProductDO::getId)); } /** * 普通商品 减库存 加销量 * @param num * @param productId * @return */ @Update("update yshop_score_product set stock=stock-#{num}, sales=sales+#{num}" + " where id=#{productId} and stock >= #{num}") int decStockIncSales(@Param("num") Integer num, @Param("productId") Long productId); } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/service/scoreorder/AppScoreOrderService.java ================================================ package co.yixiang.yshop.module.score.service.scoreorder; import co.yixiang.yshop.module.score.controller.app.order.param.AppScoreOrderParam; import co.yixiang.yshop.module.score.controller.app.order.vo.AppScoreOrderVO; import co.yixiang.yshop.module.score.dal.dataobject.scoreorder.ScoreOrderDO; import com.baomidou.mybatisplus.extension.service.IService; import java.util.List; /** * 积分商城订单 Service 接口 * * @author yshop */ public interface AppScoreOrderService extends IService { /** * 提交 * @param uid * @param appScoreOrderParam */ void submit(Long uid,AppScoreOrderParam appScoreOrderParam); /** * 订单列表 * @param uid 用户id * @param type * @param page page * @param limit limit * @return list */ List orderList(Long uid, int type, int page, int limit); /** * 订单详情 * @param uid 用户id * @param id */ AppScoreOrderVO orderDetail(Long uid, Long id); /** * 收货 * @param uid * @param id */ void take(Long uid, Long id); } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/service/scoreorder/AppScoreOrderServiceImpl.java ================================================ package co.yixiang.yshop.module.score.service.scoreorder; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.constant.ShopConstants; import co.yixiang.yshop.framework.common.enums.OrderInfoEnum; import co.yixiang.yshop.framework.common.enums.ShopCommonEnum; import co.yixiang.yshop.framework.common.exception.ErrorCode; import co.yixiang.yshop.framework.common.util.servlet.ServletUtils; import co.yixiang.yshop.framework.ip.core.utils.IPUtils; import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO; import co.yixiang.yshop.module.member.dal.dataobject.useraddress.UserAddressDO; import co.yixiang.yshop.module.member.enums.BillDetailEnum; import co.yixiang.yshop.module.member.service.user.MemberUserService; import co.yixiang.yshop.module.member.service.useraddress.AppUserAddressService; import co.yixiang.yshop.module.member.service.userbill.UserBillService; import co.yixiang.yshop.module.score.controller.app.order.param.AppScoreOrderParam; import co.yixiang.yshop.module.score.controller.app.order.vo.AppScoreOrderVO; import co.yixiang.yshop.module.score.convert.scoreorder.ScoreOrderConvert; import co.yixiang.yshop.module.score.dal.dataobject.scoreorder.ScoreOrderDO; import co.yixiang.yshop.module.score.dal.dataobject.scoreproduct.ScoreProductDO; import co.yixiang.yshop.module.score.dal.mysql.scoreorder.ScoreOrderMapper; import co.yixiang.yshop.module.score.dal.mysql.scoreproduct.ScoreProductMapper; import co.yixiang.yshop.module.score.enums.OrderStatusEnum; import co.yixiang.yshop.module.score.service.scoreproduct.AppScoreProductService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.member.enums.ErrorCodeConstants.USER_ADDRESS_NOT_EXISTS; import static co.yixiang.yshop.module.score.enums.ErrorCodeConstants.*; /** * 积分商城订单 Service 实现类 * * @author yshop */ @Service @Validated public class AppScoreOrderServiceImpl extends ServiceImpl implements AppScoreOrderService { @Resource private ScoreOrderMapper orderMapper; @Resource private ScoreProductMapper scoreProductMapper; @Resource private AppScoreProductService appScoreProductService; @Resource private MemberUserService userService; @Resource private RedissonClient redissonClient; @Resource private UserBillService billService; @Resource private AppUserAddressService appUserAddressService; private static final String STOCK_LOCK_KEY = "score:order:stock:lock"; /** * 提交 * @param uid * @param appScoreOrderParam */ @Override @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public void submit(Long uid,AppScoreOrderParam appScoreOrderParam) { ScoreProductDO scoreProductDO = appScoreProductService.getById(appScoreOrderParam.getProductId()); if(scoreProductDO == null){ throw exception(PRODUCT_NOT_EXISTS); } UserAddressDO userAddressDO = appUserAddressService.getById(appScoreOrderParam.getAddressId()); if(userAddressDO == null){ throw exception(USER_ADDRESS_NOT_EXISTS); } MemberUserDO memberUserDO = userService.getById(uid); if(scoreProductDO.getStock() <= 0){ throw exception(PRODUCT_NOT_STOCK); } if(NumberUtil.compare(memberUserDO.getIntegral().intValue(),scoreProductDO.getScore()) < 0){ throw exception(SCORE_NOT); } //生成分布式唯一值 String orderSn = IdUtil.getSnowflake(0, 0).nextIdStr(); ScoreOrderDO scoreOrderDO = ScoreOrderDO.builder() .havePaid(ShopCommonEnum.IS_STATUS_1.getValue()) .customerAddress(userAddressDO.getAddress() + userAddressDO.getDetail()) .customerName(userAddressDO.getRealName()) .customerPhone(userAddressDO.getPhone()) .ip(ServletUtils.getClientIP()) .orderId(orderSn) .productId(scoreProductDO.getId()) .number(Integer.valueOf(appScoreOrderParam.getNum())) .score(scoreProductDO.getScore()) .totalScore(scoreProductDO.getScore()) .uid(uid) .build(); //保存 this.save(scoreOrderDO); //减去积分 userService.decScore(uid,scoreOrderDO.getScore()); this.deStockIncSale(scoreProductDO.getId(),Integer.valueOf(appScoreOrderParam.getNum())); //增加流水 billService.expend(uid, "积分兑换", BillDetailEnum.CATEGORY_2.getValue(), BillDetailEnum.TYPE_3.getValue(), scoreProductDO.getScore().doubleValue(), memberUserDO.getIntegral().doubleValue(), scoreProductDO.getScore() + "积分兑换商品"); } /** * 订单列表 * * @param uid 用户id * @param type OrderStatusEnum * @param page page * @param limit limit * @return list */ @Override public List orderList(Long uid, int type, int page, int limit) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ScoreOrderDO::getUid, uid); wrapper.orderByDesc(ScoreOrderDO::getId); switch (OrderStatusEnum.toType(type)) { case STATUS__1: break; //待发货 case STATUS_0: wrapper.eq(ScoreOrderDO::getHavePaid, OrderInfoEnum.PAY_STATUS_1.getValue()) .eq(ScoreOrderDO::getHaveDelivered, OrderInfoEnum.STATUS_0.getValue()) .eq(ScoreOrderDO::getHaveReceived, OrderInfoEnum.STATUS_0.getValue()); break; //待收货 case STATUS_1: wrapper.eq(ScoreOrderDO::getHavePaid, OrderInfoEnum.PAY_STATUS_1.getValue()) .eq(ScoreOrderDO::getHaveDelivered, OrderInfoEnum.STATUS_1.getValue()) .eq(ScoreOrderDO::getHaveReceived, OrderInfoEnum.STATUS_0.getValue()); break; //已完成 case STATUS_2: wrapper.eq(ScoreOrderDO::getHavePaid, OrderInfoEnum.PAY_STATUS_1.getValue()) //.eq(ScoreOrderDO::getHaveDelivered, OrderInfoEnum.STATUS_1.getValue()) .eq(ScoreOrderDO::getHaveReceived, OrderInfoEnum.STATUS_1.getValue()); break; default: } Page pageModel = new Page<>(page, limit); IPage pageList = orderMapper.selectPage(pageModel, wrapper); List list = ScoreOrderConvert.INSTANCE.convertList01(pageList.getRecords()); return list.stream().map(this::handleOrder).collect(Collectors.toList()); } /** * 订单详情 * @param uid 用户id * @param id */ @Override public AppScoreOrderVO orderDetail(Long uid, Long id) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ScoreOrderDO::getUid, uid).eq(ScoreOrderDO::getId,id); ScoreOrderDO scoreOrderDO = this.getOne(wrapper); AppScoreOrderVO appScoreOrderVO = ScoreOrderConvert.INSTANCE.convert01(scoreOrderDO); return this.handleOrder(appScoreOrderVO); } /** * 收货 * @param uid * @param id */ @Override public void take(Long uid, Long id) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ScoreOrderDO::getUid, uid).eq(ScoreOrderDO::getId,id); ScoreOrderDO scoreOrderDO = this.getOne(wrapper); if(scoreOrderDO == null){ throw exception(ORDER_NOT_EXISTS); } scoreOrderDO.setHaveReceived(ShopCommonEnum.DEFAULT_1.getValue()); this.updateById(scoreOrderDO); } /** * 处理订单 * @param order * @return */ private AppScoreOrderVO handleOrder(AppScoreOrderVO order) { order.setProduct(appScoreProductService.getDetail(order.getProductId())); if (OrderInfoEnum.PAY_STATUS_1.getValue().equals(order.getHavePaid()) && OrderInfoEnum.STATUS_0.getValue().equals(order.getHaveDelivered()) && OrderInfoEnum.STATUS_0.getValue().equals(order.getHaveReceived())) { order.setStatusText("待发货"); } else if (OrderInfoEnum.PAY_STATUS_1.getValue().equals(order.getHavePaid()) && OrderInfoEnum.STATUS_1.getValue().equals(order.getHaveDelivered()) && OrderInfoEnum.STATUS_0.getValue().equals(order.getHaveReceived())) { order.setStatusText("待收货"); } else if (OrderInfoEnum.PAY_STATUS_1.getValue().equals(order.getHavePaid()) && OrderInfoEnum.STATUS_1.getValue().equals(order.getHaveReceived())) { order.setStatusText("已完成"); } return order; } /** * 减库存增加销量 * * @param productId 商品id * @param number 商品数量 */ private void deStockIncSale(Long productId, Integer number) { //对库存加锁 RLock lock = redissonClient.getLock(STOCK_LOCK_KEY); if (lock.tryLock()) { try { scoreProductMapper.decStockIncSales(number,productId); }catch (Exception ex) { log.error("[deStockIncSale][执行异常]", ex); throw exception(new ErrorCode(999999,ex.getMessage())); } finally { lock.unlock(); } } } } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/service/scoreorder/ScoreOrderService.java ================================================ package co.yixiang.yshop.module.score.service.scoreorder; import java.util.*; import jakarta.validation.*; import co.yixiang.yshop.module.score.controller.admin.scoreorder.vo.*; import co.yixiang.yshop.module.score.dal.dataobject.scoreorder.ScoreOrderDO; import co.yixiang.yshop.framework.common.pojo.PageResult; /** * 积分商城订单 Service 接口 * * @author yshop */ public interface ScoreOrderService { /** * 创建积分商城订单 * * @param createReqVO 创建信息 * @return 编号 */ Long createOrder(@Valid ScoreOrderCreateReqVO createReqVO); /** * 更新积分商城订单 * * @param updateReqVO 更新信息 */ void updateOrder(@Valid ScoreOrderUpdateReqVO updateReqVO); /** * 删除积分商城订单 * * @param id 编号 */ void deleteOrder(Long id); /** * 收货 * * @param id 编号 */ void takeOrder(Long id); /** * 获得积分商城订单 * * @param id 编号 * @return 积分商城订单 */ ScoreOrderRespVO getOrder(Long id); /** * 获得积分商城订单列表 * * @param ids 编号 * @return 积分商城订单列表 */ List getOrderList(Collection ids); /** * 获得积分商城订单分页 * * @param pageReqVO 分页查询 * @return 积分商城订单分页 */ PageResult getOrderPage(ScoreOrderPageReqVO pageReqVO); /** * 获得积分商城订单列表, 用于 Excel 导出 * * @param exportReqVO 查询条件 * @return 积分商城订单列表 */ List getOrderList(ScoreOrderExportReqVO exportReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/service/scoreorder/ScoreOrderServiceImpl.java ================================================ package co.yixiang.yshop.module.score.service.scoreorder; import co.yixiang.yshop.framework.common.enums.ShopCommonEnum; import co.yixiang.yshop.module.member.controller.admin.user.vo.UserRespVO; import co.yixiang.yshop.module.member.convert.user.UserConvert; import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO; import co.yixiang.yshop.module.member.dal.mysql.user.MemberUserMapper; import co.yixiang.yshop.module.score.convert.scoreproduct.ScoreProductConvert; import co.yixiang.yshop.module.score.dal.dataobject.scoreproduct.ScoreProductDO; import co.yixiang.yshop.module.score.dal.mysql.scoreproduct.ScoreProductMapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import java.util.*; import co.yixiang.yshop.module.score.controller.admin.scoreorder.vo.*; import co.yixiang.yshop.module.score.dal.dataobject.scoreorder.ScoreOrderDO; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.score.convert.scoreorder.ScoreOrderConvert; import co.yixiang.yshop.module.score.dal.mysql.scoreorder.ScoreOrderMapper; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.score.enums.ErrorCodeConstants.*; /** * 积分商城订单 Service 实现类 * * @author yshop */ @Service @Validated public class ScoreOrderServiceImpl implements ScoreOrderService { @Resource private ScoreOrderMapper orderMapper; @Resource private ScoreProductMapper scoreProductMapper; @Resource private MemberUserMapper memberUserMapper; @Override public Long createOrder(ScoreOrderCreateReqVO createReqVO) { // 插入 ScoreOrderDO order = ScoreOrderConvert.INSTANCE.convert(createReqVO); orderMapper.insert(order); // 返回 return order.getId(); } @Override public void updateOrder(ScoreOrderUpdateReqVO updateReqVO) { // 校验存在 validateOrderExists(updateReqVO.getId()); // 更新 ScoreOrderDO updateObj = ScoreOrderConvert.INSTANCE.convert(updateReqVO); updateObj.setHaveDelivered(ShopCommonEnum.DEFAULT_1.getValue()); orderMapper.updateById(updateObj); } @Override public void deleteOrder(Long id) { // 校验存在 validateOrderExists(id); // 删除 orderMapper.deleteById(id); } @Override public void takeOrder(Long id) { // 校验存在 ScoreOrderDO scoreOrderDO = orderMapper.selectById(id); if (scoreOrderDO == null) { throw exception(ORDER_NOT_EXISTS); } scoreOrderDO.setHaveReceived(ShopCommonEnum.DEFAULT_1.getValue()); orderMapper.updateById(scoreOrderDO); } private void validateOrderExists(Long id) { if (orderMapper.selectById(id) == null) { throw exception(ORDER_NOT_EXISTS); } } @Override public ScoreOrderRespVO getOrder(Long id) { ScoreOrderDO scoreOrderDO = orderMapper.selectById(id); ScoreOrderRespVO scoreOrderRespVO = ScoreOrderConvert.INSTANCE.convert(scoreOrderDO); ScoreProductDO scoreProductDO = scoreProductMapper.selectById(scoreOrderRespVO.getProductId()); scoreOrderRespVO.setScoreProductRespVO(ScoreProductConvert.INSTANCE.convert(scoreProductDO)); MemberUserDO memberUserDO = memberUserMapper.selectById(scoreOrderRespVO.getUid()); UserRespVO userRespVO = UserConvert.INSTANCE.convert4(memberUserDO); scoreOrderRespVO.setUserRespVO(userRespVO); return scoreOrderRespVO; } @Override public List getOrderList(Collection ids) { return orderMapper.selectBatchIds(ids); } @Override public PageResult getOrderPage(ScoreOrderPageReqVO pageReqVO) { PageResult pageResult = orderMapper.selectPage(pageReqVO); PageResult scoreOrderRespVOPageResult = ScoreOrderConvert.INSTANCE.convertPage(pageResult); for (ScoreOrderRespVO scoreOrderRespVO : scoreOrderRespVOPageResult.getList()) { ScoreProductDO scoreProductDO = scoreProductMapper.selectById(scoreOrderRespVO.getProductId()); scoreOrderRespVO.setScoreProductRespVO(ScoreProductConvert.INSTANCE.convert(scoreProductDO)); MemberUserDO memberUserDO = memberUserMapper.selectById(scoreOrderRespVO.getUid()); UserRespVO userRespVO = UserConvert.INSTANCE.convert4(memberUserDO); scoreOrderRespVO.setUserRespVO(userRespVO); } return scoreOrderRespVOPageResult; } @Override public List getOrderList(ScoreOrderExportReqVO exportReqVO) { return orderMapper.selectList(exportReqVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/service/scoreproduct/AppScoreProductService.java ================================================ package co.yixiang.yshop.module.score.service.scoreproduct; import co.yixiang.yshop.module.score.controller.app.product.vo.AppScoreProductVO; import co.yixiang.yshop.module.score.dal.dataobject.scoreproduct.ScoreProductDO; import com.baomidou.mybatisplus.extension.service.IService; import java.util.List; /** * 积分产品 Service 接口 * * @author yshop */ public interface AppScoreProductService extends IService { /** * 列表 * @param page page * @param limit limit * @return list */ List getList(int page, int limit); /** * 详情 * @param id 产品ID * @return list */ AppScoreProductVO getDetail(Long id); } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/service/scoreproduct/AppScoreProductServiceImpl.java ================================================ package co.yixiang.yshop.module.score.service.scoreproduct; import co.yixiang.yshop.module.score.controller.app.product.vo.AppScoreProductVO; import co.yixiang.yshop.module.score.convert.scoreproduct.ScoreProductConvert; import co.yixiang.yshop.module.score.dal.dataobject.scoreproduct.ScoreProductDO; import co.yixiang.yshop.module.score.dal.mysql.scoreproduct.ScoreProductMapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.List; /** * 积分产品 Service 实现类 * * @author yshop */ @Service @Validated public class AppScoreProductServiceImpl extends ServiceImpl implements AppScoreProductService { @Resource private ScoreProductMapper productMapper; /** * 订单列表 * @param page page * @param limit limit * @return list */ @Override public List getList(int page, int limit) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.orderByDesc(ScoreProductDO::getWeigh); Page pageModel = new Page<>(page, limit); IPage pageList = productMapper.selectPage(pageModel, wrapper); List list = ScoreProductConvert.INSTANCE.convertList01(pageList.getRecords()); return list; } /** * 详情 * @param id 产品ID * @return list */ @Override public AppScoreProductVO getDetail(Long id) { ScoreProductDO scoreProductDO = this.getById(id); return ScoreProductConvert.INSTANCE.convert01(scoreProductDO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/service/scoreproduct/ScoreProductService.java ================================================ package co.yixiang.yshop.module.score.service.scoreproduct; import java.util.*; import jakarta.validation.*; import co.yixiang.yshop.module.score.controller.admin.scoreproduct.vo.*; import co.yixiang.yshop.module.score.dal.dataobject.scoreproduct.ScoreProductDO; import co.yixiang.yshop.framework.common.pojo.PageResult; /** * 积分产品 Service 接口 * * @author yshop */ public interface ScoreProductService { /** * 创建积分产品 * * @param createReqVO 创建信息 * @return 编号 */ Long createProduct(@Valid ScoreProductCreateReqVO createReqVO); /** * 更新积分产品 * * @param updateReqVO 更新信息 */ void updateProduct(@Valid ScoreProductUpdateReqVO updateReqVO); /** * 删除积分产品 * * @param id 编号 */ void deleteProduct(Long id); /** * 获得积分产品 * * @param id 编号 * @return 积分产品 */ ScoreProductDO getProduct(Long id); /** * 获得积分产品列表 * * @param ids 编号 * @return 积分产品列表 */ List getProductList(Collection ids); /** * 获得积分产品分页 * * @param pageReqVO 分页查询 * @return 积分产品分页 */ PageResult getProductPage(ScoreProductPageReqVO pageReqVO); /** * 获得积分产品列表, 用于 Excel 导出 * * @param exportReqVO 查询条件 * @return 积分产品列表 */ List getProductList(ScoreProductExportReqVO exportReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/java/co/yixiang/yshop/module/score/service/scoreproduct/ScoreProductServiceImpl.java ================================================ package co.yixiang.yshop.module.score.service.scoreproduct; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import java.util.*; import co.yixiang.yshop.module.score.controller.admin.scoreproduct.vo.*; import co.yixiang.yshop.module.score.dal.dataobject.scoreproduct.ScoreProductDO; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.score.convert.scoreproduct.ScoreProductConvert; import co.yixiang.yshop.module.score.dal.mysql.scoreproduct.ScoreProductMapper; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.score.enums.ErrorCodeConstants.*; /** * 积分产品 Service 实现类 * * @author yshop */ @Service @Validated public class ScoreProductServiceImpl implements ScoreProductService { @Resource private ScoreProductMapper productMapper; @Override public Long createProduct(ScoreProductCreateReqVO createReqVO) { // 插入 ScoreProductDO product = ScoreProductConvert.INSTANCE.convert(createReqVO); productMapper.insert(product); // 返回 return product.getId(); } @Override public void updateProduct(ScoreProductUpdateReqVO updateReqVO) { // 校验存在 validateProductExists(updateReqVO.getId()); // 更新 ScoreProductDO updateObj = ScoreProductConvert.INSTANCE.convert(updateReqVO); productMapper.updateById(updateObj); } @Override public void deleteProduct(Long id) { // 校验存在 validateProductExists(id); // 删除 productMapper.deleteById(id); } private void validateProductExists(Long id) { if (productMapper.selectById(id) == null) { throw exception(PRODUCT_NOT_EXISTS); } } @Override public ScoreProductDO getProduct(Long id) { return productMapper.selectById(id); } @Override public List getProductList(Collection ids) { return productMapper.selectBatchIds(ids); } @Override public PageResult getProductPage(ScoreProductPageReqVO pageReqVO) { return productMapper.selectPage(pageReqVO); } @Override public List getProductList(ScoreProductExportReqVO exportReqVO) { return productMapper.selectList(exportReqVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-score/yshop-module-score-biz/src/main/resources/mapper/scoreorder/ScoreOrderMapper.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-system/pom.xml ================================================ co.yixiang.boot yshop ${revision} 4.0.0 yshop-module-system-api yshop-module-system-biz yshop-module-system pom ${project.artifactId} system 模块下,我们放通用业务,支撑上层的核心业务。 例如说:用户、部门、权限、数据字典等等 ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/pom.xml ================================================ co.yixiang.boot yshop-module-system ${revision} 4.0.0 yshop-module-system-api jar ${project.artifactId} system 模块 API,暴露给其它模块调用 co.yixiang.boot yshop-common org.springframework.boot spring-boot-starter-validation true ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/dept/DeptApi.java ================================================ package co.yixiang.yshop.module.system.api.dept; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.module.system.api.dept.dto.DeptRespDTO; import java.util.Collection; import java.util.List; import java.util.Map; /** * 部门 API 接口 * * @author yshop */ public interface DeptApi { /** * 获得部门信息 * * @param id 部门编号 * @return 部门信息 */ DeptRespDTO getDept(Long id); /** * 获得部门信息数组 * * @param ids 部门编号数组 * @return 部门信息数组 */ List getDeptList(Collection ids); /** * 校验部门们是否有效。如下情况,视为无效: * 1. 部门编号不存在 * 2. 部门被禁用 * * @param ids 角色编号数组 */ void validateDeptList(Collection ids); /** * 获得指定编号的部门 Map * * @param ids 部门编号数组 * @return 部门 Map */ default Map getDeptMap(Collection ids) { List list = getDeptList(ids); return CollectionUtils.convertMap(list, DeptRespDTO::getId); } /** * 获得指定部门的所有子部门 * * @param id 部门编号 * @return 子部门列表 */ List getChildDeptList(Long id); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/dept/PostApi.java ================================================ package co.yixiang.yshop.module.system.api.dept; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.map.MapUtil; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.module.system.api.dept.dto.PostRespDTO; import java.util.Collection; import java.util.List; import java.util.Map; /** * 岗位 API 接口 * * @author yshop */ public interface PostApi { /** * 校验岗位们是否有效。如下情况,视为无效: * 1. 岗位编号不存在 * 2. 岗位被禁用 * * @param ids 岗位编号数组 */ void validPostList(Collection ids); List getPostList(Collection ids); default Map getPostMap(Collection ids) { if (CollUtil.isEmpty(ids)) { return MapUtil.empty(); } List list = getPostList(ids); return CollectionUtils.convertMap(list, PostRespDTO::getId); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/dept/dto/DeptRespDTO.java ================================================ package co.yixiang.yshop.module.system.api.dept.dto; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import lombok.Data; /** * 部门 Response DTO * * @author yshop */ @Data public class DeptRespDTO { /** * 部门编号 */ private Long id; /** * 部门名称 */ private String name; /** * 父部门编号 */ private Long parentId; /** * 负责人的用户编号 */ private Long leaderUserId; /** * 部门状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/dept/dto/PostRespDTO.java ================================================ package co.yixiang.yshop.module.system.api.dept.dto; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import lombok.Data; /** * 岗位 Response DTO * * @author yshop */ @Data public class PostRespDTO { /** * 岗位序号 */ private Long id; /** * 岗位名称 */ private String name; /** * 岗位编码 */ private String code; /** * 岗位排序 */ private Integer sort; /** * 状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/dict/DictDataApi.java ================================================ package co.yixiang.yshop.module.system.api.dict; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.module.system.api.dict.dto.DictDataRespDTO; import java.util.Collection; import java.util.List; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertList; /** * 字典数据 API 接口 * * @author yshop */ public interface DictDataApi { /** * 校验字典数据们是否有效。如下情况,视为无效: * 1. 字典数据不存在 * 2. 字典数据被禁用 * * @param dictType 字典类型 * @param values 字典数据值的数组 */ void validateDictDataList(String dictType, Collection values); /** * 获得指定的字典数据,从缓存中 * * @param type 字典类型 * @param value 字典数据值 * @return 字典数据 */ DictDataRespDTO getDictData(String type, String value); /** * 获得指定的字典标签,从缓存中 * * @param type 字典类型 * @param value 字典数据值 * @return 字典标签 */ default String getDictDataLabel(String type, Integer value) { DictDataRespDTO dictData = getDictData(type, String.valueOf(value)); if (ObjUtil.isNull(dictData)) { return StrUtil.EMPTY; } return dictData.getLabel(); } /** * 解析获得指定的字典数据,从缓存中 * * @param type 字典类型 * @param label 字典数据标签 * @return 字典数据 */ DictDataRespDTO parseDictData(String type, String label); /** * 获得指定字典类型的字典数据列表 * * @param dictType 字典类型 * @return 字典数据列表 */ List getDictDataList(String dictType); /** * 获得字典数据标签列表 * * @param dictType 字典类型 * @return 字典数据标签列表 */ default List getDictDataLabelList(String dictType) { List list = getDictDataList(dictType); return convertList(list, DictDataRespDTO::getLabel); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/dict/dto/DictDataRespDTO.java ================================================ package co.yixiang.yshop.module.system.api.dict.dto; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import lombok.Data; /** * 字典数据 Response DTO * * @author yshop */ @Data public class DictDataRespDTO { /** * 字典标签 */ private String label; /** * 字典值 */ private String value; /** * 字典类型 */ private String dictType; /** * 状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/logger/LoginLogApi.java ================================================ package co.yixiang.yshop.module.system.api.logger; import co.yixiang.yshop.module.system.api.logger.dto.LoginLogCreateReqDTO; import jakarta.validation.Valid; /** * 登录日志的 API 接口 * * @author yshop */ public interface LoginLogApi { /** * 创建登录日志 * * @param reqDTO 日志信息 */ void createLoginLog(@Valid LoginLogCreateReqDTO reqDTO); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/logger/OperateLogApi.java ================================================ package co.yixiang.yshop.module.system.api.logger; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.api.logger.dto.OperateLogCreateReqDTO; import co.yixiang.yshop.module.system.api.logger.dto.OperateLogPageReqDTO; import co.yixiang.yshop.module.system.api.logger.dto.OperateLogRespDTO; import jakarta.validation.Valid; /** * 操作日志 API 接口 * * @author yshop */ public interface OperateLogApi { /** * 创建操作日志 * * @param createReqDTO 请求 */ void createOperateLog(@Valid OperateLogCreateReqDTO createReqDTO); /** * 获取指定模块的指定数据的操作日志分页 * * @param pageReqVO 请求 * @return 操作日志分页 */ PageResult getOperateLogPage(OperateLogPageReqDTO pageReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/logger/dto/LoginLogCreateReqDTO.java ================================================ package co.yixiang.yshop.module.system.api.logger.dto; import lombok.Data; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; /** * 登录日志创建 Request DTO * * @author yshop */ @Data public class LoginLogCreateReqDTO { /** * 日志类型 */ @NotNull(message = "日志类型不能为空") private Integer logType; /** * 链路追踪编号 */ private String traceId; /** * 用户编号 */ private Long userId; /** * 用户类型 */ @NotNull(message = "用户类型不能为空") private Integer userType; /** * 用户账号 * * 不再强制校验 username 非空,因为 Member 社交登录时,此时暂时没有 username(mobile)! */ private String username; /** * 登录结果 */ @NotNull(message = "登录结果不能为空") private Integer result; /** * 用户 IP */ @NotEmpty(message = "用户 IP 不能为空") private String userIp; /** * 浏览器 UserAgent * * 允许空,原因:Job 过期登出时,是无法传递 UserAgent 的 */ private String userAgent; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/logger/dto/OperateLogCreateReqDTO.java ================================================ package co.yixiang.yshop.module.system.api.logger.dto; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; /** * 系统操作日志 Create Request DTO * * @author HUIHUI */ @Data public class OperateLogCreateReqDTO { /** * 链路追踪编号 * * 一般来说,通过链路追踪编号,可以将访问日志,错误日志,链路追踪日志,logger 打印日志等,结合在一起,从而进行排错。 */ private String traceId; /** * 用户编号 * * 关联 MemberUserDO 的 id 属性,或者 AdminUserDO 的 id 属性 */ @NotNull(message = "用户编号不能为空") private Long userId; /** * 用户类型 * * 关联 {@link UserTypeEnum} */ @NotNull(message = "用户类型不能为空") private Integer userType; /** * 操作模块类型 */ @NotEmpty(message = "操作模块类型不能为空") private String type; /** * 操作名 */ @NotEmpty(message = "操作名不能为空") private String subType; /** * 操作模块业务编号 */ @NotNull(message = "操作模块业务编号不能为空") private Long bizId; /** * 操作内容,记录整个操作的明细 * 例如说,修改编号为 1 的用户信息,将性别从男改成女,将姓名从yshop改成源码。 */ @NotEmpty(message = "操作内容不能为空") private String action; /** * 拓展字段,有些复杂的业务,需要记录一些字段 ( JSON 格式 ) * 例如说,记录订单编号,{ orderId: "1"} */ private String extra; /** * 请求方法名 */ @NotEmpty(message = "请求方法名不能为空") private String requestMethod; /** * 请求地址 */ @NotEmpty(message = "请求地址不能为空") private String requestUrl; /** * 用户 IP */ @NotEmpty(message = "用户 IP 不能为空") private String userIp; /** * 浏览器 UA */ @NotEmpty(message = "浏览器 UA 不能为空") private String userAgent; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/logger/dto/OperateLogPageReqDTO.java ================================================ package co.yixiang.yshop.module.system.api.logger.dto; import co.yixiang.yshop.framework.common.pojo.PageParam; import lombok.Data; /** * 操作日志分页 Request DTO * * @author HUIHUI */ @Data public class OperateLogPageReqDTO extends PageParam { /** * 模块类型 */ private String type; /** * 模块数据编号 */ private Long bizId; /** * 用户编号 */ private Long userId; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/logger/dto/OperateLogRespDTO.java ================================================ package co.yixiang.yshop.module.system.api.logger.dto; import com.fhs.core.trans.anno.Trans; import com.fhs.core.trans.constant.TransType; import com.fhs.core.trans.vo.VO; import lombok.Data; import java.time.LocalDateTime; /** * 系统操作日志 Resp DTO * * @author HUIHUI */ @Data public class OperateLogRespDTO implements VO { /** * 日志编号 */ private Long id; /** * 链路追踪编号 */ private String traceId; /** * 用户编号 */ @Trans(type = TransType.RPC, targetClassName = "co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO", fields = "nickname", ref = "userName") private Long userId; /** * 用户名称 */ private String userName; /** * 用户类型 */ private Integer userType; /** * 操作模块类型 */ private String type; /** * 操作名 */ private String subType; /** * 操作模块业务编号 */ private Long bizId; /** * 操作内容 */ private String action; /** * 拓展字段 */ private String extra; /** * 请求方法名 */ private String requestMethod; /** * 请求地址 */ private String requestUrl; /** * 用户 IP */ private String userIp; /** * 浏览器 UA */ private String userAgent; /** * 创建时间 */ private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/mail/MailSendApi.java ================================================ package co.yixiang.yshop.module.system.api.mail; import co.yixiang.yshop.module.system.api.mail.dto.MailSendSingleToUserReqDTO; import jakarta.validation.Valid; /** * 邮箱发送 API 接口 * * @author yshop */ public interface MailSendApi { /** * 发送单条邮箱给 Admin 用户 * * 在 mail 为空时,使用 userId 加载对应 Admin 的邮箱 * * @param reqDTO 发送请求 * @return 发送日志编号 */ Long sendSingleMailToAdmin(@Valid MailSendSingleToUserReqDTO reqDTO); /** * 发送单条邮箱给 Member 用户 * * 在 mail 为空时,使用 userId 加载对应 Member 的邮箱 * * @param reqDTO 发送请求 * @return 发送日志编号 */ Long sendSingleMailToMember(@Valid MailSendSingleToUserReqDTO reqDTO); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/mail/dto/MailSendSingleToUserReqDTO.java ================================================ package co.yixiang.yshop.module.system.api.mail.dto; import lombok.Data; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotNull; import java.util.Map; /** * 邮件发送 Request DTO * * @author wangjingqi */ @Data public class MailSendSingleToUserReqDTO { /** * 用户编号 */ private Long userId; /** * 邮箱 */ @Email private String mail; /** * 邮件模板编号 */ @NotNull(message = "邮件模板编号不能为空") private String templateCode; /** * 邮件模板参数 */ private Map templateParams; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/notify/NotifyMessageSendApi.java ================================================ package co.yixiang.yshop.module.system.api.notify; import co.yixiang.yshop.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; import jakarta.validation.Valid; /** * 站内信发送 API 接口 * * @author xrcoder */ public interface NotifyMessageSendApi { /** * 发送单条站内信给 Admin 用户 * * @param reqDTO 发送请求 * @return 发送消息 ID */ Long sendSingleMessageToAdmin(@Valid NotifySendSingleToUserReqDTO reqDTO); /** * 发送单条站内信给 Member 用户 * * @param reqDTO 发送请求 * @return 发送消息 ID */ Long sendSingleMessageToMember(@Valid NotifySendSingleToUserReqDTO reqDTO); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/notify/dto/NotifySendSingleToUserReqDTO.java ================================================ package co.yixiang.yshop.module.system.api.notify.dto; import lombok.Data; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.util.Map; /** * 站内信发送给 Admin 或者 Member 用户 * * @author xrcoder */ @Data public class NotifySendSingleToUserReqDTO { /** * 用户编号 */ @NotNull(message = "用户编号不能为空") private Long userId; /** * 站内信模板编号 */ @NotEmpty(message = "站内信模板编号不能为空") private String templateCode; /** * 站内信模板参数 */ private Map templateParams; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/notify/dto/NotifyTemplateReqDTO.java ================================================ package co.yixiang.yshop.module.system.api.notify.dto; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.validation.InEnum; import lombok.Data; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @Data public class NotifyTemplateReqDTO { @NotEmpty(message = "模版名称不能为空") private String name; @NotNull(message = "模版编码不能为空") private String code; @NotNull(message = "模版类型不能为空") private Integer type; @NotEmpty(message = "发送人名称不能为空") private String nickname; @NotEmpty(message = "模版内容不能为空") private String content; @NotNull(message = "状态不能为空") @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}") private Integer status; private String remark; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/oauth2/OAuth2TokenApi.java ================================================ package co.yixiang.yshop.module.system.api.oauth2; import co.yixiang.yshop.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO; import co.yixiang.yshop.module.system.api.oauth2.dto.OAuth2AccessTokenCreateReqDTO; import co.yixiang.yshop.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO; import jakarta.validation.Valid; /** * OAuth2.0 Token API 接口 * * @author yshop */ public interface OAuth2TokenApi { /** * 创建访问令牌 * * @param reqDTO 访问令牌的创建信息 * @return 访问令牌的信息 */ OAuth2AccessTokenRespDTO createAccessToken(@Valid OAuth2AccessTokenCreateReqDTO reqDTO); /** * 校验访问令牌 * * @param accessToken 访问令牌 * @return 访问令牌的信息 */ OAuth2AccessTokenCheckRespDTO checkAccessToken(String accessToken); /** * 移除访问令牌 * * @param accessToken 访问令牌 * @return 访问令牌的信息 */ OAuth2AccessTokenRespDTO removeAccessToken(String accessToken); /** * 刷新访问令牌 * * @param refreshToken 刷新令牌 * @param clientId 客户端编号 * @return 访问令牌的信息 */ OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, String clientId); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/oauth2/dto/OAuth2AccessTokenCheckRespDTO.java ================================================ package co.yixiang.yshop.module.system.api.oauth2.dto; import lombok.Data; import java.io.Serializable; import java.util.List; import java.util.Map; /** * OAuth2.0 访问令牌的校验 Response DTO * * @author yshop */ @Data public class OAuth2AccessTokenCheckRespDTO implements Serializable { /** * 用户编号 */ private Long userId; /** * 用户类型 */ private Integer userType; /** * 用户信息 */ private Map userInfo; /** * 租户编号 */ private Long tenantId; /** * 授权范围的数组 */ private List scopes; private Long shopId; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/oauth2/dto/OAuth2AccessTokenCreateReqDTO.java ================================================ package co.yixiang.yshop.module.system.api.oauth2.dto; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.validation.InEnum; import lombok.Data; import jakarta.validation.constraints.NotNull; import java.io.Serializable; import java.util.List; /** * OAuth2.0 访问令牌创建 Request DTO * * @author yshop */ @Data public class OAuth2AccessTokenCreateReqDTO implements Serializable { /** * 用户编号 */ @NotNull(message = "用户编号不能为空") private Long userId; /** * 用户类型 */ @NotNull(message = "用户类型不能为空") @InEnum(value = UserTypeEnum.class, message = "用户类型必须是 {value}") private Integer userType; /** * 客户端编号 */ @NotNull(message = "客户端编号不能为空") private String clientId; /** * 授权范围 */ private List scopes; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/oauth2/dto/OAuth2AccessTokenRespDTO.java ================================================ package co.yixiang.yshop.module.system.api.oauth2.dto; import lombok.Data; import lombok.experimental.Accessors; import java.io.Serializable; import java.time.LocalDateTime; /** * OAuth2.0 访问令牌的信息 Response DTO * * @author yshop */ @Data @Accessors(chain = true) public class OAuth2AccessTokenRespDTO implements Serializable { /** * 访问令牌 */ private String accessToken; /** * 刷新令牌 */ private String refreshToken; /** * 用户编号 */ private Long userId; /** * 用户类型 */ private Integer userType; /** * 过期时间 */ private LocalDateTime expiresTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/permission/PermissionApi.java ================================================ package co.yixiang.yshop.module.system.api.permission; import co.yixiang.yshop.module.system.api.permission.dto.DeptDataPermissionRespDTO; import java.util.Collection; import java.util.Set; /** * 权限 API 接口 * * @author yshop */ public interface PermissionApi { /** * 获得拥有多个角色的用户编号集合 * * @param roleIds 角色编号集合 * @return 用户编号集合 */ Set getUserRoleIdListByRoleIds(Collection roleIds); /** * 判断是否有权限,任一一个即可 * * @param userId 用户编号 * @param permissions 权限 * @return 是否 */ boolean hasAnyPermissions(Long userId, String... permissions); /** * 判断是否有角色,任一一个即可 * * @param userId 用户编号 * @param roles 角色数组 * @return 是否 */ boolean hasAnyRoles(Long userId, String... roles); /** * 获得登陆用户的部门数据权限 * * @param userId 用户编号 * @return 部门数据权限 */ DeptDataPermissionRespDTO getDeptDataPermission(Long userId); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/permission/RoleApi.java ================================================ package co.yixiang.yshop.module.system.api.permission; import java.util.Collection; /** * 角色 API 接口 * * @author yshop */ public interface RoleApi { /** * 校验角色们是否有效。如下情况,视为无效: * 1. 角色编号不存在 * 2. 角色被禁用 * * @param ids 角色编号数组 */ void validRoleList(Collection ids); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/permission/dto/DeptDataPermissionRespDTO.java ================================================ package co.yixiang.yshop.module.system.api.permission.dto; import lombok.Data; import java.util.HashSet; import java.util.Set; /** * 部门的数据权限 Response DTO * * @author yshop */ @Data public class DeptDataPermissionRespDTO { /** * 是否可查看全部数据 */ private Boolean all; /** * 是否可查看自己的数据 */ private Boolean self; /** * 可查看的部门编号数组 */ private Set deptIds; public DeptDataPermissionRespDTO() { this.all = false; this.self = false; this.deptIds = new HashSet<>(); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/sms/SmsCodeApi.java ================================================ package co.yixiang.yshop.module.system.api.sms; import co.yixiang.yshop.framework.common.exception.ServiceException; import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeValidateReqDTO; import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeSendReqDTO; import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeUseReqDTO; import jakarta.validation.Valid; /** * 短信验证码 API 接口 * * @author yshop */ public interface SmsCodeApi { /** * 创建短信验证码,并进行发送 * * @param reqDTO 发送请求 */ void sendSmsCode(@Valid SmsCodeSendReqDTO reqDTO); /** * 验证短信验证码,并进行使用 * 如果正确,则将验证码标记成已使用 * 如果错误,则抛出 {@link ServiceException} 异常 * * @param reqDTO 使用请求 */ void useSmsCode(@Valid SmsCodeUseReqDTO reqDTO); /** * 检查验证码是否有效 * * @param reqDTO 校验请求 */ void validateSmsCode(@Valid SmsCodeValidateReqDTO reqDTO); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/sms/SmsSendApi.java ================================================ package co.yixiang.yshop.module.system.api.sms; import co.yixiang.yshop.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO; import jakarta.validation.Valid; /** * 短信发送 API 接口 * * @author yshop */ public interface SmsSendApi { /** * 发送单条短信给 Admin 用户 * * 在 mobile 为空时,使用 userId 加载对应 Admin 的手机号 * * @param reqDTO 发送请求 * @return 发送日志编号 */ Long sendSingleSmsToAdmin(@Valid SmsSendSingleToUserReqDTO reqDTO); /** * 发送单条短信给 Member 用户 * * 在 mobile 为空时,使用 userId 加载对应 Member 的手机号 * * @param reqDTO 发送请求 * @return 发送日志编号 */ Long sendSingleSmsToMember(@Valid SmsSendSingleToUserReqDTO reqDTO); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/sms/dto/code/SmsCodeSendReqDTO.java ================================================ package co.yixiang.yshop.module.system.api.sms.dto.code; import co.yixiang.yshop.framework.common.validation.InEnum; import co.yixiang.yshop.framework.common.validation.Mobile; import co.yixiang.yshop.module.system.enums.sms.SmsSceneEnum; import lombok.Data; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; /** * 短信验证码的发送 Request DTO * * @author yshop */ @Data public class SmsCodeSendReqDTO { /** * 手机号 */ @Mobile @NotEmpty(message = "手机号不能为空") private String mobile; /** * 发送场景 */ @NotNull(message = "发送场景不能为空") @InEnum(SmsSceneEnum.class) private Integer scene; /** * 发送 IP */ @NotEmpty(message = "发送 IP 不能为空") private String createIp; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/sms/dto/code/SmsCodeUseReqDTO.java ================================================ package co.yixiang.yshop.module.system.api.sms.dto.code; import co.yixiang.yshop.framework.common.validation.InEnum; import co.yixiang.yshop.framework.common.validation.Mobile; import co.yixiang.yshop.module.system.enums.sms.SmsSceneEnum; import lombok.Data; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; /** * 短信验证码的使用 Request DTO * * @author yshop */ @Data public class SmsCodeUseReqDTO { /** * 手机号 */ @Mobile @NotEmpty(message = "手机号不能为空") private String mobile; /** * 发送场景 */ @NotNull(message = "发送场景不能为空") @InEnum(SmsSceneEnum.class) private Integer scene; /** * 验证码 */ @NotEmpty(message = "验证码") private String code; /** * 使用 IP */ @NotEmpty(message = "使用 IP 不能为空") private String usedIp; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/sms/dto/code/SmsCodeValidateReqDTO.java ================================================ package co.yixiang.yshop.module.system.api.sms.dto.code; import co.yixiang.yshop.framework.common.validation.InEnum; import co.yixiang.yshop.framework.common.validation.Mobile; import co.yixiang.yshop.module.system.enums.sms.SmsSceneEnum; import lombok.Data; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; /** * 短信验证码的校验 Request DTO * * @author yshop */ @Data public class SmsCodeValidateReqDTO { /** * 手机号 */ @Mobile @NotEmpty(message = "手机号不能为空") private String mobile; /** * 发送场景 */ @NotNull(message = "发送场景不能为空") @InEnum(SmsSceneEnum.class) private Integer scene; /** * 验证码 */ @NotEmpty(message = "验证码") private String code; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/sms/dto/send/SmsSendSingleToUserReqDTO.java ================================================ package co.yixiang.yshop.module.system.api.sms.dto.send; import co.yixiang.yshop.framework.common.validation.Mobile; import lombok.Data; import jakarta.validation.constraints.NotEmpty; import java.util.Map; /** * 短信发送给 Admin 或者 Member 用户 * * @author yshop */ @Data public class SmsSendSingleToUserReqDTO { /** * 用户编号 */ private Long userId; /** * 手机号 */ @Mobile private String mobile; /** * 短信模板编号 */ @NotEmpty(message = "短信模板编号不能为空") private String templateCode; /** * 短信模板参数 */ private Map templateParams; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/social/SocialClientApi.java ================================================ package co.yixiang.yshop.module.system.api.social; import co.yixiang.yshop.module.system.api.social.dto.SocialWxJsapiSignatureRespDTO; import co.yixiang.yshop.module.system.api.social.dto.SocialWxPhoneNumberInfoRespDTO; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; /** * 社交应用的 API 接口 * * @author yshop */ public interface SocialClientApi { /** * 获得社交平台的授权 URL * * @param socialType 社交平台的类型 {@link SocialTypeEnum} * @param userType 用户类型 * @param redirectUri 重定向 URL * @return 社交平台的授权 URL */ String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri); /** * 创建微信公众号 JS SDK 初始化所需的签名 * * @param userType 用户类型 * @param url 访问的 URL 地址 * @return 签名 */ SocialWxJsapiSignatureRespDTO createWxMpJsapiSignature(Integer userType, String url); /** * 获得微信小程序的手机信息 * * @param userType 用户类型 * @param phoneCode 手机授权码 * @return 手机信息 */ SocialWxPhoneNumberInfoRespDTO getWxMaPhoneNumberInfo(Integer userType, String phoneCode); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/social/SocialUserApi.java ================================================ package co.yixiang.yshop.module.system.api.social; import co.yixiang.yshop.framework.common.exception.ServiceException; import co.yixiang.yshop.module.system.api.social.dto.SocialUserBindReqDTO; import co.yixiang.yshop.module.system.api.social.dto.SocialUserRespDTO; import co.yixiang.yshop.module.system.api.social.dto.SocialUserUnbindReqDTO; import jakarta.validation.Valid; /** * 社交用户的 API 接口 * * @author yshop */ public interface SocialUserApi { /** * 绑定社交用户 * * @param reqDTO 绑定信息 * @return 社交用户 openid */ String bindSocialUser(@Valid SocialUserBindReqDTO reqDTO); /** * 取消绑定社交用户 * * @param reqDTO 解绑 */ void unbindSocialUser(@Valid SocialUserUnbindReqDTO reqDTO); /** * 获得社交用户,基于 userId * * @param userType 用户类型 * @param userId 用户编号 * @param socialType 社交平台的类型 * @return 社交用户 */ SocialUserRespDTO getSocialUserByUserId(Integer userType, Long userId, Integer socialType); /** * 获得社交用户 * * 在认证信息不正确的情况下,也会抛出 {@link ServiceException} 业务异常 * * @param userType 用户类型 * @param socialType 社交平台的类型 * @param code 授权码 * @param state state * @return 社交用户 */ SocialUserRespDTO getSocialUserByCode(Integer userType, Integer socialType, String code, String state); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/social/dto/SocialUserBindReqDTO.java ================================================ package co.yixiang.yshop.module.system.api.social.dto; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.validation.InEnum; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; /** * 取消绑定社交用户 Request DTO * * @author yshop */ @Data @NoArgsConstructor @AllArgsConstructor public class SocialUserBindReqDTO { /** * 用户编号 */ @NotNull(message = "用户编号不能为空") private Long userId; /** * 用户类型 */ @InEnum(UserTypeEnum.class) @NotNull(message = "用户类型不能为空") private Integer userType; /** * 社交平台的类型 */ @InEnum(SocialTypeEnum.class) @NotNull(message = "社交平台的类型不能为空") private Integer socialType; /** * 授权码 */ @NotEmpty(message = "授权码不能为空") private String code; /** * state */ @NotNull(message = "state 不能为空") private String state; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/social/dto/SocialUserRespDTO.java ================================================ package co.yixiang.yshop.module.system.api.social.dto; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * 社交用户 Response DTO * * @author yshop */ @Data @NoArgsConstructor @AllArgsConstructor public class SocialUserRespDTO { /** * 社交用户的 openid */ private String openid; /** * 社交用户的昵称 */ private String nickname; /** * 社交用户的头像 */ private String avatar; /** * 关联的用户编号 */ private Long userId; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/social/dto/SocialUserUnbindReqDTO.java ================================================ package co.yixiang.yshop.module.system.api.social.dto; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.validation.InEnum; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import lombok.AllArgsConstructor; import lombok.Data; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.NoArgsConstructor; /** * 社交绑定 Request DTO,使用 code 授权码 * * @author yshop */ @Data @AllArgsConstructor @NoArgsConstructor public class SocialUserUnbindReqDTO { /** * 用户编号 */ @NotNull(message = "用户编号不能为空") private Long userId; /** * 用户类型 */ @InEnum(UserTypeEnum.class) @NotNull(message = "用户类型不能为空") private Integer userType; /** * 社交平台的类型 */ @InEnum(SocialTypeEnum.class) @NotNull(message = "社交平台的类型不能为空") private Integer socialType; /** * 社交平台的 openid */ @NotEmpty(message = "社交平台的 openid 不能为空") private String openid; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/social/dto/SocialWxJsapiSignatureRespDTO.java ================================================ package co.yixiang.yshop.module.system.api.social.dto; import lombok.Data; /** * 微信公众号 JSAPI 签名 Response DTO * * @author yshop */ @Data public class SocialWxJsapiSignatureRespDTO { /** * 微信公众号的 appId */ private String appId; /** * 匿名串 */ private String nonceStr; /** * 时间戳 */ private Long timestamp; /** * URL */ private String url; /** * 签名 */ private String signature; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/social/dto/SocialWxPhoneNumberInfoRespDTO.java ================================================ package co.yixiang.yshop.module.system.api.social.dto; import lombok.Data; /** * 微信小程序的手机信息 Response DTO * * @author yshop */ @Data public class SocialWxPhoneNumberInfoRespDTO { /** * 用户绑定的手机号(国外手机号会有区号) */ private String phoneNumber; /** * 没有区号的手机号 */ private String purePhoneNumber; /** * 区号 */ private String countryCode; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/tenant/TenantApi.java ================================================ package co.yixiang.yshop.module.system.api.tenant; import java.util.List; /** * 多租户的 API 接口 * * @author yshop */ public interface TenantApi { /** * 获得所有租户 * * @return 租户编号数组 */ List getTenantIdList(); /** * 校验租户是否合法 * * @param id 租户编号 */ void validateTenant(Long id); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/user/AdminUserApi.java ================================================ package co.yixiang.yshop.module.system.api.user; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.module.system.api.user.dto.AdminUserRespDTO; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; /** * Admin 用户 API 接口 * * @author yshop */ public interface AdminUserApi { /** * 通过用户 ID 查询用户 * * @param id 用户ID * @return 用户对象信息 */ AdminUserRespDTO getUser(Long id); /** * 通过用户 ID 查询用户下属 * * @param id 用户编号 * @return 用户下属用户列表 */ List getUserListBySubordinate(Long id); /** * 通过用户 ID 查询用户们 * * @param ids 用户 ID 们 * @return 用户对象信息 */ List getUserList(Collection ids); /** * 获得指定部门的用户数组 * * @param deptIds 部门数组 * @return 用户数组 */ List getUserListByDeptIds(Collection deptIds); /** * 获得指定岗位的用户数组 * * @param postIds 岗位数组 * @return 用户数组 */ List getUserListByPostIds(Collection postIds); /** * 获得用户 Map * * @param ids 用户编号数组 * @return 用户 Map */ default Map getUserMap(Collection ids) { List users = getUserList(ids); return CollectionUtils.convertMap(users, AdminUserRespDTO::getId); } /** * 校验用户是否有效。如下情况,视为无效: * 1. 用户编号不存在 * 2. 用户被禁用 * * @param id 用户编号 */ default void validateUser(Long id) { validateUserList(Collections.singleton(id)); } /** * 校验用户们是否有效。如下情况,视为无效: * 1. 用户编号不存在 * 2. 用户被禁用 * * @param ids 用户编号数组 */ void validateUserList(Collection ids); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/api/user/dto/AdminUserRespDTO.java ================================================ package co.yixiang.yshop.module.system.api.user.dto; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import lombok.Data; import java.util.Set; /** * Admin 用户 Response DTO * * @author yshop */ @Data public class AdminUserRespDTO { /** * 用户ID */ private Long id; /** * 用户昵称 */ private String nickname; /** * 帐号状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; /** * 部门ID */ private Long deptId; /** * 岗位编号数组 */ private Set postIds; /** * 手机号码 */ private String mobile; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/enums/DictTypeConstants.java ================================================ package co.yixiang.yshop.module.system.enums; /** * System 字典类型的枚举类 * * @author yshop */ public interface DictTypeConstants { String USER_TYPE = "user_type"; // 用户类型 String COMMON_STATUS = "common_status"; // 系统状态 // ========== SYSTEM 模块 ========== String USER_SEX = "system_user_sex"; // 用户性别 String LOGIN_TYPE = "system_login_type"; // 登录日志的类型 String LOGIN_RESULT = "system_login_result"; // 登录结果 String ERROR_CODE_TYPE = "system_error_code_type"; // 错误码的类型枚举 String SMS_CHANNEL_CODE = "system_sms_channel_code"; // 短信渠道编码 String SMS_TEMPLATE_TYPE = "system_sms_template_type"; // 短信模板类型 String SMS_SEND_STATUS = "system_sms_send_status"; // 短信发送状态 String SMS_RECEIVE_STATUS = "system_sms_receive_status"; // 短信接收状态 } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/enums/ErrorCodeConstants.java ================================================ package co.yixiang.yshop.module.system.enums; import co.yixiang.yshop.framework.common.exception.ErrorCode; /** * System 错误码枚举类 * * system 系统,使用 1-002-000-000 段 */ public interface ErrorCodeConstants { // ========== AUTH 模块 1-002-000-000 ========== ErrorCode AUTH_LOGIN_BAD_CREDENTIALS = new ErrorCode(1_002_000_000, "登录失败,账号密码不正确"); ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1_002_000_001, "登录失败,账号被禁用"); ErrorCode AUTH_LOGIN_CAPTCHA_CODE_ERROR = new ErrorCode(1_002_000_004, "验证码不正确,原因:{}"); ErrorCode AUTH_THIRD_LOGIN_NOT_BIND = new ErrorCode(1_002_000_005, "未绑定账号,需要进行绑定"); ErrorCode AUTH_TOKEN_EXPIRED = new ErrorCode(1_002_000_006, "Token 已经过期"); ErrorCode AUTH_MOBILE_NOT_EXISTS = new ErrorCode(1_002_000_007, "手机号不存在"); // ========== 菜单模块 1-002-001-000 ========== ErrorCode MENU_NAME_DUPLICATE = new ErrorCode(1_002_001_000, "已经存在该名字的菜单"); ErrorCode MENU_PARENT_NOT_EXISTS = new ErrorCode(1_002_001_001, "父菜单不存在"); ErrorCode MENU_PARENT_ERROR = new ErrorCode(1_002_001_002, "不能设置自己为父菜单"); ErrorCode MENU_NOT_EXISTS = new ErrorCode(1_002_001_003, "菜单不存在"); ErrorCode MENU_EXISTS_CHILDREN = new ErrorCode(1_002_001_004, "存在子菜单,无法删除"); ErrorCode MENU_PARENT_NOT_DIR_OR_MENU = new ErrorCode(1_002_001_005, "父菜单的类型必须是目录或者菜单"); // ========== 角色模块 1-002-002-000 ========== ErrorCode ROLE_NOT_EXISTS = new ErrorCode(1_002_002_000, "角色不存在"); ErrorCode ROLE_NAME_DUPLICATE = new ErrorCode(1_002_002_001, "已经存在名为【{}】的角色"); ErrorCode ROLE_CODE_DUPLICATE = new ErrorCode(1_002_002_002, "已经存在编码为【{}】的角色"); ErrorCode ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE = new ErrorCode(1_002_002_003, "不能操作类型为系统内置的角色"); ErrorCode ROLE_IS_DISABLE = new ErrorCode(1_002_002_004, "名字为【{}】的角色已被禁用"); ErrorCode ROLE_ADMIN_CODE_ERROR = new ErrorCode(1_002_002_005, "编码【{}】不能使用"); // ========== 用户模块 1-002-003-000 ========== ErrorCode USER_USERNAME_EXISTS = new ErrorCode(1_002_003_000, "用户账号已经存在"); ErrorCode USER_MOBILE_EXISTS = new ErrorCode(1_002_003_001, "手机号已经存在"); ErrorCode USER_EMAIL_EXISTS = new ErrorCode(1_002_003_002, "邮箱已经存在"); ErrorCode USER_NOT_EXISTS = new ErrorCode(1_002_003_003, "用户不存在"); ErrorCode USER_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_002_003_004, "导入用户数据不能为空!"); ErrorCode USER_PASSWORD_FAILED = new ErrorCode(1_002_003_005, "用户密码校验失败"); ErrorCode USER_IS_DISABLE = new ErrorCode(1_002_003_006, "名字为【{}】的用户已被禁用"); ErrorCode USER_COUNT_MAX = new ErrorCode(1_002_003_008, "创建用户失败,原因:超过租户最大租户配额({})!"); // ========== 部门模块 1-002-004-000 ========== ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1_002_004_000, "已经存在该名字的部门"); ErrorCode DEPT_PARENT_NOT_EXITS = new ErrorCode(1_002_004_001,"父级部门不存在"); ErrorCode DEPT_NOT_FOUND = new ErrorCode(1_002_004_002, "当前部门不存在"); ErrorCode DEPT_EXITS_CHILDREN = new ErrorCode(1_002_004_003, "存在子部门,无法删除"); ErrorCode DEPT_PARENT_ERROR = new ErrorCode(1_002_004_004, "不能设置自己为父部门"); ErrorCode DEPT_EXISTS_USER = new ErrorCode(1_002_004_005, "部门中存在员工,无法删除"); ErrorCode DEPT_NOT_ENABLE = new ErrorCode(1_002_004_006, "部门({})不处于开启状态,不允许选择"); ErrorCode DEPT_PARENT_IS_CHILD = new ErrorCode(1_002_004_007, "不能设置自己的子部门为父部门"); // ========== 岗位模块 1-002-005-000 ========== ErrorCode POST_NOT_FOUND = new ErrorCode(1_002_005_000, "当前岗位不存在"); ErrorCode POST_NOT_ENABLE = new ErrorCode(1_002_005_001, "岗位({}) 不处于开启状态,不允许选择"); ErrorCode POST_NAME_DUPLICATE = new ErrorCode(1_002_005_002, "已经存在该名字的岗位"); ErrorCode POST_CODE_DUPLICATE = new ErrorCode(1_002_005_003, "已经存在该标识的岗位"); // ========== 字典类型 1-002-006-000 ========== ErrorCode DICT_TYPE_NOT_EXISTS = new ErrorCode(1_002_006_001, "当前字典类型不存在"); ErrorCode DICT_TYPE_NOT_ENABLE = new ErrorCode(1_002_006_002, "字典类型不处于开启状态,不允许选择"); ErrorCode DICT_TYPE_NAME_DUPLICATE = new ErrorCode(1_002_006_003, "已经存在该名字的字典类型"); ErrorCode DICT_TYPE_TYPE_DUPLICATE = new ErrorCode(1_002_006_004, "已经存在该类型的字典类型"); ErrorCode DICT_TYPE_HAS_CHILDREN = new ErrorCode(1_002_006_005, "无法删除,该字典类型还有字典数据"); // ========== 字典数据 1-002-007-000 ========== ErrorCode DICT_DATA_NOT_EXISTS = new ErrorCode(1_002_007_001, "当前字典数据不存在"); ErrorCode DICT_DATA_NOT_ENABLE = new ErrorCode(1_002_007_002, "字典数据({})不处于开启状态,不允许选择"); ErrorCode DICT_DATA_VALUE_DUPLICATE = new ErrorCode(1_002_007_003, "已经存在该值的字典数据"); // ========== 通知公告 1-002-008-000 ========== ErrorCode NOTICE_NOT_FOUND = new ErrorCode(1_002_008_001, "当前通知公告不存在"); // ========== 短信渠道 1-002-011-000 ========== ErrorCode SMS_CHANNEL_NOT_EXISTS = new ErrorCode(1_002_011_000, "短信渠道不存在"); ErrorCode SMS_CHANNEL_DISABLE = new ErrorCode(1_002_011_001, "短信渠道不处于开启状态,不允许选择"); ErrorCode SMS_CHANNEL_HAS_CHILDREN = new ErrorCode(1_002_011_002, "无法删除,该短信渠道还有短信模板"); // ========== 短信模板 1-002-012-000 ========== ErrorCode SMS_TEMPLATE_NOT_EXISTS = new ErrorCode(1_002_012_000, "短信模板不存在"); ErrorCode SMS_TEMPLATE_CODE_DUPLICATE = new ErrorCode(1_002_012_001, "已经存在编码为【{}】的短信模板"); ErrorCode SMS_TEMPLATE_API_ERROR = new ErrorCode(1_002_012_002, "短信 API 模板调用失败,原因是:{}"); ErrorCode SMS_TEMPLATE_API_AUDIT_CHECKING = new ErrorCode(1_002_012_003, "短信 API 模版无法使用,原因:审批中"); ErrorCode SMS_TEMPLATE_API_AUDIT_FAIL = new ErrorCode(1_002_012_004, "短信 API 模版无法使用,原因:审批不通过,{}"); ErrorCode SMS_TEMPLATE_API_NOT_FOUND = new ErrorCode(1_002_012_005, "短信 API 模版无法使用,原因:模版不存在"); // ========== 短信发送 1-002-013-000 ========== ErrorCode SMS_SEND_MOBILE_NOT_EXISTS = new ErrorCode(1_002_013_000, "手机号不存在"); ErrorCode SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS = new ErrorCode(1_002_013_001, "模板参数({})缺失"); ErrorCode SMS_SEND_TEMPLATE_NOT_EXISTS = new ErrorCode(1_002_013_002, "短信模板不存在"); // ========== 短信验证码 1-002-014-000 ========== ErrorCode SMS_CODE_NOT_FOUND = new ErrorCode(1_002_014_000, "验证码不存在"); ErrorCode SMS_CODE_EXPIRED = new ErrorCode(1_002_014_001, "验证码已过期"); ErrorCode SMS_CODE_USED = new ErrorCode(1_002_014_002, "验证码已使用"); ErrorCode SMS_CODE_NOT_CORRECT = new ErrorCode(1_002_014_003, "验证码不正确"); ErrorCode SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY = new ErrorCode(1_002_014_004, "超过每日短信发送数量"); ErrorCode SMS_CODE_SEND_TOO_FAST = new ErrorCode(1_002_014_005, "短信发送过于频繁"); ErrorCode SMS_CODE_IS_EXISTS = new ErrorCode(1_002_014_006, "手机号已被使用"); ErrorCode SMS_CODE_IS_UNUSED = new ErrorCode(1_002_014_007, "验证码未被使用"); // ========== 租户信息 1-002-015-000 ========== ErrorCode TENANT_NOT_EXISTS = new ErrorCode(1_002_015_000, "租户不存在"); ErrorCode TENANT_DISABLE = new ErrorCode(1_002_015_001, "名字为【{}】的租户已被禁用"); ErrorCode TENANT_EXPIRE = new ErrorCode(1_002_015_002, "名字为【{}】的租户已过期"); ErrorCode TENANT_CAN_NOT_UPDATE_SYSTEM = new ErrorCode(1_002_015_003, "系统租户不能进行修改、删除等操作!"); ErrorCode TENANT_NAME_DUPLICATE = new ErrorCode(1_002_015_004, "名字为【{}】的租户已存在"); ErrorCode TENANT_WEBSITE_DUPLICATE = new ErrorCode(1_002_015_005, "域名为【{}】的租户已存在"); // ========== 租户套餐 1-002-016-000 ========== ErrorCode TENANT_PACKAGE_NOT_EXISTS = new ErrorCode(1_002_016_000, "租户套餐不存在"); ErrorCode TENANT_PACKAGE_USED = new ErrorCode(1_002_016_001, "租户正在使用该套餐,请给租户重新设置套餐后再尝试删除"); ErrorCode TENANT_PACKAGE_DISABLE = new ErrorCode(1_002_016_002, "名字为【{}】的租户套餐已被禁用"); // ========== 社交用户 1-002-018-000 ========== ErrorCode SOCIAL_USER_AUTH_FAILURE = new ErrorCode(1_002_018_000, "社交授权失败,原因是:{}"); ErrorCode SOCIAL_USER_NOT_FOUND = new ErrorCode(1_002_018_001, "社交授权失败,找不到对应的用户"); ErrorCode SOCIAL_CLIENT_WEIXIN_MINI_APP_PHONE_CODE_ERROR = new ErrorCode(1_002_018_200, "获得手机号失败"); ErrorCode SOCIAL_CLIENT_NOT_EXISTS = new ErrorCode(1_002_018_201, "社交客户端不存在"); ErrorCode SOCIAL_CLIENT_UNIQUE = new ErrorCode(1_002_018_202, "社交客户端已存在配置"); // ========== OAuth2 客户端 1-002-020-000 ========= ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1_002_020_000, "OAuth2 客户端不存在"); ErrorCode OAUTH2_CLIENT_EXISTS = new ErrorCode(1_002_020_001, "OAuth2 客户端编号已存在"); ErrorCode OAUTH2_CLIENT_DISABLE = new ErrorCode(1_002_020_002, "OAuth2 客户端已禁用"); ErrorCode OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS = new ErrorCode(1_002_020_003, "不支持该授权类型"); ErrorCode OAUTH2_CLIENT_SCOPE_OVER = new ErrorCode(1_002_020_004, "授权范围过大"); ErrorCode OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH = new ErrorCode(1_002_020_005, "无效 redirect_uri: {}"); ErrorCode OAUTH2_CLIENT_CLIENT_SECRET_ERROR = new ErrorCode(1_002_020_006, "无效 client_secret: {}"); // ========== OAuth2 授权 1-002-021-000 ========= ErrorCode OAUTH2_GRANT_CLIENT_ID_MISMATCH = new ErrorCode(1_002_021_000, "client_id 不匹配"); ErrorCode OAUTH2_GRANT_REDIRECT_URI_MISMATCH = new ErrorCode(1_002_021_001, "redirect_uri 不匹配"); ErrorCode OAUTH2_GRANT_STATE_MISMATCH = new ErrorCode(1_002_021_002, "state 不匹配"); ErrorCode OAUTH2_GRANT_CODE_NOT_EXISTS = new ErrorCode(1_002_021_003, "code 不存在"); // ========== OAuth2 授权 1-002-022-000 ========= ErrorCode OAUTH2_CODE_NOT_EXISTS = new ErrorCode(1_002_022_000, "code 不存在"); ErrorCode OAUTH2_CODE_EXPIRE = new ErrorCode(1_002_022_001, "code 已过期"); // ========== 邮箱账号 1-002-023-000 ========== ErrorCode MAIL_ACCOUNT_NOT_EXISTS = new ErrorCode(1_002_023_000, "邮箱账号不存在"); ErrorCode MAIL_ACCOUNT_RELATE_TEMPLATE_EXISTS = new ErrorCode(1_002_023_001, "无法删除,该邮箱账号还有邮件模板"); // ========== 邮件模版 1-002-024-000 ========== ErrorCode MAIL_TEMPLATE_NOT_EXISTS = new ErrorCode(1_002_024_000, "邮件模版不存在"); ErrorCode MAIL_TEMPLATE_CODE_EXISTS = new ErrorCode(1_002_024_001, "邮件模版 code({}) 已存在"); // ========== 邮件发送 1-002-025-000 ========== ErrorCode MAIL_SEND_TEMPLATE_PARAM_MISS = new ErrorCode(1_002_025_000, "模板参数({})缺失"); ErrorCode MAIL_SEND_MAIL_NOT_EXISTS = new ErrorCode(1_002_025_001, "邮箱不存在"); // ========== 站内信模版 1-002-026-000 ========== ErrorCode NOTIFY_TEMPLATE_NOT_EXISTS = new ErrorCode(1_002_026_000, "站内信模版不存在"); ErrorCode NOTIFY_TEMPLATE_CODE_DUPLICATE = new ErrorCode(1_002_026_001, "已经存在编码为【{}】的站内信模板"); // ========== 站内信模版 1-002-027-000 ========== // ========== 站内信发送 1-002-028-000 ========== ErrorCode NOTIFY_SEND_TEMPLATE_PARAM_MISS = new ErrorCode(1_002_028_000, "模板参数({})缺失"); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/enums/LogRecordConstants.java ================================================ package co.yixiang.yshop.module.system.enums; /** * System 操作日志枚举 * 目的:统一管理,也减少 Service 里各种“复杂”字符串 * * @author yshop */ public interface LogRecordConstants { // ======================= SYSTEM_USER 用户 ======================= String SYSTEM_USER_TYPE = "SYSTEM 用户"; String SYSTEM_USER_CREATE_SUB_TYPE = "创建用户"; String SYSTEM_USER_CREATE_SUCCESS = "创建了用户【{{#user.nickname}}】"; String SYSTEM_USER_UPDATE_SUB_TYPE = "更新用户"; String SYSTEM_USER_UPDATE_SUCCESS = "更新了用户【{{#user.nickname}}】: {_DIFF{#updateReqVO}}"; String SYSTEM_USER_DELETE_SUB_TYPE = "删除用户"; String SYSTEM_USER_DELETE_SUCCESS = "删除了用户【{{#user.nickname}}】"; String SYSTEM_USER_UPDATE_PASSWORD_SUB_TYPE = "重置用户密码"; String SYSTEM_USER_UPDATE_PASSWORD_SUCCESS = "将用户【{{#user.nickname}}】的密码从【{{#user.password}}】重置为【{{#newPassword}}】"; // ======================= SYSTEM_ROLE 角色 ======================= String SYSTEM_ROLE_TYPE = "SYSTEM 角色"; String SYSTEM_ROLE_CREATE_SUB_TYPE = "创建角色"; String SYSTEM_ROLE_CREATE_SUCCESS = "创建了角色【{{#role.name}}】"; String SYSTEM_ROLE_UPDATE_SUB_TYPE = "更新角色"; String SYSTEM_ROLE_UPDATE_SUCCESS = "更新了角色【{{#role.name}}】: {_DIFF{#updateReqVO}}"; String SYSTEM_ROLE_DELETE_SUB_TYPE = "删除角色"; String SYSTEM_ROLE_DELETE_SUCCESS = "删除了角色【{{#role.name}}】"; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/enums/common/SexEnum.java ================================================ package co.yixiang.yshop.module.system.enums.common; import lombok.AllArgsConstructor; import lombok.Getter; /** * 性别的枚举值 * * @author yshop */ @Getter @AllArgsConstructor public enum SexEnum { /** 男 */ MALE(1), /** 女 */ FEMALE(2), /* 未知 */ UNKNOWN(0); /** * 性别 */ private final Integer sex; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/enums/logger/LoginLogTypeEnum.java ================================================ package co.yixiang.yshop.module.system.enums.logger; import lombok.AllArgsConstructor; import lombok.Getter; /** * 登录日志的类型枚举 */ @Getter @AllArgsConstructor public enum LoginLogTypeEnum { LOGIN_USERNAME(100), // 使用账号登录 LOGIN_SOCIAL(101), // 使用社交登录 LOGIN_MOBILE(103), // 使用手机登陆 LOGIN_SMS(104), // 使用短信登陆 LOGOUT_SELF(200), // 自己主动登出 LOGOUT_DELETE(202), // 强制退出 ; /** * 日志类型 */ private final Integer type; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/enums/logger/LoginResultEnum.java ================================================ package co.yixiang.yshop.module.system.enums.logger; import lombok.AllArgsConstructor; import lombok.Getter; /** * 登录结果的枚举类 */ @Getter @AllArgsConstructor public enum LoginResultEnum { SUCCESS(0), // 成功 BAD_CREDENTIALS(10), // 账号或密码不正确 USER_DISABLED(20), // 用户被禁用 CAPTCHA_NOT_FOUND(30), // 图片验证码不存在 CAPTCHA_CODE_ERROR(31), // 图片验证码不正确 ; /** * 结果 */ private final Integer result; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/enums/mail/MailSendStatusEnum.java ================================================ package co.yixiang.yshop.module.system.enums.mail; import lombok.AllArgsConstructor; import lombok.Getter; /** * 邮件的发送状态枚举 * * @author wangjingyi * @since 2022/4/10 13:39 */ @Getter @AllArgsConstructor public enum MailSendStatusEnum { INIT(0), // 初始化 SUCCESS(10), // 发送成功 FAILURE(20), // 发送失败 IGNORE(30), // 忽略,即不发送 ; private final int status; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/enums/notice/NoticeTypeEnum.java ================================================ package co.yixiang.yshop.module.system.enums.notice; import lombok.AllArgsConstructor; import lombok.Getter; /** * 通知类型 * * @author yshop */ @Getter @AllArgsConstructor public enum NoticeTypeEnum { NOTICE(1), ANNOUNCEMENT(2); /** * 类型 */ private final Integer type; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/enums/notify/NotifyTemplateTypeEnum.java ================================================ package co.yixiang.yshop.module.system.enums.notify; import lombok.AllArgsConstructor; import lombok.Getter; /** * 通知模板类型枚举 * * @author HUIHUI */ @Getter @AllArgsConstructor public enum NotifyTemplateTypeEnum { /** * 系统消息 */ SYSTEM_MESSAGE(2), /** * 通知消息 */ NOTIFICATION_MESSAGE(1); private final Integer type; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/enums/oauth2/OAuth2ClientConstants.java ================================================ package co.yixiang.yshop.module.system.enums.oauth2; /** * OAuth2.0 客户端的通用枚举 * * @author yshop */ public interface OAuth2ClientConstants { String CLIENT_ID_DEFAULT = "default"; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/enums/oauth2/OAuth2GrantTypeEnum.java ================================================ package co.yixiang.yshop.module.system.enums.oauth2; import cn.hutool.core.util.ArrayUtil; import lombok.AllArgsConstructor; import lombok.Getter; /** * OAuth2 授权类型(模式)的枚举 * * @author yshop */ @AllArgsConstructor @Getter public enum OAuth2GrantTypeEnum { PASSWORD("password"), // 密码模式 AUTHORIZATION_CODE("authorization_code"), // 授权码模式 IMPLICIT("implicit"), // 简化模式 CLIENT_CREDENTIALS("client_credentials"), // 客户端模式 REFRESH_TOKEN("refresh_token"), // 刷新模式 ; private final String grantType; public static OAuth2GrantTypeEnum getByGranType(String grantType) { return ArrayUtil.firstMatch(o -> o.getGrantType().equals(grantType), values()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/enums/permission/DataScopeEnum.java ================================================ package co.yixiang.yshop.module.system.enums.permission; import co.yixiang.yshop.framework.common.core.IntArrayValuable; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.Arrays; /** * 数据范围枚举类 * * 用于实现数据级别的权限 * * @author yshop */ @Getter @AllArgsConstructor public enum DataScopeEnum implements IntArrayValuable { ALL(1), // 全部数据权限 DEPT_CUSTOM(2), // 指定部门数据权限 DEPT_ONLY(3), // 部门数据权限 DEPT_AND_CHILD(4), // 部门及以下数据权限 SELF(5); // 仅本人数据权限 /** * 范围 */ private final Integer scope; public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(DataScopeEnum::getScope).toArray(); @Override public int[] array() { return ARRAYS; } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/enums/permission/MenuTypeEnum.java ================================================ package co.yixiang.yshop.module.system.enums.permission; import lombok.AllArgsConstructor; import lombok.Getter; /** * 菜单类型枚举类 * * @author yshop */ @Getter @AllArgsConstructor public enum MenuTypeEnum { DIR(1), // 目录 MENU(2), // 菜单 BUTTON(3) // 按钮 ; /** * 类型 */ private final Integer type; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/enums/permission/RoleCodeEnum.java ================================================ package co.yixiang.yshop.module.system.enums.permission; import co.yixiang.yshop.framework.common.util.object.ObjectUtils; import lombok.AllArgsConstructor; import lombok.Getter; /** * 角色标识枚举 */ @Getter @AllArgsConstructor public enum RoleCodeEnum { SUPER_ADMIN("super_admin", "超级管理员"), TENANT_ADMIN("tenant_admin", "租户管理员"), CRM_ADMIN("crm_admin", "CRM 管理员"); // CRM 系统专用 ; /** * 角色编码 */ private final String code; /** * 名字 */ private final String name; public static boolean isSuperAdmin(String code) { return ObjectUtils.equalsAny(code, SUPER_ADMIN.getCode()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/enums/permission/RoleTypeEnum.java ================================================ package co.yixiang.yshop.module.system.enums.permission; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor public enum RoleTypeEnum { /** * 内置角色 */ SYSTEM(1), /** * 自定义角色 */ CUSTOM(2); private final Integer type; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/enums/sms/SmsReceiveStatusEnum.java ================================================ package co.yixiang.yshop.module.system.enums.sms; import lombok.AllArgsConstructor; import lombok.Getter; /** * 短信的接收状态枚举 * * @author yshop * @date 2021/2/1 13:39 */ @Getter @AllArgsConstructor public enum SmsReceiveStatusEnum { INIT(0), // 初始化 SUCCESS(10), // 接收成功 FAILURE(20), // 接收失败 ; private final int status; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/enums/sms/SmsSceneEnum.java ================================================ package co.yixiang.yshop.module.system.enums.sms; import cn.hutool.core.util.ArrayUtil; import co.yixiang.yshop.framework.common.core.IntArrayValuable; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.Arrays; /** * 用户短信验证码发送场景的枚举 * * @author yshop */ @Getter @AllArgsConstructor public enum SmsSceneEnum implements IntArrayValuable { MEMBER_LOGIN(1, "user-sms-login", "会员用户 - 手机号登陆"), MEMBER_UPDATE_MOBILE(2, "user-update-mobile", "会员用户 - 修改手机"), MEMBER_UPDATE_PASSWORD(3, "user-update-password", "会员用户 - 修改密码"), MEMBER_RESET_PASSWORD(4, "user-reset-password", "会员用户 - 忘记密码"), ADMIN_MEMBER_LOGIN(21, "admin-sms-login", "后台用户 - 手机号登录"); public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(SmsSceneEnum::getScene).toArray(); /** * 验证场景的编号 */ private final Integer scene; /** * 模版编码 */ private final String templateCode; /** * 描述 */ private final String description; @Override public int[] array() { return ARRAYS; } public static SmsSceneEnum getCodeByScene(Integer scene) { return ArrayUtil.firstMatch(sceneEnum -> sceneEnum.getScene().equals(scene), values()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/enums/sms/SmsSendStatusEnum.java ================================================ package co.yixiang.yshop.module.system.enums.sms; import lombok.AllArgsConstructor; import lombok.Getter; /** * 短信的发送状态枚举 * * @author zzf * @date 2021/2/1 13:39 */ @Getter @AllArgsConstructor public enum SmsSendStatusEnum { INIT(0), // 初始化 SUCCESS(10), // 发送成功 FAILURE(20), // 发送失败 IGNORE(30), // 忽略,即不发送 ; private final int status; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/enums/sms/SmsTemplateTypeEnum.java ================================================ package co.yixiang.yshop.module.system.enums.sms; import lombok.AllArgsConstructor; import lombok.Getter; /** * 短信的模板类型枚举 * * @author yshop */ @Getter @AllArgsConstructor public enum SmsTemplateTypeEnum { VERIFICATION_CODE(1), // 验证码 NOTICE(2), // 通知 PROMOTION(3), // 营销 ; /** * 类型 */ private final int type; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-api/src/main/java/co/yixiang/yshop/module/system/enums/social/SocialTypeEnum.java ================================================ package co.yixiang.yshop.module.system.enums.social; import cn.hutool.core.util.ArrayUtil; import co.yixiang.yshop.framework.common.core.IntArrayValuable; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.Arrays; /** * 社交平台的类型枚举 * * @author yshop */ @Getter @AllArgsConstructor public enum SocialTypeEnum implements IntArrayValuable { /** * Gitee * * @see 接入文档 */ GITEE(10, "GITEE"), /** * 钉钉 * * @see 接入文档 */ DINGTALK(20, "DINGTALK"), /** * 企业微信 * * @see 接入文档 */ WECHAT_ENTERPRISE(30, "WECHAT_ENTERPRISE"), /** * 微信公众平台 - 移动端 H5 * * @see 接入文档 */ WECHAT_MP(31, "WECHAT_MP"), /** * 微信开放平台 - 网站应用 PC 端扫码授权登录 * * @see 接入文档 */ WECHAT_OPEN(32, "WECHAT_OPEN"), /** * 微信小程序 * * @see 接入文档 */ WECHAT_MINI_APP(34, "WECHAT_MINI_APP"), ; public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(SocialTypeEnum::getType).toArray(); /** * 类型 */ private final Integer type; /** * 类型的标识 */ private final String source; @Override public int[] array() { return ARRAYS; } public static SocialTypeEnum valueOfType(Integer type) { return ArrayUtil.firstMatch(o -> o.getType().equals(type), values()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/pom.xml ================================================ co.yixiang.boot yshop-module-system ${revision} 4.0.0 yshop-module-system-biz jar ${project.artifactId} system 模块下,我们放通用业务,支撑上层的核心业务。 例如说:用户、部门、权限、数据字典等等 co.yixiang.boot yshop-module-system-api ${revision} co.yixiang.boot yshop-module-infra-api ${revision} co.yixiang.boot yshop-module-store-biz ${revision} co.yixiang.boot yshop-spring-boot-starter-biz-data-permission co.yixiang.boot yshop-spring-boot-starter-biz-tenant co.yixiang.boot yshop-spring-boot-starter-biz-ip co.yixiang.boot yshop-spring-boot-starter-security org.springframework.boot spring-boot-starter-validation co.yixiang.boot yshop-spring-boot-starter-mybatis co.yixiang.boot yshop-spring-boot-starter-redis co.yixiang.boot yshop-spring-boot-starter-job co.yixiang.boot yshop-spring-boot-starter-mq co.yixiang.boot yshop-spring-boot-starter-test test co.yixiang.boot yshop-spring-boot-starter-excel org.springframework.boot spring-boot-starter-mail com.xingyuv spring-boot-starter-justauth com.github.binarywang wx-java-mp-spring-boot-starter com.github.binarywang wx-java-miniapp-spring-boot-starter com.aliyun aliyun-java-sdk-core com.aliyun aliyun-java-sdk-dysmsapi com.tencentcloudapi tencentcloud-sdk-java-sms com.xingyuv spring-boot-starter-captcha-plus org.dromara.hutool hutool-extra ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/api/dept/DeptApiImpl.java ================================================ package co.yixiang.yshop.module.system.api.dept; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.api.dept.dto.DeptRespDTO; import co.yixiang.yshop.module.system.dal.dataobject.dept.DeptDO; import co.yixiang.yshop.module.system.service.dept.DeptService; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import java.util.Collection; import java.util.List; /** * 部门 API 实现类 * * @author yshop */ @Service public class DeptApiImpl implements DeptApi { @Resource private DeptService deptService; @Override public DeptRespDTO getDept(Long id) { DeptDO dept = deptService.getDept(id); return BeanUtils.toBean(dept, DeptRespDTO.class); } @Override public List getDeptList(Collection ids) { List depts = deptService.getDeptList(ids); return BeanUtils.toBean(depts, DeptRespDTO.class); } @Override public void validateDeptList(Collection ids) { deptService.validateDeptList(ids); } @Override public List getChildDeptList(Long id) { List childDeptList = deptService.getChildDeptList(id); return BeanUtils.toBean(childDeptList, DeptRespDTO.class); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/api/dept/PostApiImpl.java ================================================ package co.yixiang.yshop.module.system.api.dept; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.api.dept.dto.PostRespDTO; import co.yixiang.yshop.module.system.dal.dataobject.dept.PostDO; import co.yixiang.yshop.module.system.service.dept.PostService; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import java.util.Collection; import java.util.List; /** * 岗位 API 实现类 * * @author yshop */ @Service public class PostApiImpl implements PostApi { @Resource private PostService postService; @Override public void validPostList(Collection ids) { postService.validatePostList(ids); } @Override public List getPostList(Collection ids) { List list = postService.getPostList(ids); return BeanUtils.toBean(list, PostRespDTO.class); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/api/dict/DictDataApiImpl.java ================================================ package co.yixiang.yshop.module.system.api.dict; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.api.dict.dto.DictDataRespDTO; import co.yixiang.yshop.module.system.dal.dataobject.dict.DictDataDO; import co.yixiang.yshop.module.system.service.dict.DictDataService; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import java.util.Collection; import java.util.List; /** * 字典数据 API 实现类 * * @author yshop */ @Service public class DictDataApiImpl implements DictDataApi { @Resource private DictDataService dictDataService; @Override public void validateDictDataList(String dictType, Collection values) { dictDataService.validateDictDataList(dictType, values); } @Override public DictDataRespDTO getDictData(String dictType, String value) { DictDataDO dictData = dictDataService.getDictData(dictType, value); return BeanUtils.toBean(dictData, DictDataRespDTO.class); } @Override public DictDataRespDTO parseDictData(String dictType, String label) { DictDataDO dictData = dictDataService.parseDictData(dictType, label); return BeanUtils.toBean(dictData, DictDataRespDTO.class); } @Override public List getDictDataList(String dictType) { List list = dictDataService.getDictDataListByDictType(dictType); return BeanUtils.toBean(list, DictDataRespDTO.class); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/api/logger/LoginLogApiImpl.java ================================================ package co.yixiang.yshop.module.system.api.logger; import co.yixiang.yshop.module.system.api.logger.dto.LoginLogCreateReqDTO; import co.yixiang.yshop.module.system.service.logger.LoginLogService; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; /** * 登录日志的 API 实现类 * * @author yshop */ @Service @Validated public class LoginLogApiImpl implements LoginLogApi { @Resource private LoginLogService loginLogService; @Override public void createLoginLog(LoginLogCreateReqDTO reqDTO) { loginLogService.createLoginLog(reqDTO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/api/logger/OperateLogApiImpl.java ================================================ package co.yixiang.yshop.module.system.api.logger; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.api.logger.dto.OperateLogCreateReqDTO; import co.yixiang.yshop.module.system.api.logger.dto.OperateLogPageReqDTO; import co.yixiang.yshop.module.system.api.logger.dto.OperateLogRespDTO; import co.yixiang.yshop.module.system.dal.dataobject.logger.OperateLogDO; import co.yixiang.yshop.module.system.service.logger.OperateLogService; import com.fhs.core.trans.anno.TransMethodResult; import jakarta.annotation.Resource; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; /** * 操作日志 API 实现类 * * @author yshop */ @Service @Validated public class OperateLogApiImpl implements OperateLogApi { @Resource private OperateLogService operateLogService; @Override @Async public void createOperateLog(OperateLogCreateReqDTO createReqDTO) { operateLogService.createOperateLog(createReqDTO); } @Override @TransMethodResult public PageResult getOperateLogPage(OperateLogPageReqDTO pageReqVO) { PageResult operateLogPage = operateLogService.getOperateLogPage(pageReqVO); return BeanUtils.toBean(operateLogPage, OperateLogRespDTO.class); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/api/mail/MailSendApiImpl.java ================================================ package co.yixiang.yshop.module.system.api.mail; import co.yixiang.yshop.module.system.api.mail.dto.MailSendSingleToUserReqDTO; import co.yixiang.yshop.module.system.service.mail.MailSendService; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; /** * 邮件发送 API 实现类 * * @author wangjingyi */ @Service @Validated public class MailSendApiImpl implements MailSendApi { @Resource private MailSendService mailSendService; @Override public Long sendSingleMailToAdmin(MailSendSingleToUserReqDTO reqDTO) { return mailSendService.sendSingleMailToAdmin(reqDTO.getMail(), reqDTO.getUserId(), reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); } @Override public Long sendSingleMailToMember(MailSendSingleToUserReqDTO reqDTO) { return mailSendService.sendSingleMailToMember(reqDTO.getMail(), reqDTO.getUserId(), reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/api/notify/NotifyMessageSendApiImpl.java ================================================ package co.yixiang.yshop.module.system.api.notify; import co.yixiang.yshop.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; import co.yixiang.yshop.module.system.service.notify.NotifySendService; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; /** * 站内信发送 API 实现类 * * @author xrcoder */ @Service public class NotifyMessageSendApiImpl implements NotifyMessageSendApi { @Resource private NotifySendService notifySendService; @Override public Long sendSingleMessageToAdmin(NotifySendSingleToUserReqDTO reqDTO) { return notifySendService.sendSingleNotifyToAdmin(reqDTO.getUserId(), reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); } @Override public Long sendSingleMessageToMember(NotifySendSingleToUserReqDTO reqDTO) { return notifySendService.sendSingleNotifyToMember(reqDTO.getUserId(), reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/api/oauth2/OAuth2TokenApiImpl.java ================================================ package co.yixiang.yshop.module.system.api.oauth2; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO; import co.yixiang.yshop.module.system.api.oauth2.dto.OAuth2AccessTokenCreateReqDTO; import co.yixiang.yshop.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import co.yixiang.yshop.module.system.service.oauth2.OAuth2TokenService; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; /** * OAuth2.0 Token API 实现类 * * @author yshop */ @Service public class OAuth2TokenApiImpl implements OAuth2TokenApi { @Resource private OAuth2TokenService oauth2TokenService; @Override public OAuth2AccessTokenRespDTO createAccessToken(OAuth2AccessTokenCreateReqDTO reqDTO) { OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken( reqDTO.getUserId(), reqDTO.getUserType(), reqDTO.getClientId(), reqDTO.getScopes()); return BeanUtils.toBean(accessTokenDO, OAuth2AccessTokenRespDTO.class); } @Override public OAuth2AccessTokenCheckRespDTO checkAccessToken(String accessToken) { OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.checkAccessToken(accessToken); return BeanUtils.toBean(accessTokenDO, OAuth2AccessTokenCheckRespDTO.class); } @Override public OAuth2AccessTokenRespDTO removeAccessToken(String accessToken) { OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.removeAccessToken(accessToken); return BeanUtils.toBean(accessTokenDO, OAuth2AccessTokenRespDTO.class); } @Override public OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, String clientId) { OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, clientId); return BeanUtils.toBean(accessTokenDO, OAuth2AccessTokenRespDTO.class); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/api/permission/PermissionApiImpl.java ================================================ package co.yixiang.yshop.module.system.api.permission; import co.yixiang.yshop.module.system.api.permission.dto.DeptDataPermissionRespDTO; import co.yixiang.yshop.module.system.service.permission.PermissionService; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import java.util.Collection; import java.util.Set; /** * 权限 API 实现类 * * @author yshop */ @Service public class PermissionApiImpl implements PermissionApi { @Resource private PermissionService permissionService; @Override public Set getUserRoleIdListByRoleIds(Collection roleIds) { return permissionService.getUserRoleIdListByRoleId(roleIds); } @Override public boolean hasAnyPermissions(Long userId, String... permissions) { return permissionService.hasAnyPermissions(userId, permissions); } @Override public boolean hasAnyRoles(Long userId, String... roles) { return permissionService.hasAnyRoles(userId, roles); } @Override public DeptDataPermissionRespDTO getDeptDataPermission(Long userId) { return permissionService.getDeptDataPermission(userId); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/api/permission/RoleApiImpl.java ================================================ package co.yixiang.yshop.module.system.api.permission; import co.yixiang.yshop.module.system.service.permission.RoleService; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import java.util.Collection; /** * 角色 API 实现类 * * @author yshop */ @Service public class RoleApiImpl implements RoleApi { @Resource private RoleService roleService; @Override public void validRoleList(Collection ids) { roleService.validateRoleList(ids); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/api/sms/SmsCodeApiImpl.java ================================================ package co.yixiang.yshop.module.system.api.sms; import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeValidateReqDTO; import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeSendReqDTO; import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeUseReqDTO; import co.yixiang.yshop.module.system.service.sms.SmsCodeService; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; /** * 短信验证码 API 实现类 * * @author yshop */ @Service @Validated public class SmsCodeApiImpl implements SmsCodeApi { @Resource private SmsCodeService smsCodeService; @Override public void sendSmsCode(SmsCodeSendReqDTO reqDTO) { smsCodeService.sendSmsCode(reqDTO); } @Override public void useSmsCode(SmsCodeUseReqDTO reqDTO) { smsCodeService.useSmsCode(reqDTO); } @Override public void validateSmsCode(SmsCodeValidateReqDTO reqDTO) { smsCodeService.validateSmsCode(reqDTO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/api/sms/SmsSendApiImpl.java ================================================ package co.yixiang.yshop.module.system.api.sms; import co.yixiang.yshop.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO; import co.yixiang.yshop.module.system.service.sms.SmsSendService; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; /** * 短信发送 API 接口 * * @author yshop */ @Service @Validated public class SmsSendApiImpl implements SmsSendApi { @Resource private SmsSendService smsSendService; @Override public Long sendSingleSmsToAdmin(SmsSendSingleToUserReqDTO reqDTO) { return smsSendService.sendSingleSmsToAdmin(reqDTO.getMobile(), reqDTO.getUserId(), reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); } @Override public Long sendSingleSmsToMember(SmsSendSingleToUserReqDTO reqDTO) { return smsSendService.sendSingleSmsToMember(reqDTO.getMobile(), reqDTO.getUserId(), reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/api/social/SocialClientApiImpl.java ================================================ package co.yixiang.yshop.module.system.api.social; import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.api.social.dto.SocialWxJsapiSignatureRespDTO; import co.yixiang.yshop.module.system.api.social.dto.SocialWxPhoneNumberInfoRespDTO; import co.yixiang.yshop.module.system.service.social.SocialClientService; import me.chanjar.weixin.common.bean.WxJsapiSignature; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; /** * 社交应用的 API 实现类 * * @author yshop */ @Service @Validated public class SocialClientApiImpl implements SocialClientApi { @Resource private SocialClientService socialClientService; @Override public String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri) { return socialClientService.getAuthorizeUrl(socialType, userType, redirectUri); } @Override public SocialWxJsapiSignatureRespDTO createWxMpJsapiSignature(Integer userType, String url) { WxJsapiSignature signature = socialClientService.createWxMpJsapiSignature(userType, url); return BeanUtils.toBean(signature, SocialWxJsapiSignatureRespDTO.class); } @Override public SocialWxPhoneNumberInfoRespDTO getWxMaPhoneNumberInfo(Integer userType, String phoneCode) { WxMaPhoneNumberInfo info = socialClientService.getWxMaPhoneNumberInfo(userType, phoneCode); return BeanUtils.toBean(info, SocialWxPhoneNumberInfoRespDTO.class); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/api/social/SocialUserApiImpl.java ================================================ package co.yixiang.yshop.module.system.api.social; import co.yixiang.yshop.module.system.api.social.dto.SocialUserBindReqDTO; import co.yixiang.yshop.module.system.api.social.dto.SocialUserRespDTO; import co.yixiang.yshop.module.system.api.social.dto.SocialUserUnbindReqDTO; import co.yixiang.yshop.module.system.service.social.SocialUserService; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; /** * 社交用户的 API 实现类 * * @author yshop */ @Service @Validated public class SocialUserApiImpl implements SocialUserApi { @Resource private SocialUserService socialUserService; @Override public String bindSocialUser(SocialUserBindReqDTO reqDTO) { return socialUserService.bindSocialUser(reqDTO); } @Override public void unbindSocialUser(SocialUserUnbindReqDTO reqDTO) { socialUserService.unbindSocialUser(reqDTO.getUserId(), reqDTO.getUserType(), reqDTO.getSocialType(), reqDTO.getOpenid()); } @Override public SocialUserRespDTO getSocialUserByUserId(Integer userType, Long userId, Integer socialType) { return socialUserService.getSocialUserByUserId(userType, userId, socialType); } @Override public SocialUserRespDTO getSocialUserByCode(Integer userType, Integer socialType, String code, String state) { return socialUserService.getSocialUserByCode(userType, socialType, code, state); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/api/tenant/TenantApiImpl.java ================================================ package co.yixiang.yshop.module.system.api.tenant; import co.yixiang.yshop.module.system.service.tenant.TenantService; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import java.util.List; /** * 多租户的 API 实现类 * * @author yshop */ @Service public class TenantApiImpl implements TenantApi { @Resource private TenantService tenantService; @Override public List getTenantIdList() { return tenantService.getTenantIdList(); } @Override public void validateTenant(Long id) { tenantService.validTenant(id); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/api/user/AdminUserApiImpl.java ================================================ package co.yixiang.yshop.module.system.api.user; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjUtil; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.api.user.dto.AdminUserRespDTO; import co.yixiang.yshop.module.system.dal.dataobject.dept.DeptDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.module.system.service.dept.DeptService; import co.yixiang.yshop.module.system.service.user.AdminUserService; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertSet; /** * Admin 用户 API 实现类 * * @author yshop */ @Service public class AdminUserApiImpl implements AdminUserApi { @Resource private AdminUserService userService; @Resource private DeptService deptService; @Override public AdminUserRespDTO getUser(Long id) { AdminUserDO user = userService.getUser(id); return BeanUtils.toBean(user, AdminUserRespDTO.class); } @Override public List getUserListBySubordinate(Long id) { // 1.1 获取用户负责的部门 AdminUserDO user = userService.getUser(id); if (user == null) { return Collections.emptyList(); } ArrayList deptIds = new ArrayList<>(); DeptDO dept = deptService.getDept(user.getDeptId()); if (dept == null) { return Collections.emptyList(); } if (ObjUtil.notEqual(dept.getLeaderUserId(), id)) { // 校验为负责人 return Collections.emptyList(); } deptIds.add(dept.getId()); // 1.2 获取所有子部门 List childDeptList = deptService.getChildDeptList(dept.getId()); if (CollUtil.isNotEmpty(childDeptList)) { deptIds.addAll(convertSet(childDeptList, DeptDO::getId)); } // 2. 获取部门对应的用户信息 List users = userService.getUserListByDeptIds(deptIds); users.removeIf(item -> ObjUtil.equal(item.getId(), id)); // 排除自己 return BeanUtils.toBean(users, AdminUserRespDTO.class); } @Override public List getUserList(Collection ids) { List users = userService.getUserList(ids); return BeanUtils.toBean(users, AdminUserRespDTO.class); } @Override public List getUserListByDeptIds(Collection deptIds) { List users = userService.getUserListByDeptIds(deptIds); return BeanUtils.toBean(users, AdminUserRespDTO.class); } @Override public List getUserListByPostIds(Collection postIds) { List users = userService.getUserListByPostIds(postIds); return BeanUtils.toBean(users, AdminUserRespDTO.class); } @Override public void validateUserList(Collection ids) { userService.validateUserList(ids); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/auth/AuthController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.auth; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.security.config.SecurityProperties; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import co.yixiang.yshop.module.system.controller.admin.auth.vo.*; import co.yixiang.yshop.module.system.convert.auth.AuthConvert; import co.yixiang.yshop.module.system.dal.dataobject.permission.MenuDO; import co.yixiang.yshop.module.system.dal.dataobject.permission.RoleDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.module.system.enums.logger.LoginLogTypeEnum; import co.yixiang.yshop.module.system.service.auth.AdminAuthService; import co.yixiang.yshop.module.system.service.permission.MenuService; import co.yixiang.yshop.module.system.service.permission.PermissionService; import co.yixiang.yshop.module.system.service.permission.RoleService; import co.yixiang.yshop.module.system.service.social.SocialClientService; import co.yixiang.yshop.module.system.service.user.AdminUserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.annotation.security.PermitAll; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import java.util.Collections; import java.util.List; import java.util.Set; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertSet; import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @Tag(name = "管理后台 - 认证") @RestController @RequestMapping("/system/auth") @Validated @Slf4j public class AuthController { @Resource private AdminAuthService authService; @Resource private AdminUserService userService; @Resource private RoleService roleService; @Resource private MenuService menuService; @Resource private PermissionService permissionService; @Resource private SocialClientService socialClientService; @Resource private SecurityProperties securityProperties; @PostMapping("/login") @PermitAll @Operation(summary = "使用账号密码登录") public CommonResult login(@RequestBody @Valid AuthLoginReqVO reqVO) { return success(authService.login(reqVO)); } @PostMapping("/logout") @PermitAll @Operation(summary = "登出系统") public CommonResult logout(HttpServletRequest request) { String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader(), securityProperties.getTokenParameter()); if (StrUtil.isNotBlank(token)) { authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType()); } return success(true); } @PostMapping("/refresh-token") @PermitAll @Operation(summary = "刷新令牌") @Parameter(name = "refreshToken", description = "刷新令牌", required = true) public CommonResult refreshToken(@RequestParam("refreshToken") String refreshToken) { return success(authService.refreshToken(refreshToken)); } @GetMapping("/get-permission-info") @Operation(summary = "获取登录用户的权限信息") public CommonResult getPermissionInfo() { // 1.1 获得用户信息 AdminUserDO user = userService.getUser(getLoginUserId()); if (user == null) { return success(null); } // 1.2 获得角色列表 Set roleIds = permissionService.getUserRoleIdListByUserId(getLoginUserId()); if (CollUtil.isEmpty(roleIds)) { return success(AuthConvert.INSTANCE.convert(user, Collections.emptyList(), Collections.emptyList())); } List roles = roleService.getRoleList(roleIds); roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); // 移除禁用的角色 // 1.3 获得菜单列表 Set menuIds = permissionService.getRoleMenuListByRoleId(convertSet(roles, RoleDO::getId)); List menuList = menuService.getMenuList(menuIds); menuList.removeIf(menu -> !CommonStatusEnum.ENABLE.getStatus().equals(menu.getStatus())); // 移除禁用的菜单 // 2. 拼接结果返回 return success(AuthConvert.INSTANCE.convert(user, roles, menuList)); } // ========== 短信登录相关 ========== @PostMapping("/sms-login") @PermitAll @Operation(summary = "使用短信验证码登录") public CommonResult smsLogin(@RequestBody @Valid AuthSmsLoginReqVO reqVO) { return success(authService.smsLogin(reqVO)); } @PostMapping("/send-sms-code") @PermitAll @Operation(summary = "发送手机验证码") public CommonResult sendLoginSmsCode(@RequestBody @Valid AuthSmsSendReqVO reqVO) { authService.sendSmsCode(reqVO); return success(true); } // ========== 社交登录相关 ========== @GetMapping("/social-auth-redirect") @PermitAll @Operation(summary = "社交授权的跳转") @Parameters({ @Parameter(name = "type", description = "社交类型", required = true), @Parameter(name = "redirectUri", description = "回调路径") }) public CommonResult socialLogin(@RequestParam("type") Integer type, @RequestParam("redirectUri") String redirectUri) { return success(socialClientService.getAuthorizeUrl( type, UserTypeEnum.ADMIN.getValue(), redirectUri)); } @PostMapping("/social-login") @PermitAll @Operation(summary = "社交快捷登录,使用 code 授权码", description = "适合未登录的用户,但是社交账号已绑定用户") public CommonResult socialQuickLogin(@RequestBody @Valid AuthSocialLoginReqVO reqVO) { return success(authService.socialLogin(reqVO)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/auth/vo/AuthLoginReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.auth.vo; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.validation.InEnum; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.Length; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Pattern; @Schema(description = "管理后台 - 账号密码登录 Request VO,如果登录并绑定社交用户,需要传递 social 开头的参数") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AuthLoginReqVO { @Schema(description = "账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshopyuanma") @NotEmpty(message = "登录账号不能为空") @Length(min = 4, max = 16, message = "账号长度为 4-16 位") @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母") private String username; @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao") @NotEmpty(message = "密码不能为空") @Length(min = 4, max = 16, message = "密码长度为 4-16 位") private String password; // ========== 图片验证码相关 ========== @Schema(description = "验证码,验证码开启时,需要传递", requiredMode = Schema.RequiredMode.REQUIRED, example = "PfcH6mgr8tpXuMWFjvW6YVaqrswIuwmWI5dsVZSg7sGpWtDCUbHuDEXl3cFB1+VvCC/rAkSwK8Fad52FSuncVg==") @NotEmpty(message = "验证码不能为空", groups = CodeEnableGroup.class) private String captchaVerification; // ========== 绑定社交登录时,需要传递如下参数 ========== @Schema(description = "社交平台的类型,参见 SocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") @InEnum(SocialTypeEnum.class) private Integer socialType; @Schema(description = "授权码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private String socialCode; @Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") private String socialState; /** * 开启验证码的 Group */ public interface CodeEnableGroup {} @AssertTrue(message = "授权码不能为空") public boolean isSocialCodeValid() { return socialType == null || StrUtil.isNotEmpty(socialCode); } @AssertTrue(message = "授权 state 不能为空") public boolean isSocialState() { return socialType == null || StrUtil.isNotEmpty(socialState); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/auth/vo/AuthLoginRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.auth.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.time.LocalDateTime; @Schema(description = "管理后台 - 登录 Response VO") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AuthLoginRespVO { @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long userId; @Schema(description = "访问令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "happy") private String accessToken; @Schema(description = "刷新令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "nice") private String refreshToken; @Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime expiresTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/auth/vo/AuthMenuRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.auth.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; @Schema(description = "管理后台 - 登录用户的菜单信息 Response VO") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AuthMenuRespVO { @Schema(description = "菜单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private Long id; @Schema(description = "父菜单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long parentId; @Schema(description = "菜单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String name; @Schema(description = "路由地址,仅菜单类型为菜单或者目录时,才需要传", example = "post") private String path; @Schema(description = "组件路径,仅菜单类型为菜单时,才需要传", example = "system/post/index") private String component; @Schema(description = "组件名", example = "SystemUser") private String componentName; @Schema(description = "菜单图标,仅菜单类型为菜单或者目录时,才需要传", example = "/menu/list") private String icon; @Schema(description = "是否可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") private Boolean visible; @Schema(description = "是否缓存", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") private Boolean keepAlive; @Schema(description = "是否总是显示", example = "false") private Boolean alwaysShow; /** * 子路由 */ private List children; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/auth/vo/AuthPermissionInfoRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.auth.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; import java.util.Set; @Schema(description = "管理后台 - 登录用户的权限信息 Response VO,额外包括用户信息和角色列表") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AuthPermissionInfoRespVO { @Schema(description = "用户信息", requiredMode = Schema.RequiredMode.REQUIRED) private UserVO user; @Schema(description = "角色标识数组", requiredMode = Schema.RequiredMode.REQUIRED) private Set roles; @Schema(description = "操作权限数组", requiredMode = Schema.RequiredMode.REQUIRED) private Set permissions; @Schema(description = "菜单树", requiredMode = Schema.RequiredMode.REQUIRED) private List menus; @Schema(description = "用户信息 VO") @Data @NoArgsConstructor @AllArgsConstructor @Builder public static class UserVO { @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String nickname; @Schema(description = "用户头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.yixiang.co/xx.jpg") private String avatar; @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") private Long deptId; private Long shopId; } @Schema(description = "管理后台 - 登录用户的菜单信息 Response VO") @Data @NoArgsConstructor @AllArgsConstructor @Builder public static class MenuVO { @Schema(description = "菜单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private Long id; @Schema(description = "父菜单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long parentId; @Schema(description = "菜单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String name; @Schema(description = "路由地址,仅菜单类型为菜单或者目录时,才需要传", example = "post") private String path; @Schema(description = "组件路径,仅菜单类型为菜单时,才需要传", example = "system/post/index") private String component; @Schema(description = "组件名", example = "SystemUser") private String componentName; @Schema(description = "菜单图标,仅菜单类型为菜单或者目录时,才需要传", example = "/menu/list") private String icon; @Schema(description = "是否可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") private Boolean visible; @Schema(description = "是否缓存", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") private Boolean keepAlive; @Schema(description = "是否总是显示", example = "false") private Boolean alwaysShow; /** * 子路由 */ private List children; } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/auth/vo/AuthSmsLoginReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.auth.vo; import co.yixiang.yshop.framework.common.validation.Mobile; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import jakarta.validation.constraints.NotEmpty; @Schema(description = "管理后台 - 短信验证码的登录 Request VO") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AuthSmsLoginReqVO { @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshopyuanma") @NotEmpty(message = "手机号不能为空") @Mobile private String mobile; @Schema(description = "短信验证码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotEmpty(message = "验证码不能为空") private String code; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/auth/vo/AuthSmsSendReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.auth.vo; import co.yixiang.yshop.framework.common.validation.InEnum; import co.yixiang.yshop.framework.common.validation.Mobile; import co.yixiang.yshop.module.system.enums.sms.SmsSceneEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 发送手机验证码 Request VO") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AuthSmsSendReqVO { @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshopyuanma") @NotEmpty(message = "手机号不能为空") @Mobile private String mobile; @Schema(description = "短信场景", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "发送场景不能为空") @InEnum(SmsSceneEnum.class) private Integer scene; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/auth/vo/AuthSocialLoginReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.auth.vo; import co.yixiang.yshop.framework.common.validation.InEnum; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 社交绑定登录 Request VO,使用 code 授权码 + 账号密码") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AuthSocialLoginReqVO { @Schema(description = "社交平台的类型,参见 UserSocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") @InEnum(SocialTypeEnum.class) @NotNull(message = "社交平台的类型不能为空") private Integer type; @Schema(description = "授权码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotEmpty(message = "授权码不能为空") private String code; @Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") @NotEmpty(message = "state 不能为空") private String state; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/captcha/CaptchaController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.captcha; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.util.servlet.ServletUtils; import com.xingyuv.captcha.model.common.ResponseModel; import com.xingyuv.captcha.model.vo.CaptchaVO; import com.xingyuv.captcha.service.CaptchaService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import jakarta.annotation.Resource; import jakarta.annotation.security.PermitAll; import jakarta.servlet.http.HttpServletRequest; @Tag(name = "管理后台 - 验证码") @RestController("adminCaptchaController") @RequestMapping("/system/captcha") public class CaptchaController { @Resource private CaptchaService captchaService; @PostMapping({"/get"}) @Operation(summary = "获得验证码") @PermitAll public ResponseModel get(@RequestBody CaptchaVO data, HttpServletRequest request) { assert request.getRemoteHost() != null; data.setBrowserInfo(getRemoteId(request)); return captchaService.get(data); } @PostMapping("/check") @Operation(summary = "校验验证码") @PermitAll public ResponseModel check(@RequestBody CaptchaVO data, HttpServletRequest request) { data.setBrowserInfo(getRemoteId(request)); return captchaService.check(data); } public static String getRemoteId(HttpServletRequest request) { String ip = ServletUtils.getClientIP(request); String ua = request.getHeader("user-agent"); if (StrUtil.isNotBlank(ip)) { return ip + ua; } return request.getRemoteAddr() + ua; } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/dept/DeptController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.dept; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.dept.vo.dept.DeptListReqVO; import co.yixiang.yshop.module.system.controller.admin.dept.vo.dept.DeptRespVO; import co.yixiang.yshop.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO; import co.yixiang.yshop.module.system.controller.admin.dept.vo.dept.DeptSimpleRespVO; import co.yixiang.yshop.module.system.dal.dataobject.dept.DeptDO; import co.yixiang.yshop.module.system.service.dept.DeptService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 部门") @RestController @RequestMapping("/system/dept") @Validated public class DeptController { @Resource private DeptService deptService; @PostMapping("create") @Operation(summary = "创建部门") @PreAuthorize("@ss.hasPermission('system:dept:create')") public CommonResult createDept(@Valid @RequestBody DeptSaveReqVO createReqVO) { Long deptId = deptService.createDept(createReqVO); return success(deptId); } @PutMapping("update") @Operation(summary = "更新部门") @PreAuthorize("@ss.hasPermission('system:dept:update')") public CommonResult updateDept(@Valid @RequestBody DeptSaveReqVO updateReqVO) { deptService.updateDept(updateReqVO); return success(true); } @DeleteMapping("delete") @Operation(summary = "删除部门") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:dept:delete')") public CommonResult deleteDept(@RequestParam("id") Long id) { deptService.deleteDept(id); return success(true); } @GetMapping("/list") @Operation(summary = "获取部门列表") @PreAuthorize("@ss.hasPermission('system:dept:query')") public CommonResult> getDeptList(DeptListReqVO reqVO) { List list = deptService.getDeptList(reqVO); return success(BeanUtils.toBean(list, DeptRespVO.class)); } @GetMapping(value = {"/list-all-simple", "/simple-list"}) @Operation(summary = "获取部门精简信息列表", description = "只包含被开启的部门,主要用于前端的下拉选项") public CommonResult> getSimpleDeptList() { List list = deptService.getDeptList( new DeptListReqVO().setStatus(CommonStatusEnum.ENABLE.getStatus())); return success(BeanUtils.toBean(list, DeptSimpleRespVO.class)); } @GetMapping("/get") @Operation(summary = "获得部门信息") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:dept:query')") public CommonResult getDept(@RequestParam("id") Long id) { DeptDO dept = deptService.getDept(id); return success(BeanUtils.toBean(dept, DeptRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/dept/PostController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.dept; import co.yixiang.yshop.framework.apilog.core.annotation.ApiAccessLog; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.system.controller.admin.dept.vo.post.PostPageReqVO; import co.yixiang.yshop.module.system.controller.admin.dept.vo.post.PostRespVO; import co.yixiang.yshop.module.system.controller.admin.dept.vo.post.PostSaveReqVO; import co.yixiang.yshop.module.system.controller.admin.dept.vo.post.PostSimpleRespVO; import co.yixiang.yshop.module.system.dal.dataobject.dept.PostDO; import co.yixiang.yshop.module.system.service.dept.PostService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.io.IOException; import java.util.Collections; import java.util.Comparator; import java.util.List; import static co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 岗位") @RestController @RequestMapping("/system/post") @Validated public class PostController { @Resource private PostService postService; @PostMapping("/create") @Operation(summary = "创建岗位") @PreAuthorize("@ss.hasPermission('system:post:create')") public CommonResult createPost(@Valid @RequestBody PostSaveReqVO createReqVO) { Long postId = postService.createPost(createReqVO); return success(postId); } @PutMapping("/update") @Operation(summary = "修改岗位") @PreAuthorize("@ss.hasPermission('system:post:update')") public CommonResult updatePost(@Valid @RequestBody PostSaveReqVO updateReqVO) { postService.updatePost(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除岗位") @PreAuthorize("@ss.hasPermission('system:post:delete')") public CommonResult deletePost(@RequestParam("id") Long id) { postService.deletePost(id); return success(true); } @GetMapping(value = "/get") @Operation(summary = "获得岗位信息") @Parameter(name = "id", description = "岗位编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:post:query')") public CommonResult getPost(@RequestParam("id") Long id) { PostDO post = postService.getPost(id); return success(BeanUtils.toBean(post, PostRespVO.class)); } @GetMapping(value = {"/list-all-simple", "simple-list"}) @Operation(summary = "获取岗位全列表", description = "只包含被开启的岗位,主要用于前端的下拉选项") public CommonResult> getSimplePostList() { // 获得岗位列表,只要开启状态的 List list = postService.getPostList(null, Collections.singleton(CommonStatusEnum.ENABLE.getStatus())); // 排序后,返回给前端 list.sort(Comparator.comparing(PostDO::getSort)); return success(BeanUtils.toBean(list, PostSimpleRespVO.class)); } @GetMapping("/page") @Operation(summary = "获得岗位分页列表") @PreAuthorize("@ss.hasPermission('system:post:query')") public CommonResult> getPostPage(@Validated PostPageReqVO pageReqVO) { PageResult pageResult = postService.getPostPage(pageReqVO); return success(BeanUtils.toBean(pageResult, PostRespVO.class)); } @GetMapping("/export") @Operation(summary = "岗位管理") @PreAuthorize("@ss.hasPermission('system:post:export')") @ApiAccessLog(operateType = EXPORT) public void export(HttpServletResponse response, @Validated PostPageReqVO reqVO) throws IOException { reqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = postService.getPostPage(reqVO).getList(); // 输出 ExcelUtils.write(response, "岗位数据.xls", "岗位列表", PostRespVO.class, BeanUtils.toBean(list, PostRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/dept/vo/dept/DeptListReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.dept.vo.dept; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "管理后台 - 部门列表 Request VO") @Data public class DeptListReqVO { @Schema(description = "部门名称,模糊匹配", example = "yshop") private String name; @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/dept/vo/dept/DeptRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.dept.vo.dept; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 部门信息 Response VO") @Data public class DeptRespVO { @Schema(description = "部门编号", example = "1024") private Long id; @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String name; @Schema(description = "父部门 ID", example = "1024") private Long parentId; @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Integer sort; @Schema(description = "负责人的用户编号", example = "2048") private Long leaderUserId; @Schema(description = "联系电话", example = "15601691000") private String phone; @Schema(description = "邮箱", example = "yshop@yixiang.co") private String email; @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer status; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/dept/vo/dept/DeptSaveReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.dept.vo.dept; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.validation.InEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @Schema(description = "管理后台 - 部门创建/修改 Request VO") @Data public class DeptSaveReqVO { @Schema(description = "部门编号", example = "1024") private Long id; @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @NotBlank(message = "部门名称不能为空") @Size(max = 30, message = "部门名称长度不能超过 30 个字符") private String name; @Schema(description = "父部门 ID", example = "1024") private Long parentId; @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "显示顺序不能为空") private Integer sort; @Schema(description = "负责人的用户编号", example = "2048") private Long leaderUserId; @Schema(description = "联系电话", example = "15601691000") @Size(max = 11, message = "联系电话长度不能超过11个字符") private String phone; @Schema(description = "邮箱", example = "yshop@yixiang.co") @Email(message = "邮箱格式不正确") @Size(max = 50, message = "邮箱长度不能超过 50 个字符") private String email; @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "状态不能为空") @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/dept/vo/dept/DeptSimpleRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.dept.vo.dept; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Schema(description = "管理后台 - 部门精简信息 Response VO") @Data @NoArgsConstructor @AllArgsConstructor public class DeptSimpleRespVO { @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String name; @Schema(description = "父部门 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long parentId; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/dept/vo/post/PostPageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.dept.vo.post; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; @Schema(description = "管理后台 - 岗位分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) public class PostPageReqVO extends PageParam { @Schema(description = "岗位编码,模糊匹配", example = "yshop") private String code; @Schema(description = "岗位名称,模糊匹配", example = "yshop") private String name; @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/dept/vo/post/PostRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.dept.vo.post; import co.yixiang.yshop.framework.excel.core.annotations.DictFormat; import co.yixiang.yshop.framework.excel.core.convert.DictConvert; import co.yixiang.yshop.module.system.enums.DictTypeConstants; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 岗位信息 Response VO") @Data @ExcelIgnoreUnannotated public class PostRespVO { @Schema(description = "岗位序号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @ExcelProperty("岗位序号") private Long id; @Schema(description = "岗位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小土豆") @ExcelProperty("岗位名称") private String name; @Schema(description = "岗位编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @ExcelProperty("岗位编码") private String code; @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @ExcelProperty("岗位排序") private Integer sort; @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty(value = "状态", converter = DictConvert.class) @DictFormat(DictTypeConstants.COMMON_STATUS) private Integer status; @Schema(description = "备注", example = "快乐的备注") private String remark; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/dept/vo/post/PostSaveReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.dept.vo.post; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.validation.InEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @Schema(description = "管理后台 - 岗位创建/修改 Request VO") @Data public class PostSaveReqVO { @Schema(description = "岗位编号", example = "1024") private Long id; @Schema(description = "岗位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小土豆") @NotBlank(message = "岗位名称不能为空") @Size(max = 50, message = "岗位名称长度不能超过 50 个字符") private String name; @Schema(description = "岗位编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @NotBlank(message = "岗位编码不能为空") @Size(max = 64, message = "岗位编码长度不能超过64个字符") private String code; @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "显示顺序不能为空") private Integer sort; @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @InEnum(CommonStatusEnum.class) private Integer status; @Schema(description = "备注", example = "快乐的备注") private String remark; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/dept/vo/post/PostSimpleRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.dept.vo.post; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "管理后台 - 岗位信息的精简 Response VO") @Data public class PostSimpleRespVO { @Schema(description = "岗位序号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @ExcelProperty("岗位序号") private Long id; @Schema(description = "岗位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小土豆") @ExcelProperty("岗位名称") private String name; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/dict/DictDataController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.dict; import co.yixiang.yshop.framework.apilog.core.annotation.ApiAccessLog; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.system.controller.admin.dict.vo.data.DictDataPageReqVO; import co.yixiang.yshop.module.system.controller.admin.dict.vo.data.DictDataRespVO; import co.yixiang.yshop.module.system.controller.admin.dict.vo.data.DictDataSaveReqVO; import co.yixiang.yshop.module.system.controller.admin.dict.vo.data.DictDataSimpleRespVO; import co.yixiang.yshop.module.system.dal.dataobject.dict.DictDataDO; import co.yixiang.yshop.module.system.service.dict.DictDataService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.io.IOException; import java.util.List; import static co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 字典数据") @RestController @RequestMapping("/system/dict-data") @Validated public class DictDataController { @Resource private DictDataService dictDataService; @PostMapping("/create") @Operation(summary = "新增字典数据") @PreAuthorize("@ss.hasPermission('system:dict:create')") public CommonResult createDictData(@Valid @RequestBody DictDataSaveReqVO createReqVO) { Long dictDataId = dictDataService.createDictData(createReqVO); return success(dictDataId); } @PutMapping("/update") @Operation(summary = "修改字典数据") @PreAuthorize("@ss.hasPermission('system:dict:update')") public CommonResult updateDictData(@Valid @RequestBody DictDataSaveReqVO updateReqVO) { dictDataService.updateDictData(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除字典数据") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:dict:delete')") public CommonResult deleteDictData(Long id) { dictDataService.deleteDictData(id); return success(true); } @GetMapping(value = {"/list-all-simple", "simple-list"}) @Operation(summary = "获得全部字典数据列表", description = "一般用于管理后台缓存字典数据在本地") // 无需添加权限认证,因为前端全局都需要 public CommonResult> getSimpleDictDataList() { List list = dictDataService.getDictDataList( CommonStatusEnum.ENABLE.getStatus(), null); return success(BeanUtils.toBean(list, DictDataSimpleRespVO.class)); } @GetMapping("/page") @Operation(summary = "/获得字典类型的分页列表") @PreAuthorize("@ss.hasPermission('system:dict:query')") public CommonResult> getDictTypePage(@Valid DictDataPageReqVO pageReqVO) { PageResult pageResult = dictDataService.getDictDataPage(pageReqVO); return success(BeanUtils.toBean(pageResult, DictDataRespVO.class)); } @GetMapping(value = "/get") @Operation(summary = "/查询字典数据详细") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:dict:query')") public CommonResult getDictData(@RequestParam("id") Long id) { DictDataDO dictData = dictDataService.getDictData(id); return success(BeanUtils.toBean(dictData, DictDataRespVO.class)); } @GetMapping("/export") @Operation(summary = "导出字典数据") @PreAuthorize("@ss.hasPermission('system:dict:export')") @ApiAccessLog(operateType = EXPORT) public void export(HttpServletResponse response, @Valid DictDataPageReqVO exportReqVO) throws IOException { exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = dictDataService.getDictDataPage(exportReqVO).getList(); // 输出 ExcelUtils.write(response, "字典数据.xls", "数据", DictDataRespVO.class, BeanUtils.toBean(list, DictDataRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/dict/DictTypeController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.dict; import co.yixiang.yshop.framework.apilog.core.annotation.ApiAccessLog; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.system.controller.admin.dict.vo.type.DictTypePageReqVO; import co.yixiang.yshop.module.system.controller.admin.dict.vo.type.DictTypeRespVO; import co.yixiang.yshop.module.system.controller.admin.dict.vo.type.DictTypeSaveReqVO; import co.yixiang.yshop.module.system.controller.admin.dict.vo.type.DictTypeSimpleRespVO; import co.yixiang.yshop.module.system.dal.dataobject.dict.DictTypeDO; import co.yixiang.yshop.module.system.service.dict.DictTypeService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.io.IOException; import java.util.List; import static co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 字典类型") @RestController @RequestMapping("/system/dict-type") @Validated public class DictTypeController { @Resource private DictTypeService dictTypeService; @PostMapping("/create") @Operation(summary = "创建字典类型") @PreAuthorize("@ss.hasPermission('system:dict:create')") public CommonResult createDictType(@Valid @RequestBody DictTypeSaveReqVO createReqVO) { Long dictTypeId = dictTypeService.createDictType(createReqVO); return success(dictTypeId); } @PutMapping("/update") @Operation(summary = "修改字典类型") @PreAuthorize("@ss.hasPermission('system:dict:update')") public CommonResult updateDictType(@Valid @RequestBody DictTypeSaveReqVO updateReqVO) { dictTypeService.updateDictType(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除字典类型") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:dict:delete')") public CommonResult deleteDictType(Long id) { dictTypeService.deleteDictType(id); return success(true); } @GetMapping("/page") @Operation(summary = "获得字典类型的分页列表") @PreAuthorize("@ss.hasPermission('system:dict:query')") public CommonResult> pageDictTypes(@Valid DictTypePageReqVO pageReqVO) { PageResult pageResult = dictTypeService.getDictTypePage(pageReqVO); return success(BeanUtils.toBean(pageResult, DictTypeRespVO.class)); } @Operation(summary = "/查询字典类型详细") @Parameter(name = "id", description = "编号", required = true, example = "1024") @GetMapping(value = "/get") @PreAuthorize("@ss.hasPermission('system:dict:query')") public CommonResult getDictType(@RequestParam("id") Long id) { DictTypeDO dictType = dictTypeService.getDictType(id); return success(BeanUtils.toBean(dictType, DictTypeRespVO.class)); } @GetMapping(value = {"/list-all-simple", "simple-list"}) @Operation(summary = "获得全部字典类型列表", description = "包括开启 + 禁用的字典类型,主要用于前端的下拉选项") // 无需添加权限认证,因为前端全局都需要 public CommonResult> getSimpleDictTypeList() { List list = dictTypeService.getDictTypeList(); return success(BeanUtils.toBean(list, DictTypeSimpleRespVO.class)); } @Operation(summary = "导出数据类型") @GetMapping("/export") @PreAuthorize("@ss.hasPermission('system:dict:query')") @ApiAccessLog(operateType = EXPORT) public void export(HttpServletResponse response, @Valid DictTypePageReqVO exportReqVO) throws IOException { exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = dictTypeService.getDictTypePage(exportReqVO).getList(); // 导出 ExcelUtils.write(response, "字典类型.xls", "数据", DictTypeRespVO.class, BeanUtils.toBean(list, DictTypeRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/dict/vo/data/DictDataPageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.dict.vo.data; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.validation.InEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import jakarta.validation.constraints.Size; @Schema(description = "管理后台 - 字典类型分页列表 Request VO") @Data @EqualsAndHashCode(callSuper = true) public class DictDataPageReqVO extends PageParam { @Schema(description = "字典标签", example = "yshop") @Size(max = 100, message = "字典标签长度不能超过100个字符") private String label; @Schema(description = "字典类型,模糊匹配", example = "sys_common_sex") @Size(max = 100, message = "字典类型类型长度不能超过100个字符") private String dictType; @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/dict/vo/data/DictDataRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.dict.vo.data; import co.yixiang.yshop.framework.excel.core.annotations.DictFormat; import co.yixiang.yshop.framework.excel.core.convert.DictConvert; import co.yixiang.yshop.module.system.enums.DictTypeConstants; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 字典数据信息 Response VO") @Data @ExcelIgnoreUnannotated public class DictDataRespVO { @Schema(description = "字典数据编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @ExcelProperty("字典编码") private Long id; @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @ExcelProperty("字典排序") private Integer sort; @Schema(description = "字典标签", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @ExcelProperty("字典标签") private String label; @Schema(description = "字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "yixiang.co") @ExcelProperty("字典键值") private String value; @Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "sys_common_sex") @ExcelProperty("字典类型") private String dictType; @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty(value = "状态", converter = DictConvert.class) @DictFormat(DictTypeConstants.COMMON_STATUS) private Integer status; @Schema(description = "颜色类型,default、primary、success、info、warning、danger", example = "default") private String colorType; @Schema(description = "css 样式", example = "btn-visible") private String cssClass; @Schema(description = "备注", example = "我是一个角色") private String remark; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/dict/vo/data/DictDataSaveReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.dict.vo.data; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.validation.InEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @Schema(description = "管理后台 - 字典数据创建/修改 Request VO") @Data public class DictDataSaveReqVO { @Schema(description = "字典数据编号", example = "1024") private Long id; @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "显示顺序不能为空") private Integer sort; @Schema(description = "字典标签", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @NotBlank(message = "字典标签不能为空") @Size(max = 100, message = "字典标签长度不能超过100个字符") private String label; @Schema(description = "字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "yixiang.co") @NotBlank(message = "字典键值不能为空") @Size(max = 100, message = "字典键值长度不能超过100个字符") private String value; @Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "sys_common_sex") @NotBlank(message = "字典类型不能为空") @Size(max = 100, message = "字典类型长度不能超过100个字符") private String dictType; @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "状态不能为空") @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") private Integer status; @Schema(description = "颜色类型,default、primary、success、info、warning、danger", example = "default") private String colorType; @Schema(description = "css 样式", example = "btn-visible") private String cssClass; @Schema(description = "备注", example = "我是一个角色") private String remark; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/dict/vo/data/DictDataSimpleRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.dict.vo.data; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "管理后台 - 数据字典精简 Response VO") @Data public class DictDataSimpleRespVO { @Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "gender") private String dictType; @Schema(description = "字典键值", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private String value; @Schema(description = "字典标签", requiredMode = Schema.RequiredMode.REQUIRED, example = "男") private String label; @Schema(description = "颜色类型,default、primary、success、info、warning、danger", example = "default") private String colorType; @Schema(description = "css 样式", example = "btn-visible") private String cssClass; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/dict/vo/type/DictTypePageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.dict.vo.type; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.springframework.format.annotation.DateTimeFormat; import jakarta.validation.constraints.Size; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 字典类型分页列表 Request VO") @Data @EqualsAndHashCode(callSuper = true) public class DictTypePageReqVO extends PageParam { @Schema(description = "字典类型名称,模糊匹配", example = "yshop") private String name; @Schema(description = "字典类型,模糊匹配", example = "sys_common_sex") @Size(max = 100, message = "字典类型类型长度不能超过100个字符") private String type; @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") private Integer status; @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @Schema(description = "创建时间") private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/dict/vo/type/DictTypeRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.dict.vo.type; import co.yixiang.yshop.framework.excel.core.annotations.DictFormat; import co.yixiang.yshop.framework.excel.core.convert.DictConvert; import co.yixiang.yshop.module.system.enums.DictTypeConstants; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 字典类型信息 Response VO") @Data @ExcelIgnoreUnannotated public class DictTypeRespVO { @Schema(description = "字典类型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @ExcelProperty("字典主键") private Long id; @Schema(description = "字典名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "性别") @ExcelProperty("字典名称") private String name; @Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "sys_common_sex") @ExcelProperty("字典类型") private String type; @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty(value = "状态", converter = DictConvert.class) @DictFormat(DictTypeConstants.COMMON_STATUS) private Integer status; @Schema(description = "备注", example = "快乐的备注") private String remark; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/dict/vo/type/DictTypeSaveReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.dict.vo.type; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @Schema(description = "管理后台 - 字典类型创建/修改 Request VO") @Data public class DictTypeSaveReqVO { @Schema(description = "字典类型编号", example = "1024") private Long id; @Schema(description = "字典名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "性别") @NotBlank(message = "字典名称不能为空") @Size(max = 100, message = "字典类型名称长度不能超过100个字符") private String name; @Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "sys_common_sex") @NotNull(message = "字典类型不能为空") @Size(max = 100, message = "字典类型类型长度不能超过 100 个字符") private String type; @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "状态不能为空") private Integer status; @Schema(description = "备注", example = "快乐的备注") private String remark; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/dict/vo/type/DictTypeSimpleRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.dict.vo.type; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "管理后台 - 字典类型精简信息 Response VO") @Data public class DictTypeSimpleRespVO { @Schema(description = "字典类型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "字典类型名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String name; @Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "sys_common_sex") private String type; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/ip/AreaController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.ip; import cn.hutool.core.lang.Assert; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.ip.core.Area; import co.yixiang.yshop.framework.ip.core.utils.AreaUtils; import co.yixiang.yshop.framework.ip.core.utils.IPUtils; import co.yixiang.yshop.module.system.controller.admin.ip.vo.AreaNodeRespVO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 地区") @RestController @RequestMapping("/system/area") @Validated public class AreaController { @GetMapping("/tree") @Operation(summary = "获得地区树") public CommonResult> getAreaTree() { Area area = AreaUtils.getArea(Area.ID_CHINA); Assert.notNull(area, "获取不到中国"); return success(BeanUtils.toBean(area.getChildren(), AreaNodeRespVO.class)); } @GetMapping("/get-by-ip") @Operation(summary = "获得 IP 对应的地区名") @Parameter(name = "ip", description = "IP", required = true) public CommonResult getAreaByIp(@RequestParam("ip") String ip) { // 获得城市 Area area = IPUtils.getArea(ip); if (area == null) { return success("未知"); } // 格式化返回 return success(AreaUtils.format(area.getId())); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/ip/vo/AreaNodeRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.ip.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.List; @Schema(description = "管理后台 - 地区节点 Response VO") @Data public class AreaNodeRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "110000") private Integer id; @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "北京") private String name; /** * 子节点 */ private List children; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/logger/LoginLogController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.logger; import co.yixiang.yshop.framework.apilog.core.annotation.ApiAccessLog; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.system.controller.admin.logger.vo.loginlog.LoginLogPageReqVO; import co.yixiang.yshop.module.system.controller.admin.logger.vo.loginlog.LoginLogRespVO; import co.yixiang.yshop.module.system.dal.dataobject.logger.LoginLogDO; import co.yixiang.yshop.module.system.service.logger.LoginLogService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; 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.io.IOException; import java.util.List; import static co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 登录日志") @RestController @RequestMapping("/system/login-log") @Validated public class LoginLogController { @Resource private LoginLogService loginLogService; @GetMapping("/page") @Operation(summary = "获得登录日志分页列表") @PreAuthorize("@ss.hasPermission('system:login-log:query')") public CommonResult> getLoginLogPage(@Valid LoginLogPageReqVO pageReqVO) { PageResult pageResult = loginLogService.getLoginLogPage(pageReqVO); return success(BeanUtils.toBean(pageResult, LoginLogRespVO.class)); } @GetMapping("/export") @Operation(summary = "导出登录日志 Excel") @PreAuthorize("@ss.hasPermission('system:login-log:export')") @ApiAccessLog(operateType = EXPORT) public void exportLoginLog(HttpServletResponse response, @Valid LoginLogPageReqVO exportReqVO) throws IOException { exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = loginLogService.getLoginLogPage(exportReqVO).getList(); // 输出 ExcelUtils.write(response, "登录日志.xls", "数据列表", LoginLogRespVO.class, BeanUtils.toBean(list, LoginLogRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/logger/OperateLogController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.logger; import co.yixiang.yshop.framework.apilog.core.annotation.ApiAccessLog; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.framework.translate.core.TranslateUtils; import co.yixiang.yshop.module.system.controller.admin.logger.vo.operatelog.OperateLogPageReqVO; import co.yixiang.yshop.module.system.controller.admin.logger.vo.operatelog.OperateLogRespVO; import co.yixiang.yshop.module.system.dal.dataobject.logger.OperateLogDO; import co.yixiang.yshop.module.system.service.logger.OperateLogService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; 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.io.IOException; import java.util.List; import static co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 操作日志") @RestController @RequestMapping("/system/operate-log") @Validated public class OperateLogController { @Resource private OperateLogService operateLogService; @GetMapping("/page") @Operation(summary = "查看操作日志分页列表") @PreAuthorize("@ss.hasPermission('system:operate-log:query')") public CommonResult> pageOperateLog(@Valid OperateLogPageReqVO pageReqVO) { PageResult pageResult = operateLogService.getOperateLogPage(pageReqVO); return success(BeanUtils.toBean(pageResult, OperateLogRespVO.class)); } @Operation(summary = "导出操作日志") @GetMapping("/export") @PreAuthorize("@ss.hasPermission('system:operate-log:export')") @ApiAccessLog(operateType = EXPORT) public void exportOperateLog(HttpServletResponse response, @Valid OperateLogPageReqVO exportReqVO) throws IOException { exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = operateLogService.getOperateLogPage(exportReqVO).getList(); ExcelUtils.write(response, "操作日志.xls", "数据列表", OperateLogRespVO.class, TranslateUtils.translate(BeanUtils.toBean(list, OperateLogRespVO.class))); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/logger/vo/loginlog/LoginLogPageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.logger.vo.loginlog; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 登录日志分页列表 Request VO") @Data @EqualsAndHashCode(callSuper = true) public class LoginLogPageReqVO extends PageParam { @Schema(description = "用户 IP,模拟匹配", example = "127.0.0.1") private String userIp; @Schema(description = "用户账号,模拟匹配", example = "yshop") private String username; @Schema(description = "操作状态", example = "true") private Boolean status; @Schema(description = "登录时间", example = "[2022-07-01 00:00:00,2022-07-01 23:59:59]") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/logger/vo/loginlog/LoginLogRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.logger.vo.loginlog; import co.yixiang.yshop.framework.excel.core.annotations.DictFormat; import co.yixiang.yshop.framework.excel.core.convert.DictConvert; import co.yixiang.yshop.module.system.enums.DictTypeConstants; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 登录日志 Response VO") @Data @ExcelIgnoreUnannotated public class LoginLogRespVO { @Schema(description = "日志编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @ExcelProperty("日志主键") private Long id; @Schema(description = "日志类型,参见 LoginLogTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty(value = "日志类型", converter = DictConvert.class) @DictFormat(DictTypeConstants.LOGIN_TYPE) private Integer logType; @Schema(description = "用户编号", example = "666") private Long userId; @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") private Integer userType; @Schema(description = "链路追踪编号", example = "89aca178-a370-411c-ae02-3f0d672be4ab") private String traceId; @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @ExcelProperty("用户账号") private String username; @Schema(description = "登录结果,参见 LoginResultEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty(value = "登录结果", converter = DictConvert.class) @DictFormat(DictTypeConstants.LOGIN_RESULT) private Integer result; @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") @ExcelProperty("登录 IP") private String userIp; @Schema(description = "浏览器 UserAgent", example = "Mozilla/5.0") @ExcelProperty("浏览器 UA") private String userAgent; @Schema(description = "登录时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("登录时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/logger/vo/operatelog/OperateLogPageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.logger.vo.operatelog; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 操作日志分页列表 Request VO") @Data public class OperateLogPageReqVO extends PageParam { @Schema(description = "用户编号", example = "yshop") private Long userId; @Schema(description = "操作模块业务编号", example = "1") private Long bizId; @Schema(description = "操作模块,模拟匹配", example = "订单") private String type; @Schema(description = "操作名,模拟匹配", example = "创建订单") private String subType; @Schema(description = "操作明细,模拟匹配", example = "修改编号为 1 的用户信息") private String action; @Schema(description = "开始时间", example = "[2022-07-01 00:00:00,2022-07-01 23:59:59]") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.logger.vo.operatelog; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import com.fhs.core.trans.anno.Trans; import com.fhs.core.trans.constant.TransType; import com.fhs.core.trans.vo.VO; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 操作日志 Response VO") @Data @ExcelIgnoreUnannotated public class OperateLogRespVO implements VO { @Schema(description = "日志编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @ExcelProperty("日志编号") private Long id; @Schema(description = "链路追踪编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "89aca178-a370-411c-ae02-3f0d672be4ab") private String traceId; @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @Trans(type = TransType.SIMPLE, target = AdminUserDO.class, fields = "nickname", ref = "userName") private Long userId; @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @ExcelProperty("操作人") private String userName; @Schema(description = "操作模块类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "订单") @ExcelProperty("操作模块类型") private String type; @Schema(description = "操作名", requiredMode = Schema.RequiredMode.REQUIRED, example = "创建订单") @ExcelProperty("操作名") private String subType; @Schema(description = "操作模块业务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty("操作模块业务编号") private Long bizId; @Schema(description = "操作明细", example = "修改编号为 1 的用户信息,将性别从男改成女,将姓名从yshop改成源码。") private String action; @Schema(description = "拓展字段", example = "{'orderId': 1}") private String extra; @Schema(description = "请求方法名", requiredMode = Schema.RequiredMode.REQUIRED, example = "GET") @NotEmpty(message = "请求方法名不能为空") private String requestMethod; @Schema(description = "请求地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "/xxx/yyy") private String requestUrl; @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") private String userIp; @Schema(description = "浏览器 UserAgent", requiredMode = Schema.RequiredMode.REQUIRED, example = "Mozilla/5.0") private String userAgent; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/mail/MailAccountController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.mail; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.mail.vo.account.MailAccountPageReqVO; import co.yixiang.yshop.module.system.controller.admin.mail.vo.account.MailAccountRespVO; import co.yixiang.yshop.module.system.controller.admin.mail.vo.account.MailAccountSaveReqVO; import co.yixiang.yshop.module.system.controller.admin.mail.vo.account.MailAccountSimpleRespVO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailAccountDO; import co.yixiang.yshop.module.system.service.mail.MailAccountService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 邮箱账号") @RestController @RequestMapping("/system/mail-account") public class MailAccountController { @Resource private MailAccountService mailAccountService; @PostMapping("/create") @Operation(summary = "创建邮箱账号") @PreAuthorize("@ss.hasPermission('system:mail-account:create')") public CommonResult createMailAccount(@Valid @RequestBody MailAccountSaveReqVO createReqVO) { return success(mailAccountService.createMailAccount(createReqVO)); } @PutMapping("/update") @Operation(summary = "修改邮箱账号") @PreAuthorize("@ss.hasPermission('system:mail-account:update')") public CommonResult updateMailAccount(@Valid @RequestBody MailAccountSaveReqVO updateReqVO) { mailAccountService.updateMailAccount(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除邮箱账号") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('system:mail-account:delete')") public CommonResult deleteMailAccount(@RequestParam Long id) { mailAccountService.deleteMailAccount(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得邮箱账号") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:mail-account:query')") public CommonResult getMailAccount(@RequestParam("id") Long id) { MailAccountDO account = mailAccountService.getMailAccount(id); return success(BeanUtils.toBean(account, MailAccountRespVO.class)); } @GetMapping("/page") @Operation(summary = "获得邮箱账号分页") @PreAuthorize("@ss.hasPermission('system:mail-account:query')") public CommonResult> getMailAccountPage(@Valid MailAccountPageReqVO pageReqVO) { PageResult pageResult = mailAccountService.getMailAccountPage(pageReqVO); return success(BeanUtils.toBean(pageResult, MailAccountRespVO.class)); } @GetMapping({"/list-all-simple", "simple-list"}) @Operation(summary = "获得邮箱账号精简列表") public CommonResult> getSimpleMailAccountList() { List list = mailAccountService.getMailAccountList(); return success(BeanUtils.toBean(list, MailAccountSimpleRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/mail/MailLogController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.mail; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.mail.vo.log.MailLogPageReqVO; import co.yixiang.yshop.module.system.controller.admin.mail.vo.log.MailLogRespVO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailLogDO; import co.yixiang.yshop.module.system.service.mail.MailLogService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import jakarta.annotation.Resource; import jakarta.validation.Valid; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 邮件日志") @RestController @RequestMapping("/system/mail-log") public class MailLogController { @Resource private MailLogService mailLogService; @GetMapping("/page") @Operation(summary = "获得邮箱日志分页") @PreAuthorize("@ss.hasPermission('system:mail-log:query')") public CommonResult> getMailLogPage(@Valid MailLogPageReqVO pageVO) { PageResult pageResult = mailLogService.getMailLogPage(pageVO); return success(BeanUtils.toBean(pageResult, MailLogRespVO.class)); } @GetMapping("/get") @Operation(summary = "获得邮箱日志") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:mail-log:query')") public CommonResult getMailTemplate(@RequestParam("id") Long id) { MailLogDO log = mailLogService.getMailLog(id); return success(BeanUtils.toBean(log, MailLogRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/mail/MailTemplateController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.mail; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.mail.vo.template.*; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailTemplateDO; import co.yixiang.yshop.module.system.service.mail.MailSendService; import co.yixiang.yshop.module.system.service.mail.MailTemplateService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @Tag(name = "管理后台 - 邮件模版") @RestController @RequestMapping("/system/mail-template") public class MailTemplateController { @Resource private MailTemplateService mailTempleService; @Resource private MailSendService mailSendService; @PostMapping("/create") @Operation(summary = "创建邮件模版") @PreAuthorize("@ss.hasPermission('system:mail-template:create')") public CommonResult createMailTemplate(@Valid @RequestBody MailTemplateSaveReqVO createReqVO){ return success(mailTempleService.createMailTemplate(createReqVO)); } @PutMapping("/update") @Operation(summary = "修改邮件模版") @PreAuthorize("@ss.hasPermission('system:mail-template:update')") public CommonResult updateMailTemplate(@Valid @RequestBody MailTemplateSaveReqVO updateReqVO){ mailTempleService.updateMailTemplate(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除邮件模版") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:mail-template:delete')") public CommonResult deleteMailTemplate(@RequestParam("id") Long id) { mailTempleService.deleteMailTemplate(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得邮件模版") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:mail-template:query')") public CommonResult getMailTemplate(@RequestParam("id") Long id) { MailTemplateDO template = mailTempleService.getMailTemplate(id); return success(BeanUtils.toBean(template, MailTemplateRespVO.class)); } @GetMapping("/page") @Operation(summary = "获得邮件模版分页") @PreAuthorize("@ss.hasPermission('system:mail-template:query')") public CommonResult> getMailTemplatePage(@Valid MailTemplatePageReqVO pageReqVO) { PageResult pageResult = mailTempleService.getMailTemplatePage(pageReqVO); return success(BeanUtils.toBean(pageResult, MailTemplateRespVO.class)); } @GetMapping({"/list-all-simple", "simple-list"}) @Operation(summary = "获得邮件模版精简列表") public CommonResult> getSimpleTemplateList() { List list = mailTempleService.getMailTemplateList(); return success(BeanUtils.toBean(list, MailTemplateSimpleRespVO.class)); } @PostMapping("/send-mail") @Operation(summary = "发送短信") @PreAuthorize("@ss.hasPermission('system:mail-template:send-mail')") public CommonResult sendMail(@Valid @RequestBody MailTemplateSendReqVO sendReqVO) { return success(mailSendService.sendSingleMailToAdmin(sendReqVO.getMail(), getLoginUserId(), sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams())); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/mail/vo/account/MailAccountPageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.mail.vo.account; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @Schema(description = "管理后台 - 邮箱账号分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MailAccountPageReqVO extends PageParam { @Schema(description = "邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshopyuanma@123.com") private String mail; @Schema(description = "用户名" , requiredMode = Schema.RequiredMode.REQUIRED , example = "yshop") private String username; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/mail/vo/account/MailAccountRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.mail.vo.account; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 邮箱账号 Response VO") @Data public class MailAccountRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshopyuanma@123.com") private String mail; @Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String username; @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") private String password; @Schema(description = "SMTP 服务器域名", requiredMode = Schema.RequiredMode.REQUIRED, example = "www.yixiang.co") private String host; @Schema(description = "SMTP 服务器端口", requiredMode = Schema.RequiredMode.REQUIRED, example = "80") private Integer port; @Schema(description = "是否开启 ssl", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") private Boolean sslEnable; @Schema(description = "是否开启 starttls", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") private Boolean starttlsEnable; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/mail/vo/account/MailAccountSaveReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.mail.vo.account; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 邮箱账号创建/修改 Request VO") @Data public class MailAccountSaveReqVO { @Schema(description = "编号", example = "1024") private Long id; @Schema(description = "邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshopyuanma@123.com") @NotNull(message = "邮箱不能为空") @Email(message = "必须是 Email 格式") private String mail; @Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @NotNull(message = "用户名不能为空") private String username; @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") @NotNull(message = "密码必填") private String password; @Schema(description = "SMTP 服务器域名", requiredMode = Schema.RequiredMode.REQUIRED, example = "www.yixiang.co") @NotNull(message = "SMTP 服务器域名不能为空") private String host; @Schema(description = "SMTP 服务器端口", requiredMode = Schema.RequiredMode.REQUIRED, example = "80") @NotNull(message = "SMTP 服务器端口不能为空") private Integer port; @Schema(description = "是否开启 ssl", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") @NotNull(message = "是否开启 ssl 必填") private Boolean sslEnable; @Schema(description = "是否开启 starttls", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") @NotNull(message = "是否开启 starttls 必填") private Boolean starttlsEnable; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/mail/vo/account/MailAccountSimpleRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.mail.vo.account; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "管理后台 - 邮箱账号的精简 Response VO") @Data public class MailAccountSimpleRespVO { @Schema(description = "邮箱编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "768541388@qq.com") private String mail; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/mail/vo/log/MailLogPageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.mail.vo.log; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 邮箱日志分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MailLogPageReqVO extends PageParam { @Schema(description = "用户编号", example = "30883") private Long userId; @Schema(description = "用户类型,参见 UserTypeEnum 枚举", example = "2") private Integer userType; @Schema(description = "接收邮箱地址,模糊匹配", example = "76854@qq.com") private String toMail; @Schema(description = "邮箱账号编号", example = "18107") private Long accountId; @Schema(description = "模板编号", example = "5678") private Long templateId; @Schema(description = "发送状态,参见 MailSendStatusEnum 枚举", example = "1") private Integer sendStatus; @Schema(description = "发送时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] sendTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/mail/vo/log/MailLogRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.mail.vo.log; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import java.util.Map; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 邮件日志 Response VO") @Data public class MailLogRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "31020") private Long id; @Schema(description = "用户编号", example = "30883") private Long userId; @Schema(description = "用户类型,参见 UserTypeEnum 枚举", example = "2") private Byte userType; @Schema(description = "接收邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "76854@qq.com") private String toMail; @Schema(description = "邮箱账号编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18107") private Long accountId; @Schema(description = "发送邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "85757@qq.com") private String fromMail; @Schema(description = "模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5678") private Long templateId; @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01") private String templateCode; @Schema(description = "模版发送人名称", example = "李四") private String templateNickname; @Schema(description = "邮件标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试标题") private String templateTitle; @Schema(description = "邮件内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试内容") private String templateContent; @Schema(description = "邮件参数", requiredMode = Schema.RequiredMode.REQUIRED) private Map templateParams; @Schema(description = "发送状态,参见 MailSendStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Byte sendStatus; @Schema(description = "发送时间") private LocalDateTime sendTime; @Schema(description = "发送返回的消息 ID", example = "28568") private String sendMessageId; @Schema(description = "发送异常") private String sendException; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/mail/vo/template/MailTemplatePageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.mail.vo.template; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 邮件模版分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class MailTemplatePageReqVO extends PageParam { @Schema(description = "状态,参见 CommonStatusEnum 枚举", example = "1") private Integer status; @Schema(description = "标识,模糊匹配", example = "code_1024") private String code; @Schema(description = "名称,模糊匹配", example = "芋头") private String name; @Schema(description = "账号编号", example = "2048") private Long accountId; @Schema(description = "创建时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/mail/vo/template/MailTemplateRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.mail.vo.template; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; import java.util.List; @Schema(description = "管理后台 - 邮件末班 Response VO") @Data public class MailTemplateRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "模版名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试名字") private String name; @Schema(description = "模版编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "test") private String code; @Schema(description = "发送的邮箱账号编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long accountId; @Schema(description = "发送人名称", example = "芋头") private String nickname; @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "注册成功") private String title; @Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,注册成功啦") private String content; @Schema(description = "参数数组", example = "name,code") private List params; @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer status; @Schema(description = "备注", example = "奥特曼") private String remark; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/mail/vo/template/MailTemplateSaveReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.mail.vo.template; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 邮件模版创建/修改 Request VO") @Data public class MailTemplateSaveReqVO { @Schema(description = "编号", example = "1024") private Long id; @Schema(description = "模版名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试名字") @NotNull(message = "名称不能为空") private String name; @Schema(description = "模版编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "test") @NotNull(message = "模版编号不能为空") private String code; @Schema(description = "发送的邮箱账号编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "发送的邮箱账号编号不能为空") private Long accountId; @Schema(description = "发送人名称", example = "芋头") private String nickname; @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "注册成功") @NotEmpty(message = "标题不能为空") private String title; @Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,注册成功啦") @NotEmpty(message = "内容不能为空") private String content; @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "状态不能为空") private Integer status; @Schema(description = "备注", example = "奥特曼") private String remark; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/mail/vo/template/MailTemplateSendReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.mail.vo.template; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.util.Map; @Schema(description = "管理后台 - 邮件发送 Req VO") @Data public class MailTemplateSendReqVO { @Schema(description = "接收邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "7685413@qq.com") @NotEmpty(message = "接收邮箱不能为空") private String mail; @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01") @NotNull(message = "模板编码不能为空") private String templateCode; @Schema(description = "模板参数") private Map templateParams; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/mail/vo/template/MailTemplateSimpleRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.mail.vo.template; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "管理后台 - 邮件模版的精简 Response VO") @Data public class MailTemplateSimpleRespVO { @Schema(description = "模版编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "模版名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "哒哒哒") private String name; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/notice/NoticeController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.notice; import cn.hutool.core.lang.Assert; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.infra.api.websocket.WebSocketSenderApi; import co.yixiang.yshop.module.system.controller.admin.notice.vo.NoticePageReqVO; import co.yixiang.yshop.module.system.controller.admin.notice.vo.NoticeRespVO; import co.yixiang.yshop.module.system.controller.admin.notice.vo.NoticeSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.notice.NoticeDO; import co.yixiang.yshop.module.system.service.notice.NoticeService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 通知公告") @RestController @RequestMapping("/system/notice") @Validated public class NoticeController { @Resource private NoticeService noticeService; @Resource private WebSocketSenderApi webSocketSenderApi; @PostMapping("/create") @Operation(summary = "创建通知公告") @PreAuthorize("@ss.hasPermission('system:notice:create')") public CommonResult createNotice(@Valid @RequestBody NoticeSaveReqVO createReqVO) { Long noticeId = noticeService.createNotice(createReqVO); return success(noticeId); } @PutMapping("/update") @Operation(summary = "修改通知公告") @PreAuthorize("@ss.hasPermission('system:notice:update')") public CommonResult updateNotice(@Valid @RequestBody NoticeSaveReqVO updateReqVO) { noticeService.updateNotice(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除通知公告") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:notice:delete')") public CommonResult deleteNotice(@RequestParam("id") Long id) { noticeService.deleteNotice(id); return success(true); } @GetMapping("/page") @Operation(summary = "获取通知公告列表") @PreAuthorize("@ss.hasPermission('system:notice:query')") public CommonResult> getNoticePage(@Validated NoticePageReqVO pageReqVO) { PageResult pageResult = noticeService.getNoticePage(pageReqVO); return success(BeanUtils.toBean(pageResult, NoticeRespVO.class)); } @GetMapping("/get") @Operation(summary = "获得通知公告") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:notice:query')") public CommonResult getNotice(@RequestParam("id") Long id) { NoticeDO notice = noticeService.getNotice(id); return success(BeanUtils.toBean(notice, NoticeRespVO.class)); } @PostMapping("/push") @Operation(summary = "推送通知公告", description = "只发送给 websocket 连接在线的用户") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:notice:update')") public CommonResult push(@RequestParam("id") Long id) { NoticeDO notice = noticeService.getNotice(id); Assert.notNull(notice, "公告不能为空"); // 通过 websocket 推送给在线的用户 webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), "notice-push", notice); return success(true); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/notice/vo/NoticePageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.notice.vo; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; @Schema(description = "管理后台 - 通知公告分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) public class NoticePageReqVO extends PageParam { @Schema(description = "通知公告名称,模糊匹配", example = "yshop") private String title; @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/notice/vo/NoticeRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.notice.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 通知公告信息 Response VO") @Data public class NoticeRespVO { @Schema(description = "通知公告序号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "公告标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "小博主") private String title; private String picUrl; @Schema(description = "公告类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "小博主") private Integer type; @Schema(description = "公告内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "半生编码") private String content; @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer status; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/notice/vo/NoticeSaveReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.notice.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @Schema(description = "管理后台 - 通知公告创建/修改 Request VO") @Data public class NoticeSaveReqVO { @Schema(description = "岗位公告编号", example = "1024") private Long id; @Schema(description = "公告标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "小博主") @NotBlank(message = "公告标题不能为空") @Size(max = 50, message = "公告标题不能超过50个字符") private String title; @Schema(description = "公告类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "小博主") @NotNull(message = "公告类型不能为空") private Integer type; @Schema(description = "公告内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "半生编码") private String content; @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/notify/NotifyMessageController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.notify; import co.yixiang.yshop.framework.apilog.core.annotation.ApiAccessLog; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.notify.vo.message.NotifyMessageMyPageReqVO; import co.yixiang.yshop.module.system.controller.admin.notify.vo.message.NotifyMessagePageReqVO; import co.yixiang.yshop.module.system.controller.admin.notify.vo.message.NotifyMessageRespVO; import co.yixiang.yshop.module.system.dal.dataobject.notify.NotifyMessageDO; import co.yixiang.yshop.module.system.service.notify.NotifyMessageService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @Tag(name = "管理后台 - 我的站内信") @RestController @RequestMapping("/system/notify-message") @Validated public class NotifyMessageController { @Resource private NotifyMessageService notifyMessageService; // ========== 管理所有的站内信 ========== @GetMapping("/get") @Operation(summary = "获得站内信") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:notify-message:query')") public CommonResult getNotifyMessage(@RequestParam("id") Long id) { NotifyMessageDO message = notifyMessageService.getNotifyMessage(id); return success(BeanUtils.toBean(message, NotifyMessageRespVO.class)); } @GetMapping("/page") @Operation(summary = "获得站内信分页") @PreAuthorize("@ss.hasPermission('system:notify-message:query')") public CommonResult> getNotifyMessagePage(@Valid NotifyMessagePageReqVO pageVO) { PageResult pageResult = notifyMessageService.getNotifyMessagePage(pageVO); return success(BeanUtils.toBean(pageResult, NotifyMessageRespVO.class)); } // ========== 查看自己的站内信 ========== @GetMapping("/my-page") @Operation(summary = "获得我的站内信分页") public CommonResult> getMyMyNotifyMessagePage(@Valid NotifyMessageMyPageReqVO pageVO) { PageResult pageResult = notifyMessageService.getMyMyNotifyMessagePage(pageVO, getLoginUserId(), UserTypeEnum.ADMIN.getValue()); return success(BeanUtils.toBean(pageResult, NotifyMessageRespVO.class)); } @PutMapping("/update-read") @Operation(summary = "标记站内信为已读") @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") public CommonResult updateNotifyMessageRead(@RequestParam("ids") List ids) { notifyMessageService.updateNotifyMessageRead(ids, getLoginUserId(), UserTypeEnum.ADMIN.getValue()); return success(Boolean.TRUE); } @PutMapping("/update-all-read") @Operation(summary = "标记所有站内信为已读") public CommonResult updateAllNotifyMessageRead() { notifyMessageService.updateAllNotifyMessageRead(getLoginUserId(), UserTypeEnum.ADMIN.getValue()); return success(Boolean.TRUE); } @GetMapping("/get-unread-list") @Operation(summary = "获取当前用户的最新站内信列表,默认 10 条") @Parameter(name = "size", description = "10") public CommonResult> getUnreadNotifyMessageList( @RequestParam(name = "size", defaultValue = "10") Integer size) { List list = notifyMessageService.getUnreadNotifyMessageList( getLoginUserId(), UserTypeEnum.ADMIN.getValue(), size); return success(BeanUtils.toBean(list, NotifyMessageRespVO.class)); } @GetMapping("/get-unread-count") @Operation(summary = "获得当前用户的未读站内信数量") @ApiAccessLog(enable = false) // 由于前端会不断轮询该接口,记录日志没有意义 public CommonResult getUnreadNotifyMessageCount() { return success(notifyMessageService.getUnreadNotifyMessageCount( getLoginUserId(), UserTypeEnum.ADMIN.getValue())); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/notify/NotifyTemplateController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.notify; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.notify.vo.template.*; import co.yixiang.yshop.module.system.dal.dataobject.notify.NotifyTemplateDO; import co.yixiang.yshop.module.system.service.notify.NotifySendService; import co.yixiang.yshop.module.system.service.notify.NotifyTemplateService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 站内信模版") @RestController @RequestMapping("/system/notify-template") @Validated public class NotifyTemplateController { @Resource private NotifyTemplateService notifyTemplateService; @Resource private NotifySendService notifySendService; @PostMapping("/create") @Operation(summary = "创建站内信模版") @PreAuthorize("@ss.hasPermission('system:notify-template:create')") public CommonResult createNotifyTemplate(@Valid @RequestBody NotifyTemplateSaveReqVO createReqVO) { return success(notifyTemplateService.createNotifyTemplate(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新站内信模版") @PreAuthorize("@ss.hasPermission('system:notify-template:update')") public CommonResult updateNotifyTemplate(@Valid @RequestBody NotifyTemplateSaveReqVO updateReqVO) { notifyTemplateService.updateNotifyTemplate(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除站内信模版") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('system:notify-template:delete')") public CommonResult deleteNotifyTemplate(@RequestParam("id") Long id) { notifyTemplateService.deleteNotifyTemplate(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得站内信模版") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:notify-template:query')") public CommonResult getNotifyTemplate(@RequestParam("id") Long id) { NotifyTemplateDO template = notifyTemplateService.getNotifyTemplate(id); return success(BeanUtils.toBean(template, NotifyTemplateRespVO.class)); } @GetMapping("/page") @Operation(summary = "获得站内信模版分页") @PreAuthorize("@ss.hasPermission('system:notify-template:query')") public CommonResult> getNotifyTemplatePage(@Valid NotifyTemplatePageReqVO pageVO) { PageResult pageResult = notifyTemplateService.getNotifyTemplatePage(pageVO); return success(BeanUtils.toBean(pageResult, NotifyTemplateRespVO.class)); } @PostMapping("/send-notify") @Operation(summary = "发送站内信") @PreAuthorize("@ss.hasPermission('system:notify-template:send-notify')") public CommonResult sendNotify(@Valid @RequestBody NotifyTemplateSendReqVO sendReqVO) { if (UserTypeEnum.MEMBER.getValue().equals(sendReqVO.getUserType())) { return success(notifySendService.sendSingleNotifyToMember(sendReqVO.getUserId(), sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams())); } else { return success(notifySendService.sendSingleNotifyToAdmin(sendReqVO.getUserId(), sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams())); } } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/notify/vo/message/NotifyMessageMyPageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.notify.vo.message; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 站内信分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class NotifyMessageMyPageReqVO extends PageParam { @Schema(description = "是否已读", example = "true") private Boolean readStatus; @Schema(description = "创建时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/notify/vo/message/NotifyMessagePageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.notify.vo.message; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 站内信分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class NotifyMessagePageReqVO extends PageParam { @Schema(description = "用户编号", example = "25025") private Long userId; @Schema(description = "用户类型", example = "1") private Integer userType; @Schema(description = "模板编码", example = "test_01") private String templateCode; @Schema(description = "模版类型", example = "2") private Integer templateType; @Schema(description = "创建时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/notify/vo/message/NotifyMessageRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.notify.vo.message; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; import java.util.Map; @Schema(description = "管理后台 - 站内信 Response VO") @Data public class NotifyMessageRespVO { @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25025") private Long userId; @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Byte userType; @Schema(description = "模版编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13013") private Long templateId; @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01") private String templateCode; @Schema(description = "模版发送人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String templateNickname; @Schema(description = "模版内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试内容") private String templateContent; @Schema(description = "模版类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") private Integer templateType; @Schema(description = "模版参数", requiredMode = Schema.RequiredMode.REQUIRED) private Map templateParams; @Schema(description = "是否已读", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") private Boolean readStatus; @Schema(description = "阅读时间") private LocalDateTime readTime; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/notify/vo/template/NotifyTemplatePageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.notify.vo.template; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 站内信模版分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class NotifyTemplatePageReqVO extends PageParam { @Schema(description = "模版编码", example = "test_01") private String code; @Schema(description = "模版名称", example = "我是名称") private String name; @Schema(description = "状态,参见 CommonStatusEnum 枚举类", example = "1") private Integer status; @Schema(description = "创建时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/notify/vo/template/NotifyTemplateRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.notify.vo.template; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; import java.util.List; @Schema(description = "管理后台 - 站内信模版 Response VO") @Data public class NotifyTemplateRespVO { @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "模版名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试模版") private String name; @Schema(description = "模版编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "SEND_TEST") private String code; @Schema(description = "模版类型,对应 system_notify_template_type 字典", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer type; @Schema(description = "发送人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆") private String nickname; @Schema(description = "模版内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是模版内容") private String content; @Schema(description = "参数数组", example = "name,code") private List params; @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer status; @Schema(description = "备注", example = "我是备注") private String remark; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/notify/vo/template/NotifyTemplateSaveReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.notify.vo.template; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.validation.InEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 站内信模版创建/修改 Request VO") @Data public class NotifyTemplateSaveReqVO { @Schema(description = "ID", example = "1024") private Long id; @Schema(description = "模版名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试模版") @NotEmpty(message = "模版名称不能为空") private String name; @Schema(description = "模版编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "SEND_TEST") @NotNull(message = "模版编码不能为空") private String code; @Schema(description = "模版类型,对应 system_notify_template_type 字典", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "模版类型不能为空") private Integer type; @Schema(description = "发送人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆") @NotEmpty(message = "发送人名称不能为空") private String nickname; @Schema(description = "模版内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是模版内容") @NotEmpty(message = "模版内容不能为空") private String content; @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "状态不能为空") @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}") private Integer status; @Schema(description = "备注", example = "我是备注") private String remark; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/notify/vo/template/NotifyTemplateSendReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.notify.vo.template; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.util.Map; @Schema(description = "管理后台 - 站内信模板的发送 Request VO") @Data public class NotifyTemplateSendReqVO { @Schema(description = "用户id", requiredMode = Schema.RequiredMode.REQUIRED, example = "01") @NotNull(message = "用户id不能为空") private Long userId; @Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "用户类型不能为空") private Integer userType; @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "01") @NotEmpty(message = "模板编码不能为空") private String templateCode; @Schema(description = "模板参数") private Map templateParams; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/oauth2/OAuth2ClientController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.oauth2; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.client.OAuth2ClientRespVO; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.client.OAuth2ClientSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2ClientDO; import co.yixiang.yshop.module.system.service.oauth2.OAuth2ClientService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - OAuth2 客户端") @RestController @RequestMapping("/system/oauth2-client") @Validated public class OAuth2ClientController { @Resource private OAuth2ClientService oAuth2ClientService; @PostMapping("/create") @Operation(summary = "创建 OAuth2 客户端") @PreAuthorize("@ss.hasPermission('system:oauth2-client:create')") public CommonResult createOAuth2Client(@Valid @RequestBody OAuth2ClientSaveReqVO createReqVO) { return success(oAuth2ClientService.createOAuth2Client(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新 OAuth2 客户端") @PreAuthorize("@ss.hasPermission('system:oauth2-client:update')") public CommonResult updateOAuth2Client(@Valid @RequestBody OAuth2ClientSaveReqVO updateReqVO) { oAuth2ClientService.updateOAuth2Client(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除 OAuth2 客户端") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('system:oauth2-client:delete')") public CommonResult deleteOAuth2Client(@RequestParam("id") Long id) { oAuth2ClientService.deleteOAuth2Client(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得 OAuth2 客户端") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:oauth2-client:query')") public CommonResult getOAuth2Client(@RequestParam("id") Long id) { OAuth2ClientDO client = oAuth2ClientService.getOAuth2Client(id); return success(BeanUtils.toBean(client, OAuth2ClientRespVO.class)); } @GetMapping("/page") @Operation(summary = "获得 OAuth2 客户端分页") @PreAuthorize("@ss.hasPermission('system:oauth2-client:query')") public CommonResult> getOAuth2ClientPage(@Valid OAuth2ClientPageReqVO pageVO) { PageResult pageResult = oAuth2ClientService.getOAuth2ClientPage(pageVO); return success(BeanUtils.toBean(pageResult, OAuth2ClientRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/oauth2/OAuth2OpenController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.oauth2; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.util.http.HttpUtils; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAuthorizeInfoRespVO; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO; import co.yixiang.yshop.module.system.convert.oauth2.OAuth2OpenConvert; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2ClientDO; import co.yixiang.yshop.module.system.enums.oauth2.OAuth2GrantTypeEnum; import co.yixiang.yshop.module.system.service.oauth2.OAuth2ApproveService; import co.yixiang.yshop.module.system.service.oauth2.OAuth2ClientService; import co.yixiang.yshop.module.system.service.oauth2.OAuth2GrantService; import co.yixiang.yshop.module.system.service.oauth2.OAuth2TokenService; import co.yixiang.yshop.module.system.util.oauth2.OAuth2Utils; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.Operation; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.annotation.security.PermitAll; import jakarta.servlet.http.HttpServletRequest; import java.util.Collections; import java.util.List; import java.util.Map; import static co.yixiang.yshop.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception0; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertList; import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; /** * 提供给外部应用调用为主 * * 一般来说,管理后台的 /system-api/* 是不直接提供给外部应用使用,主要是外部应用能够访问的数据与接口是有限的,而管理后台的 RBAC 无法很好的控制。 * 参考大量的开放平台,都是独立的一套 OpenAPI,对应到【本系统】就是在 Controller 下新建 open 包,实现 /open-api/* 接口,然后通过 scope 进行控制。 * 另外,一个公司如果有多个管理后台,它们 client_id 产生的 access token 相互之间是无法互通的,即无法访问它们系统的 API 接口,直到两个 client_id 产生信任授权。 * * 考虑到【本系统】暂时不想做的过于复杂,默认只有获取到 access token 之后,可以访问【本系统】管理后台的 /system-api/* 所有接口,除非手动添加 scope 控制。 * scope 的使用示例,可见 {@link OAuth2UserController} 类 * * @author yshop */ @Tag(name = "管理后台 - OAuth2.0 授权") @RestController @RequestMapping("/system/oauth2") @Validated @Slf4j public class OAuth2OpenController { @Resource private OAuth2GrantService oauth2GrantService; @Resource private OAuth2ClientService oauth2ClientService; @Resource private OAuth2ApproveService oauth2ApproveService; @Resource private OAuth2TokenService oauth2TokenService; /** * 对应 Spring Security OAuth 的 TokenEndpoint 类的 postAccessToken 方法 * * 授权码 authorization_code 模式时:code + redirectUri + state 参数 * 密码 password 模式时:username + password + scope 参数 * 刷新 refresh_token 模式时:refreshToken 参数 * 客户端 client_credentials 模式:scope 参数 * 简化 implicit 模式时:不支持 * * 注意,默认需要传递 client_id + client_secret 参数 */ @PostMapping("/token") @PermitAll @Operation(summary = "获得访问令牌", description = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【获取】调用") @Parameters({ @Parameter(name = "grant_type", required = true, description = "授权类型", example = "code"), @Parameter(name = "code", description = "授权范围", example = "userinfo.read"), @Parameter(name = "redirect_uri", description = "重定向 URI", example = "https://www.yixiang.co"), @Parameter(name = "state", description = "状态", example = "1"), @Parameter(name = "username", example = "tudou"), @Parameter(name = "password", example = "cai"), // 多个使用空格分隔 @Parameter(name = "scope", example = "user_info"), @Parameter(name = "refresh_token", example = "123424233"), }) public CommonResult postAccessToken(HttpServletRequest request, @RequestParam("grant_type") String grantType, @RequestParam(value = "code", required = false) String code, // 授权码模式 @RequestParam(value = "redirect_uri", required = false) String redirectUri, // 授权码模式 @RequestParam(value = "state", required = false) String state, // 授权码模式 @RequestParam(value = "username", required = false) String username, // 密码模式 @RequestParam(value = "password", required = false) String password, // 密码模式 @RequestParam(value = "scope", required = false) String scope, // 密码模式 @RequestParam(value = "refresh_token", required = false) String refreshToken) { // 刷新模式 List scopes = OAuth2Utils.buildScopes(scope); // 1.1 校验授权类型 OAuth2GrantTypeEnum grantTypeEnum = OAuth2GrantTypeEnum.getByGranType(grantType); if (grantTypeEnum == null) { throw exception0(BAD_REQUEST.getCode(), StrUtil.format("未知授权类型({})", grantType)); } if (grantTypeEnum == OAuth2GrantTypeEnum.IMPLICIT) { throw exception0(BAD_REQUEST.getCode(), "Token 接口不支持 implicit 授权模式"); } // 1.2 校验客户端 String[] clientIdAndSecret = obtainBasicAuthorization(request); OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1], grantType, scopes, redirectUri); // 2. 根据授权模式,获取访问令牌 OAuth2AccessTokenDO accessTokenDO; switch (grantTypeEnum) { case AUTHORIZATION_CODE: accessTokenDO = oauth2GrantService.grantAuthorizationCodeForAccessToken(client.getClientId(), code, redirectUri, state); break; case PASSWORD: accessTokenDO = oauth2GrantService.grantPassword(username, password, client.getClientId(), scopes); break; case CLIENT_CREDENTIALS: accessTokenDO = oauth2GrantService.grantClientCredentials(client.getClientId(), scopes); break; case REFRESH_TOKEN: accessTokenDO = oauth2GrantService.grantRefreshToken(refreshToken, client.getClientId()); break; default: throw new IllegalArgumentException("未知授权类型:" + grantType); } Assert.notNull(accessTokenDO, "访问令牌不能为空"); // 防御性检查 return success(OAuth2OpenConvert.INSTANCE.convert(accessTokenDO)); } @DeleteMapping("/token") @PermitAll @Operation(summary = "删除访问令牌") @Parameter(name = "token", required = true, description = "访问令牌", example = "biu") public CommonResult revokeToken(HttpServletRequest request, @RequestParam("token") String token) { // 校验客户端 String[] clientIdAndSecret = obtainBasicAuthorization(request); OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1], null, null, null); // 删除访问令牌 return success(oauth2GrantService.revokeToken(client.getClientId(), token)); } /** * 对应 Spring Security OAuth 的 CheckTokenEndpoint 类的 checkToken 方法 */ @PostMapping("/check-token") @PermitAll @Operation(summary = "校验访问令牌") @Parameter(name = "token", required = true, description = "访问令牌", example = "biu") public CommonResult checkToken(HttpServletRequest request, @RequestParam("token") String token) { // 校验客户端 String[] clientIdAndSecret = obtainBasicAuthorization(request); oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1], null, null, null); // 校验令牌 OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.checkAccessToken(token); Assert.notNull(accessTokenDO, "访问令牌不能为空"); // 防御性检查 return success(OAuth2OpenConvert.INSTANCE.convert2(accessTokenDO)); } /** * 对应 Spring Security OAuth 的 AuthorizationEndpoint 类的 authorize 方法 */ @GetMapping("/authorize") @Operation(summary = "获得授权信息", description = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【获取】调用") @Parameter(name = "clientId", required = true, description = "客户端编号", example = "tudou") public CommonResult authorize(@RequestParam("clientId") String clientId) { // 0. 校验用户已经登录。通过 Spring Security 实现 // 1. 获得 Client 客户端的信息 OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId); // 2. 获得用户已经授权的信息 List approves = oauth2ApproveService.getApproveList(getLoginUserId(), getUserType(), clientId); // 拼接返回 return success(OAuth2OpenConvert.INSTANCE.convert(client, approves)); } /** * 对应 Spring Security OAuth 的 AuthorizationEndpoint 类的 approveOrDeny 方法 * * 场景一:【自动授权 autoApprove = true】 * 刚进入 sso.vue 界面,调用该接口,用户历史已经给该应用做过对应的授权,或者 OAuth2Client 支持该 scope 的自动授权 * 场景二:【手动授权 autoApprove = false】 * 在 sso.vue 界面,用户选择好 scope 授权范围,调用该接口,进行授权。此时,approved 为 true 或者 false * * 因为前后端分离,Axios 无法很好的处理 302 重定向,所以和 Spring Security OAuth 略有不同,返回结果是重定向的 URL,剩余交给前端处理 */ @PostMapping("/authorize") @Operation(summary = "申请授权", description = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【提交】调用") @Parameters({ @Parameter(name = "response_type", required = true, description = "响应类型", example = "code"), @Parameter(name = "client_id", required = true, description = "客户端编号", example = "tudou"), @Parameter(name = "scope", description = "授权范围", example = "userinfo.read"), // 使用 Map 格式,Spring MVC 暂时不支持这么接收参数 @Parameter(name = "redirect_uri", required = true, description = "重定向 URI", example = "https://www.yixiang.co"), @Parameter(name = "auto_approve", required = true, description = "用户是否接受", example = "true"), @Parameter(name = "state", example = "1") }) public CommonResult approveOrDeny(@RequestParam("response_type") String responseType, @RequestParam("client_id") String clientId, @RequestParam(value = "scope", required = false) String scope, @RequestParam("redirect_uri") String redirectUri, @RequestParam(value = "auto_approve") Boolean autoApprove, @RequestParam(value = "state", required = false) String state) { @SuppressWarnings("unchecked") Map scopes = JsonUtils.parseObject(scope, Map.class); scopes = ObjectUtil.defaultIfNull(scopes, Collections.emptyMap()); // 0. 校验用户已经登录。通过 Spring Security 实现 // 1.1 校验 responseType 是否满足 code 或者 token 值 OAuth2GrantTypeEnum grantTypeEnum = getGrantTypeEnum(responseType); // 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内 OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId, null, grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri); // 2.1 假设 approved 为 null,说明是场景一 if (Boolean.TRUE.equals(autoApprove)) { // 如果无法自动授权通过,则返回空 url,前端不进行跳转 if (!oauth2ApproveService.checkForPreApproval(getLoginUserId(), getUserType(), clientId, scopes.keySet())) { return success(null); } } else { // 2.2 假设 approved 非 null,说明是场景二 // 如果计算后不通过,则跳转一个错误链接 if (!oauth2ApproveService.updateAfterApproval(getLoginUserId(), getUserType(), clientId, scopes)) { return success(OAuth2Utils.buildUnsuccessfulRedirect(redirectUri, responseType, state, "access_denied", "User denied access")); } } // 3.1 如果是 code 授权码模式,则发放 code 授权码,并重定向 List approveScopes = convertList(scopes.entrySet(), Map.Entry::getKey, Map.Entry::getValue); if (grantTypeEnum == OAuth2GrantTypeEnum.AUTHORIZATION_CODE) { return success(getAuthorizationCodeRedirect(getLoginUserId(), client, approveScopes, redirectUri, state)); } // 3.2 如果是 token 则是 implicit 简化模式,则发送 accessToken 访问令牌,并重定向 return success(getImplicitGrantRedirect(getLoginUserId(), client, approveScopes, redirectUri, state)); } private static OAuth2GrantTypeEnum getGrantTypeEnum(String responseType) { if (StrUtil.equals(responseType, "code")) { return OAuth2GrantTypeEnum.AUTHORIZATION_CODE; } if (StrUtil.equalsAny(responseType, "token")) { return OAuth2GrantTypeEnum.IMPLICIT; } throw exception0(BAD_REQUEST.getCode(), "response_type 参数值只允许 code 和 token"); } private String getImplicitGrantRedirect(Long userId, OAuth2ClientDO client, List scopes, String redirectUri, String state) { // 1. 创建 access token 访问令牌 OAuth2AccessTokenDO accessTokenDO = oauth2GrantService.grantImplicit(userId, getUserType(), client.getClientId(), scopes); Assert.notNull(accessTokenDO, "访问令牌不能为空"); // 防御性检查 // 2. 拼接重定向的 URL // noinspection unchecked return OAuth2Utils.buildImplicitRedirectUri(redirectUri, accessTokenDO.getAccessToken(), state, accessTokenDO.getExpiresTime(), scopes, JsonUtils.parseObject(client.getAdditionalInformation(), Map.class)); } private String getAuthorizationCodeRedirect(Long userId, OAuth2ClientDO client, List scopes, String redirectUri, String state) { // 1. 创建 code 授权码 String authorizationCode = oauth2GrantService.grantAuthorizationCodeForCode(userId, getUserType(), client.getClientId(), scopes, redirectUri, state); // 2. 拼接重定向的 URL return OAuth2Utils.buildAuthorizationCodeRedirectUri(redirectUri, authorizationCode, state); } private Integer getUserType() { return UserTypeEnum.ADMIN.getValue(); } private String[] obtainBasicAuthorization(HttpServletRequest request) { String[] clientIdAndSecret = HttpUtils.obtainBasicAuthorization(request); if (ArrayUtil.isEmpty(clientIdAndSecret) || clientIdAndSecret.length != 2) { throw exception0(BAD_REQUEST.getCode(), "client_id 或 client_secret 未正确传递"); } return clientIdAndSecret; } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/oauth2/OAuth2TokenController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.oauth2; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenRespVO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import co.yixiang.yshop.module.system.enums.logger.LoginLogTypeEnum; import co.yixiang.yshop.module.system.service.auth.AdminAuthService; import co.yixiang.yshop.module.system.service.oauth2.OAuth2TokenService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - OAuth2.0 令牌") @RestController @RequestMapping("/system/oauth2-token") public class OAuth2TokenController { @Resource private OAuth2TokenService oauth2TokenService; @Resource private AdminAuthService authService; @GetMapping("/page") @Operation(summary = "获得访问令牌分页", description = "只返回有效期内的") @PreAuthorize("@ss.hasPermission('system:oauth2-token:page')") public CommonResult> getAccessTokenPage(@Valid OAuth2AccessTokenPageReqVO reqVO) { PageResult pageResult = oauth2TokenService.getAccessTokenPage(reqVO); return success(BeanUtils.toBean(pageResult, OAuth2AccessTokenRespVO.class)); } @DeleteMapping("/delete") @Operation(summary = "删除访问令牌") @Parameter(name = "accessToken", description = "访问令牌", required = true, example = "tudou") @PreAuthorize("@ss.hasPermission('system:oauth2-token:delete')") public CommonResult deleteAccessToken(@RequestParam("accessToken") String accessToken) { authService.logout(accessToken, LoginLogTypeEnum.LOGOUT_DELETE.getType()); return success(true); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/oauth2/OAuth2UserController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.oauth2; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.user.OAuth2UserInfoRespVO; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.user.OAuth2UserUpdateReqVO; import co.yixiang.yshop.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; import co.yixiang.yshop.module.system.dal.dataobject.dept.DeptDO; import co.yixiang.yshop.module.system.dal.dataobject.dept.PostDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.module.system.service.dept.DeptService; import co.yixiang.yshop.module.system.service.dept.PostService; import co.yixiang.yshop.module.system.service.user.AdminUserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; /** * 提供给外部应用调用为主 * * 1. 在 getUserInfo 方法上,添加 @PreAuthorize("@ss.hasScope('user.read')") 注解,声明需要满足 scope = user.read * 2. 在 updateUserInfo 方法上,添加 @PreAuthorize("@ss.hasScope('user.write')") 注解,声明需要满足 scope = user.write * * @author yshop */ @Tag(name = "管理后台 - OAuth2.0 用户") @RestController @RequestMapping("/system/oauth2/user") @Validated @Slf4j public class OAuth2UserController { @Resource private AdminUserService userService; @Resource private DeptService deptService; @Resource private PostService postService; @GetMapping("/get") @Operation(summary = "获得用户基本信息") @PreAuthorize("@ss.hasScope('user.read')") // public CommonResult getUserInfo() { // 获得用户基本信息 AdminUserDO user = userService.getUser(getLoginUserId()); OAuth2UserInfoRespVO resp = BeanUtils.toBean(user, OAuth2UserInfoRespVO.class); // 获得部门信息 if (user.getDeptId() != null) { DeptDO dept = deptService.getDept(user.getDeptId()); resp.setDept(BeanUtils.toBean(dept, OAuth2UserInfoRespVO.Dept.class)); } // 获得岗位信息 if (CollUtil.isNotEmpty(user.getPostIds())) { List posts = postService.getPostList(user.getPostIds()); resp.setPosts(BeanUtils.toBean(posts, OAuth2UserInfoRespVO.Post.class)); } return success(resp); } @PutMapping("/update") @Operation(summary = "更新用户基本信息") @PreAuthorize("@ss.hasScope('user.write')") public CommonResult updateUserInfo(@Valid @RequestBody OAuth2UserUpdateReqVO reqVO) { // 这里将 UserProfileUpdateReqVO =》UserProfileUpdateReqVO 对象,实现接口的复用。 // 主要是,AdminUserService 没有自己的 BO 对象,所以复用只能这么做 userService.updateUserProfile(getLoginUserId(), BeanUtils.toBean(reqVO, UserProfileUpdateReqVO.class)); return success(true); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/oauth2/vo/client/OAuth2ClientPageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.oauth2.vo.client; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import co.yixiang.yshop.framework.common.pojo.PageParam; @Schema(description = "管理后台 - OAuth2 客户端分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class OAuth2ClientPageReqVO extends PageParam { @Schema(description = "应用名,模糊匹配", example = "土豆") private String name; @Schema(description = "状态,参见 CommonStatusEnum 枚举", example = "1") private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/oauth2/vo/client/OAuth2ClientRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.oauth2.vo.client; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; import java.util.List; @Schema(description = "管理后台 - OAuth2 客户端 Response VO") @Data public class OAuth2ClientRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "tudou") private String clientId; @Schema(description = "客户端密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "fan") private String secret; @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆") private String name; @Schema(description = "应用图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.yixiang.co/xx.png") private String logo; @Schema(description = "应用描述", example = "我是一个应用") private String description; @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer status; @Schema(description = "访问令牌的有效期", requiredMode = Schema.RequiredMode.REQUIRED, example = "8640") private Integer accessTokenValiditySeconds; @Schema(description = "刷新令牌的有效期", requiredMode = Schema.RequiredMode.REQUIRED, example = "8640000") private Integer refreshTokenValiditySeconds; @Schema(description = "可重定向的 URI 地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.yixiang.co") private List redirectUris; @Schema(description = "授权类型,参见 OAuth2GrantTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "password") private List authorizedGrantTypes; @Schema(description = "授权范围", example = "user_info") private List scopes; @Schema(description = "自动通过的授权范围", example = "user_info") private List autoApproveScopes; @Schema(description = "权限", example = "system:user:query") private List authorities; @Schema(description = "资源", example = "1024") private List resourceIds; @Schema(description = "附加信息", example = "{yshop: true}") private String additionalInformation; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/oauth2/vo/client/OAuth2ClientSaveReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.oauth2.vo.client; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import org.hibernate.validator.constraints.URL; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.util.List; @Schema(description = "管理后台 - OAuth2 客户端创建/修改 Request VO") @Data public class OAuth2ClientSaveReqVO { @Schema(description = "编号", example = "1024") private Long id; @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "tudou") @NotNull(message = "客户端编号不能为空") private String clientId; @Schema(description = "客户端密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "fan") @NotNull(message = "客户端密钥不能为空") private String secret; @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆") @NotNull(message = "应用名不能为空") private String name; @Schema(description = "应用图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.yixiang.co/xx.png") @NotNull(message = "应用图标不能为空") @URL(message = "应用图标的地址不正确") private String logo; @Schema(description = "应用描述", example = "我是一个应用") private String description; @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "状态不能为空") private Integer status; @Schema(description = "访问令牌的有效期", requiredMode = Schema.RequiredMode.REQUIRED, example = "8640") @NotNull(message = "访问令牌的有效期不能为空") private Integer accessTokenValiditySeconds; @Schema(description = "刷新令牌的有效期", requiredMode = Schema.RequiredMode.REQUIRED, example = "8640000") @NotNull(message = "刷新令牌的有效期不能为空") private Integer refreshTokenValiditySeconds; @Schema(description = "可重定向的 URI 地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.yixiang.co") @NotNull(message = "可重定向的 URI 地址不能为空") private List<@NotEmpty(message = "重定向的 URI 不能为空") @URL(message = "重定向的 URI 格式不正确") String> redirectUris; @Schema(description = "授权类型,参见 OAuth2GrantTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "password") @NotNull(message = "授权类型不能为空") private List authorizedGrantTypes; @Schema(description = "授权范围", example = "user_info") private List scopes; @Schema(description = "自动通过的授权范围", example = "user_info") private List autoApproveScopes; @Schema(description = "权限", example = "system:user:query") private List authorities; @Schema(description = "资源", example = "1024") private List resourceIds; @Schema(description = "附加信息", example = "{yshop: true}") private String additionalInformation; @AssertTrue(message = "附加信息必须是 JSON 格式") public boolean isAdditionalInformationJson() { return StrUtil.isEmpty(additionalInformation) || JsonUtils.isJson(additionalInformation); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAccessTokenRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.oauth2.vo.open; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Schema(description = "管理后台 - 【开放接口】访问令牌 Response VO") @Data @NoArgsConstructor @AllArgsConstructor public class OAuth2OpenAccessTokenRespVO { @Schema(description = "访问令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "tudou") @JsonProperty("access_token") private String accessToken; @Schema(description = "刷新令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "nice") @JsonProperty("refresh_token") private String refreshToken; @Schema(description = "令牌类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "bearer") @JsonProperty("token_type") private String tokenType; @Schema(description = "过期时间,单位:秒", requiredMode = Schema.RequiredMode.REQUIRED, example = "42430") @JsonProperty("expires_in") private Long expiresIn; @Schema(description = "授权范围,如果多个授权范围,使用空格分隔", example = "user_info") private String scope; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAuthorizeInfoRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.oauth2.vo.open; import co.yixiang.yshop.framework.common.core.KeyValue; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; @Schema(description = "管理后台 - 授权页的信息 Response VO") @Data @NoArgsConstructor @AllArgsConstructor public class OAuth2OpenAuthorizeInfoRespVO { /** * 客户端 */ private Client client; @Schema(description = "scope 的选中信息,使用 List 保证有序性,Key 是 scope,Value 为是否选中", requiredMode = Schema.RequiredMode.REQUIRED) private List> scopes; @Data @NoArgsConstructor @AllArgsConstructor public static class Client { @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆") private String name; @Schema(description = "应用图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.yixiang.co/xx.png") private String logo; } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/oauth2/vo/open/OAuth2OpenCheckTokenRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.oauth2.vo.open; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; @Schema(description = "管理后台 - 【开放接口】校验令牌 Response VO") @Data @NoArgsConstructor @AllArgsConstructor public class OAuth2OpenCheckTokenRespVO { @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") @JsonProperty("user_id") private Long userId; @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") @JsonProperty("user_type") private Integer userType; @Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @JsonProperty("tenant_id") private Long tenantId; @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "car") @JsonProperty("client_id") private String clientId; @Schema(description = "授权范围", requiredMode = Schema.RequiredMode.REQUIRED, example = "user_info") private List scopes; @Schema(description = "访问令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "tudou") @JsonProperty("access_token") private String accessToken; @Schema(description = "过期时间,时间戳 / 1000,即单位:秒", requiredMode = Schema.RequiredMode.REQUIRED, example = "1593092157") private Long exp; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenPageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.oauth2.vo.token; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; @Schema(description = "管理后台 - 访问令牌分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) public class OAuth2AccessTokenPageReqVO extends PageParam { @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") private Long userId; @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") private Integer userType; @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") private String clientId; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.oauth2.vo.token; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.time.LocalDateTime; @Schema(description = "管理后台 - 访问令牌 Response VO") @Data @NoArgsConstructor @AllArgsConstructor public class OAuth2AccessTokenRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "访问令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "tudou") private String accessToken; @Schema(description = "刷新令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "nice") private String refreshToken; @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") private Long userId; @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") private Integer userType; @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") private String clientId; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; @Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime expiresTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/oauth2/vo/user/OAuth2UserInfoRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.oauth2.vo.user; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; @Schema(description = "管理后台 - OAuth2 获得用户基本信息 Response VO") @Data @NoArgsConstructor @AllArgsConstructor public class OAuth2UserInfoRespVO { @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long id; @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String username; @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String nickname; @Schema(description = "用户邮箱", example = "yshop@yixiang.co") private String email; @Schema(description = "手机号码", example = "15601691300") private String mobile; @Schema(description = "用户性别,参见 SexEnum 枚举类", example = "1") private Integer sex; @Schema(description = "用户头像", example = "https://www.yixiang.co/xxx.png") private String avatar; /** * 所在部门 */ private Dept dept; /** * 所属岗位数组 */ private List posts; @Schema(description = "部门") @Data public static class Dept { @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long id; @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "研发部") private String name; } @Schema(description = "岗位") @Data public static class Post { @Schema(description = "岗位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long id; @Schema(description = "岗位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "开发") private String name; } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/oauth2/vo/user/OAuth2UserUpdateReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.oauth2.vo.user; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.Length; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Size; @Schema(description = "管理后台 - OAuth2 更新用户基本信息 Request VO") @Data @NoArgsConstructor @AllArgsConstructor public class OAuth2UserUpdateReqVO { @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @Size(max = 30, message = "用户昵称长度不能超过 30 个字符") private String nickname; @Schema(description = "用户邮箱", example = "yshop@yixiang.co") @Email(message = "邮箱格式不正确") @Size(max = 50, message = "邮箱长度不能超过 50 个字符") private String email; @Schema(description = "手机号码", example = "15601691300") @Length(min = 11, max = 11, message = "手机号长度必须 11 位") private String mobile; @Schema(description = "用户性别,参见 SexEnum 枚举类", example = "1") private Integer sex; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/permission/MenuController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.permission; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.permission.vo.menu.MenuListReqVO; import co.yixiang.yshop.module.system.controller.admin.permission.vo.menu.MenuRespVO; import co.yixiang.yshop.module.system.controller.admin.permission.vo.menu.MenuSaveVO; import co.yixiang.yshop.module.system.controller.admin.permission.vo.menu.MenuSimpleRespVO; import co.yixiang.yshop.module.system.dal.dataobject.permission.MenuDO; import co.yixiang.yshop.module.system.service.permission.MenuService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import java.util.Comparator; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 菜单") @RestController @RequestMapping("/system/menu") @Validated public class MenuController { @Resource private MenuService menuService; @PostMapping("/create") @Operation(summary = "创建菜单") @PreAuthorize("@ss.hasPermission('system:menu:create')") public CommonResult createMenu(@Valid @RequestBody MenuSaveVO createReqVO) { Long menuId = menuService.createMenu(createReqVO); return success(menuId); } @PutMapping("/update") @Operation(summary = "修改菜单") @PreAuthorize("@ss.hasPermission('system:menu:update')") public CommonResult updateMenu(@Valid @RequestBody MenuSaveVO updateReqVO) { menuService.updateMenu(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除菜单") @Parameter(name = "id", description = "菜单编号", required= true, example = "1024") @PreAuthorize("@ss.hasPermission('system:menu:delete')") public CommonResult deleteMenu(@RequestParam("id") Long id) { menuService.deleteMenu(id); return success(true); } @GetMapping("/list") @Operation(summary = "获取菜单列表", description = "用于【菜单管理】界面") @PreAuthorize("@ss.hasPermission('system:menu:query')") public CommonResult> getMenuList(MenuListReqVO reqVO) { List list = menuService.getMenuList(reqVO); list.sort(Comparator.comparing(MenuDO::getSort)); return success(BeanUtils.toBean(list, MenuRespVO.class)); } @GetMapping({"/list-all-simple", "simple-list"}) @Operation(summary = "获取菜单精简信息列表", description = "只包含被开启的菜单,用于【角色分配菜单】功能的选项。" + "在多租户的场景下,会只返回租户所在套餐有的菜单") public CommonResult> getSimpleMenuList() { List list = menuService.getMenuListByTenant( new MenuListReqVO().setStatus(CommonStatusEnum.ENABLE.getStatus())); list.sort(Comparator.comparing(MenuDO::getSort)); return success(BeanUtils.toBean(list, MenuSimpleRespVO.class)); } @GetMapping("/get") @Operation(summary = "获取菜单信息") @PreAuthorize("@ss.hasPermission('system:menu:query')") public CommonResult getMenu(Long id) { MenuDO menu = menuService.getMenu(id); return success(BeanUtils.toBean(menu, MenuRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/permission/PermissionController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.permission; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.module.system.controller.admin.permission.vo.permission.PermissionAssignRoleDataScopeReqVO; import co.yixiang.yshop.module.system.controller.admin.permission.vo.permission.PermissionAssignRoleMenuReqVO; import co.yixiang.yshop.module.system.controller.admin.permission.vo.permission.PermissionAssignUserRoleReqVO; import co.yixiang.yshop.module.system.service.permission.PermissionService; import co.yixiang.yshop.module.system.service.tenant.TenantService; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Operation; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import java.util.Set; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; /** * 权限 Controller,提供赋予用户、角色的权限的 API 接口 * * @author yshop */ @Tag(name = "管理后台 - 权限") @RestController @RequestMapping("/system/permission") public class PermissionController { @Resource private PermissionService permissionService; @Resource private TenantService tenantService; @Operation(summary = "获得角色拥有的菜单编号") @Parameter(name = "roleId", description = "角色编号", required = true) @GetMapping("/list-role-menus") @PreAuthorize("@ss.hasPermission('system:permission:assign-role-menu')") public CommonResult> getRoleMenuList(Long roleId) { return success(permissionService.getRoleMenuListByRoleId(roleId)); } @PostMapping("/assign-role-menu") @Operation(summary = "赋予角色菜单") @PreAuthorize("@ss.hasPermission('system:permission:assign-role-menu')") public CommonResult assignRoleMenu(@Validated @RequestBody PermissionAssignRoleMenuReqVO reqVO) { // 开启多租户的情况下,需要过滤掉未开通的菜单 tenantService.handleTenantMenu(menuIds -> reqVO.getMenuIds().removeIf(menuId -> !CollUtil.contains(menuIds, menuId))); // 执行菜单的分配 permissionService.assignRoleMenu(reqVO.getRoleId(), reqVO.getMenuIds()); return success(true); } @PostMapping("/assign-role-data-scope") @Operation(summary = "赋予角色数据权限") @PreAuthorize("@ss.hasPermission('system:permission:assign-role-data-scope')") public CommonResult assignRoleDataScope(@Valid @RequestBody PermissionAssignRoleDataScopeReqVO reqVO) { permissionService.assignRoleDataScope(reqVO.getRoleId(), reqVO.getDataScope(), reqVO.getDataScopeDeptIds()); return success(true); } @Operation(summary = "获得管理员拥有的角色编号列表") @Parameter(name = "userId", description = "用户编号", required = true) @GetMapping("/list-user-roles") @PreAuthorize("@ss.hasPermission('system:permission:assign-user-role')") public CommonResult> listAdminRoles(@RequestParam("userId") Long userId) { return success(permissionService.getUserRoleIdListByUserId(userId)); } @Operation(summary = "赋予用户角色") @PostMapping("/assign-user-role") @PreAuthorize("@ss.hasPermission('system:permission:assign-user-role')") public CommonResult assignUserRole(@Validated @RequestBody PermissionAssignUserRoleReqVO reqVO) { permissionService.assignUserRole(reqVO.getUserId(), reqVO.getRoleIds()); return success(true); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/permission/RoleController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.permission; import co.yixiang.yshop.framework.apilog.core.annotation.ApiAccessLog; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.system.controller.admin.permission.vo.role.*; import co.yixiang.yshop.module.system.dal.dataobject.permission.RoleDO; import co.yixiang.yshop.module.system.service.permission.RoleService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.io.IOException; import java.util.Comparator; import java.util.List; import static co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import static java.util.Collections.singleton; @Tag(name = "管理后台 - 角色") @RestController @RequestMapping("/system/role") @Validated public class RoleController { @Resource private RoleService roleService; @PostMapping("/create") @Operation(summary = "创建角色") @PreAuthorize("@ss.hasPermission('system:role:create')") public CommonResult createRole(@Valid @RequestBody RoleSaveReqVO createReqVO) { return success(roleService.createRole(createReqVO, null)); } @PutMapping("/update") @Operation(summary = "修改角色") @PreAuthorize("@ss.hasPermission('system:role:update')") public CommonResult updateRole(@Valid @RequestBody RoleSaveReqVO updateReqVO) { roleService.updateRole(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除角色") @Parameter(name = "id", description = "角色编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:role:delete')") public CommonResult deleteRole(@RequestParam("id") Long id) { roleService.deleteRole(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得角色信息") @PreAuthorize("@ss.hasPermission('system:role:query')") public CommonResult getRole(@RequestParam("id") Long id) { RoleDO role = roleService.getRole(id); return success(BeanUtils.toBean(role, RoleRespVO.class)); } @GetMapping("/page") @Operation(summary = "获得角色分页") @PreAuthorize("@ss.hasPermission('system:role:query')") public CommonResult> getRolePage(RolePageReqVO pageReqVO) { PageResult pageResult = roleService.getRolePage(pageReqVO); return success(BeanUtils.toBean(pageResult, RoleRespVO.class)); } @GetMapping({"/list-all-simple", "/simple-list"}) @Operation(summary = "获取角色精简信息列表", description = "只包含被开启的角色,主要用于前端的下拉选项") public CommonResult> getSimpleRoleList() { List list = roleService.getRoleListByStatus(singleton(CommonStatusEnum.ENABLE.getStatus())); list.sort(Comparator.comparing(RoleDO::getSort)); return success(BeanUtils.toBean(list, RoleRespVO.class)); } @GetMapping("/export-excel") @Operation(summary = "导出角色 Excel") @ApiAccessLog(operateType = EXPORT) @PreAuthorize("@ss.hasPermission('system:role:export')") public void export(HttpServletResponse response, @Validated RolePageReqVO exportReqVO) throws IOException { exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = roleService.getRolePage(exportReqVO).getList(); // 输出 ExcelUtils.write(response, "角色数据.xls", "数据", RoleRespVO.class, BeanUtils.toBean(list, RoleRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/permission/vo/menu/MenuListReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.permission.vo.menu; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "管理后台 - 菜单列表 Request VO") @Data public class MenuListReqVO { @Schema(description = "菜单名称,模糊匹配", example = "yshop") private String name; @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/permission/vo/menu/MenuRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.permission.vo.menu; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import java.time.LocalDateTime; @Schema(description = "管理后台 - 菜单信息 Response VO") @Data public class MenuRespVO { @Schema(description = "菜单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "菜单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @NotBlank(message = "菜单名称不能为空") @Size(max = 50, message = "菜单名称长度不能超过50个字符") private String name; @Schema(description = "权限标识,仅菜单类型为按钮时,才需要传递", example = "sys:menu:add") @Size(max = 100) private String permission; @Schema(description = "类型,参见 MenuTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "菜单类型不能为空") private Integer type; @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "显示顺序不能为空") private Integer sort; @Schema(description = "父菜单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "父菜单 ID 不能为空") private Long parentId; @Schema(description = "路由地址,仅菜单类型为菜单或者目录时,才需要传", example = "post") @Size(max = 200, message = "路由地址不能超过200个字符") private String path; @Schema(description = "菜单图标,仅菜单类型为菜单或者目录时,才需要传", example = "/menu/list") private String icon; @Schema(description = "组件路径,仅菜单类型为菜单时,才需要传", example = "system/post/index") @Size(max = 200, message = "组件路径不能超过255个字符") private String component; @Schema(description = "组件名", example = "SystemUser") private String componentName; @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "状态不能为空") private Integer status; @Schema(description = "是否可见", example = "false") private Boolean visible; @Schema(description = "是否缓存", example = "false") private Boolean keepAlive; @Schema(description = "是否总是显示", example = "false") private Boolean alwaysShow; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/permission/vo/menu/MenuSaveVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.permission.vo.menu; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @Schema(description = "管理后台 - 菜单创建/修改 Request VO") @Data public class MenuSaveVO { @Schema(description = "菜单编号", example = "1024") private Long id; @Schema(description = "菜单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @NotBlank(message = "菜单名称不能为空") @Size(max = 50, message = "菜单名称长度不能超过50个字符") private String name; @Schema(description = "权限标识,仅菜单类型为按钮时,才需要传递", example = "sys:menu:add") @Size(max = 100) private String permission; @Schema(description = "类型,参见 MenuTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "菜单类型不能为空") private Integer type; @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "显示顺序不能为空") private Integer sort; @Schema(description = "父菜单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "父菜单 ID 不能为空") private Long parentId; @Schema(description = "路由地址,仅菜单类型为菜单或者目录时,才需要传", example = "post") @Size(max = 200, message = "路由地址不能超过200个字符") private String path; @Schema(description = "菜单图标,仅菜单类型为菜单或者目录时,才需要传", example = "/menu/list") private String icon; @Schema(description = "组件路径,仅菜单类型为菜单时,才需要传", example = "system/post/index") @Size(max = 200, message = "组件路径不能超过255个字符") private String component; @Schema(description = "组件名", example = "SystemUser") private String componentName; @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "状态不能为空") private Integer status; @Schema(description = "是否可见", example = "false") private Boolean visible; @Schema(description = "是否缓存", example = "false") private Boolean keepAlive; @Schema(description = "是否总是显示", example = "false") private Boolean alwaysShow; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/permission/vo/menu/MenuSimpleRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.permission.vo.menu; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "管理后台 - 菜单精简信息 Response VO") @Data public class MenuSimpleRespVO { @Schema(description = "菜单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "菜单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String name; @Schema(description = "父菜单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long parentId; @Schema(description = "类型,参见 MenuTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer type; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleDataScopeReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.permission.vo.permission; import co.yixiang.yshop.framework.common.validation.InEnum; import co.yixiang.yshop.module.system.enums.permission.DataScopeEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotNull; import java.util.Collections; import java.util.Set; @Schema(description = "管理后台 - 赋予角色数据权限 Request VO") @Data public class PermissionAssignRoleDataScopeReqVO { @Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "角色编号不能为空") private Long roleId; @Schema(description = "数据范围,参见 DataScopeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "数据范围不能为空") @InEnum(value = DataScopeEnum.class, message = "数据范围必须是 {value}") private Integer dataScope; @Schema(description = "部门编号列表,只有范围类型为 DEPT_CUSTOM 时,该字段才需要", example = "1,3,5") private Set dataScopeDeptIds = Collections.emptySet(); // 兜底 } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.permission.vo.permission; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotNull; import java.util.Collections; import java.util.Set; @Schema(description = "管理后台 - 赋予角色菜单 Request VO") @Data public class PermissionAssignRoleMenuReqVO { @Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "角色编号不能为空") private Long roleId; @Schema(description = "菜单编号列表", example = "1,3,5") private Set menuIds = Collections.emptySet(); // 兜底 } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/permission/vo/permission/PermissionAssignUserRoleReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.permission.vo.permission; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotNull; import java.util.Collections; import java.util.Set; @Schema(description = "管理后台 - 赋予用户角色 Request VO") @Data public class PermissionAssignUserRoleReqVO { @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "用户编号不能为空") private Long userId; @Schema(description = "角色编号列表", example = "1,3,5") private Set roleIds = Collections.emptySet(); // 兜底 } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/permission/vo/role/RolePageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.permission.vo.role; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 角色分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) public class RolePageReqVO extends PageParam { @Schema(description = "角色名称,模糊匹配", example = "yshop") private String name; @Schema(description = "角色标识,模糊匹配", example = "yshop") private String code; @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") private Integer status; @Schema(description = "创建时间", example = "[2022-07-01 00:00:00,2022-07-01 23:59:59]") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/permission/vo/role/RoleRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.permission.vo.role; import co.yixiang.yshop.framework.excel.core.annotations.DictFormat; import co.yixiang.yshop.framework.excel.core.convert.DictConvert; import co.yixiang.yshop.module.system.enums.DictTypeConstants; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotBlank; import java.time.LocalDateTime; import java.util.Set; @Schema(description = "管理后台 - 角色信息 Response VO") @Data @ExcelIgnoreUnannotated public class RoleRespVO { @Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty("角色序号") private Long id; @Schema(description = "角色名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "管理员") @ExcelProperty("角色名称") private String name; @Schema(description = "角色标志", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin") @NotBlank(message = "角色标志不能为空") @ExcelProperty("角色标志") private String code; @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @ExcelProperty("角色排序") private Integer sort; @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty(value = "角色状态", converter = DictConvert.class) @DictFormat(DictTypeConstants.COMMON_STATUS) private Integer status; @Schema(description = "角色类型,参见 RoleTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer type; @Schema(description = "备注", example = "我是一个角色") private String remark; @Schema(description = "数据范围,参见 DataScopeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty("数据范围") private Integer dataScope; @Schema(description = "数据范围(指定部门数组)", example = "1") private Set dataScopeDeptIds; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/permission/vo/role/RoleSaveReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.permission.vo.role; import com.mzt.logapi.starter.annotation.DiffLogField; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @Schema(description = "管理后台 - 角色创建/更新 Request VO") @Data public class RoleSaveReqVO { @Schema(description = "角色编号", example = "1") private Long id; @Schema(description = "角色名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "管理员") @NotBlank(message = "角色名称不能为空") @Size(max = 30, message = "角色名称长度不能超过 30 个字符") @DiffLogField(name = "角色名称") private String name; @NotBlank(message = "角色标志不能为空") @Size(max = 100, message = "角色标志长度不能超过 100 个字符") @Schema(description = "角色编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ADMIN") @DiffLogField(name = "角色标志") private String code; @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "显示顺序不能为空") @DiffLogField(name = "显示顺序") private Integer sort; @Schema(description = "备注", example = "我是一个角色") @DiffLogField(name = "备注") private String remark; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/permission/vo/role/RoleSimpleRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.permission.vo.role; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Schema(description = "管理后台 - 角色精简信息 Response VO") @Data public class RoleSimpleRespVO { @Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "角色名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String name; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/sms/SmsCallbackController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.sms; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.util.servlet.ServletUtils; import co.yixiang.yshop.module.system.framework.sms.core.enums.SmsChannelEnum; import co.yixiang.yshop.module.system.service.sms.SmsSendService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import jakarta.annotation.Resource; import jakarta.annotation.security.PermitAll; import jakarta.servlet.http.HttpServletRequest; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 短信回调") @RestController @RequestMapping("/system/sms/callback") public class SmsCallbackController { @Resource private SmsSendService smsSendService; @PostMapping("/aliyun") @PermitAll @Operation(summary = "阿里云短信的回调", description = "参见 https://help.aliyun.com/zh/sms/developer-reference/configure-delivery-receipts-1 文档") public CommonResult receiveAliyunSmsStatus(HttpServletRequest request) throws Throwable { String text = ServletUtils.getBody(request); smsSendService.receiveSmsStatus(SmsChannelEnum.ALIYUN.getCode(), text); return success(true); } @PostMapping("/tencent") @PermitAll @Operation(summary = "腾讯云短信的回调", description = "参见 https://cloud.tencent.com/document/product/382/59178 文档") public CommonResult receiveTencentSmsStatus(HttpServletRequest request) throws Throwable { String text = ServletUtils.getBody(request); smsSendService.receiveSmsStatus(SmsChannelEnum.TENCENT.getCode(), text); return success(true); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/sms/SmsChannelController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.sms; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; import co.yixiang.yshop.module.system.controller.admin.sms.vo.channel.SmsChannelRespVO; import co.yixiang.yshop.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO; import co.yixiang.yshop.module.system.controller.admin.sms.vo.channel.SmsChannelSimpleRespVO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsChannelDO; import co.yixiang.yshop.module.system.service.sms.SmsChannelService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import java.util.Comparator; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 短信渠道") @RestController @RequestMapping("system/sms-channel") public class SmsChannelController { @Resource private SmsChannelService smsChannelService; @PostMapping("/create") @Operation(summary = "创建短信渠道") @PreAuthorize("@ss.hasPermission('system:sms-channel:create')") public CommonResult createSmsChannel(@Valid @RequestBody SmsChannelSaveReqVO createReqVO) { return success(smsChannelService.createSmsChannel(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新短信渠道") @PreAuthorize("@ss.hasPermission('system:sms-channel:update')") public CommonResult updateSmsChannel(@Valid @RequestBody SmsChannelSaveReqVO updateReqVO) { smsChannelService.updateSmsChannel(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除短信渠道") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('system:sms-channel:delete')") public CommonResult deleteSmsChannel(@RequestParam("id") Long id) { smsChannelService.deleteSmsChannel(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得短信渠道") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:sms-channel:query')") public CommonResult getSmsChannel(@RequestParam("id") Long id) { SmsChannelDO channel = smsChannelService.getSmsChannel(id); return success(BeanUtils.toBean(channel, SmsChannelRespVO.class)); } @GetMapping("/page") @Operation(summary = "获得短信渠道分页") @PreAuthorize("@ss.hasPermission('system:sms-channel:query')") public CommonResult> getSmsChannelPage(@Valid SmsChannelPageReqVO pageVO) { PageResult pageResult = smsChannelService.getSmsChannelPage(pageVO); return success(BeanUtils.toBean(pageResult, SmsChannelRespVO.class)); } @GetMapping({"/list-all-simple", "/simple-list"}) @Operation(summary = "获得短信渠道精简列表", description = "包含被禁用的短信渠道") public CommonResult> getSimpleSmsChannelList() { List list = smsChannelService.getSmsChannelList(); list.sort(Comparator.comparing(SmsChannelDO::getId)); return success(BeanUtils.toBean(list, SmsChannelSimpleRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/sms/SmsLogController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.sms; import co.yixiang.yshop.framework.apilog.core.annotation.ApiAccessLog; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO; import co.yixiang.yshop.module.system.controller.admin.sms.vo.log.SmsLogRespVO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsLogDO; import co.yixiang.yshop.module.system.service.sms.SmsLogService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; 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.io.IOException; import java.util.List; import static co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 短信日志") @RestController @RequestMapping("/system/sms-log") @Validated public class SmsLogController { @Resource private SmsLogService smsLogService; @GetMapping("/page") @Operation(summary = "获得短信日志分页") @PreAuthorize("@ss.hasPermission('system:sms-log:query')") public CommonResult> getSmsLogPage(@Valid SmsLogPageReqVO pageReqVO) { PageResult pageResult = smsLogService.getSmsLogPage(pageReqVO); return success(BeanUtils.toBean(pageResult, SmsLogRespVO.class)); } @GetMapping("/export-excel") @Operation(summary = "导出短信日志 Excel") @PreAuthorize("@ss.hasPermission('system:sms-log:export')") @ApiAccessLog(operateType = EXPORT) public void exportSmsLogExcel(@Valid SmsLogPageReqVO exportReqVO, HttpServletResponse response) throws IOException { exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = smsLogService.getSmsLogPage(exportReqVO).getList(); // 导出 Excel ExcelUtils.write(response, "短信日志.xls", "数据", SmsLogRespVO.class, BeanUtils.toBean(list, SmsLogRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/sms/SmsTemplateController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.sms; import co.yixiang.yshop.framework.apilog.core.annotation.ApiAccessLog; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.sms.vo.template.*; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsTemplateDO; import co.yixiang.yshop.module.system.service.sms.SmsTemplateService; import co.yixiang.yshop.module.system.service.sms.SmsSendService; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Operation; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import java.io.IOException; import java.util.List; import static co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 短信模板") @RestController @RequestMapping("/system/sms-template") public class SmsTemplateController { @Resource private SmsTemplateService smsTemplateService; @Resource private SmsSendService smsSendService; @PostMapping("/create") @Operation(summary = "创建短信模板") @PreAuthorize("@ss.hasPermission('system:sms-template:create')") public CommonResult createSmsTemplate(@Valid @RequestBody SmsTemplateSaveReqVO createReqVO) { return success(smsTemplateService.createSmsTemplate(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新短信模板") @PreAuthorize("@ss.hasPermission('system:sms-template:update')") public CommonResult updateSmsTemplate(@Valid @RequestBody SmsTemplateSaveReqVO updateReqVO) { smsTemplateService.updateSmsTemplate(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除短信模板") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('system:sms-template:delete')") public CommonResult deleteSmsTemplate(@RequestParam("id") Long id) { smsTemplateService.deleteSmsTemplate(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得短信模板") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:sms-template:query')") public CommonResult getSmsTemplate(@RequestParam("id") Long id) { SmsTemplateDO template = smsTemplateService.getSmsTemplate(id); return success(BeanUtils.toBean(template, SmsTemplateRespVO.class)); } @GetMapping("/page") @Operation(summary = "获得短信模板分页") @PreAuthorize("@ss.hasPermission('system:sms-template:query')") public CommonResult> getSmsTemplatePage(@Valid SmsTemplatePageReqVO pageVO) { PageResult pageResult = smsTemplateService.getSmsTemplatePage(pageVO); return success(BeanUtils.toBean(pageResult, SmsTemplateRespVO.class)); } @GetMapping("/export-excel") @Operation(summary = "导出短信模板 Excel") @PreAuthorize("@ss.hasPermission('system:sms-template:export')") @ApiAccessLog(operateType = EXPORT) public void exportSmsTemplateExcel(@Valid SmsTemplatePageReqVO exportReqVO, HttpServletResponse response) throws IOException { exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = smsTemplateService.getSmsTemplatePage(exportReqVO).getList(); // 导出 Excel ExcelUtils.write(response, "短信模板.xls", "数据", SmsTemplateRespVO.class, BeanUtils.toBean(list, SmsTemplateRespVO.class)); } @PostMapping("/send-sms") @Operation(summary = "发送短信") @PreAuthorize("@ss.hasPermission('system:sms-template:send-sms')") public CommonResult sendSms(@Valid @RequestBody SmsTemplateSendReqVO sendReqVO) { return success(smsSendService.sendSingleSmsToAdmin(sendReqVO.getMobile(), null, sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams())); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/sms/vo/channel/SmsChannelPageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.sms.vo.channel; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 短信渠道分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class SmsChannelPageReqVO extends PageParam { @Schema(description = "任务状态", example = "1") private Integer status; @Schema(description = "短信签名,模糊匹配", example = "yshop") private String signature; @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @Schema(description = "创建时间") private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/sms/vo/channel/SmsChannelRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.sms.vo.channel; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.hibernate.validator.constraints.URL; import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; @Schema(description = "管理后台 - 短信渠道 Response VO") @Data public class SmsChannelRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "短信签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @NotNull(message = "短信签名不能为空") private String signature; @Schema(description = "渠道编码,参见 SmsChannelEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "YUN_PIAN") private String code; @Schema(description = "启用状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "启用状态不能为空") private Integer status; @Schema(description = "备注", example = "好吃!") private String remark; @Schema(description = "短信 API 的账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @NotNull(message = "短信 API 的账号不能为空") private String apiKey; @Schema(description = "短信 API 的密钥", example = "yuanma") private String apiSecret; @Schema(description = "短信发送回调 URL", example = "https://www.yixiang.co") @URL(message = "回调 URL 格式不正确") private String callbackUrl; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/sms/vo/channel/SmsChannelSaveReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.sms.vo.channel; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.hibernate.validator.constraints.URL; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 短信渠道创建/修改 Request VO") @Data public class SmsChannelSaveReqVO { @Schema(description = "编号", example = "1024") private Long id; @Schema(description = "短信签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @NotNull(message = "短信签名不能为空") private String signature; @Schema(description = "渠道编码,参见 SmsChannelEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "YUN_PIAN") @NotNull(message = "渠道编码不能为空") private String code; @Schema(description = "启用状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "启用状态不能为空") private Integer status; @Schema(description = "备注", example = "好吃!") private String remark; @Schema(description = "短信 API 的账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @NotNull(message = "短信 API 的账号不能为空") private String apiKey; @Schema(description = "短信 API 的密钥", example = "yuanma") private String apiSecret; @Schema(description = "短信发送回调 URL", example = "http://www.yixiang.co") @URL(message = "回调 URL 格式不正确") private String callbackUrl; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/sms/vo/channel/SmsChannelSimpleRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.sms.vo.channel; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "管理后台 - 短信渠道精简 Response VO") @Data public class SmsChannelSimpleRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "短信签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String signature; @Schema(description = "渠道编码,参见 SmsChannelEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "YUN_PIAN") private String code; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/sms/vo/log/SmsLogPageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.sms.vo.log; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 短信日志分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class SmsLogPageReqVO extends PageParam { @Schema(description = "短信渠道编号", example = "10") private Long channelId; @Schema(description = "模板编号", example = "20") private Long templateId; @Schema(description = "手机号", example = "15601691300") private String mobile; @Schema(description = "发送状态,参见 SmsSendStatusEnum 枚举类", example = "1") private Integer sendStatus; @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @Schema(description = "发送时间") private LocalDateTime[] sendTime; @Schema(description = "接收状态,参见 SmsReceiveStatusEnum 枚举类", example = "0") private Integer receiveStatus; @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @Schema(description = "接收时间") private LocalDateTime[] receiveTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/sms/vo/log/SmsLogRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.sms.vo.log; import co.yixiang.yshop.framework.excel.core.annotations.DictFormat; import co.yixiang.yshop.framework.excel.core.convert.DictConvert; import co.yixiang.yshop.framework.excel.core.convert.JsonConvert; import co.yixiang.yshop.module.system.enums.DictTypeConstants; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; import java.util.Map; @Schema(description = "管理后台 - 短信日志 Response VO") @Data @ExcelIgnoreUnannotated public class SmsLogRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @ExcelProperty("编号") private Long id; @Schema(description = "短信渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") @ExcelProperty("短信渠道编号") private Long channelId; @Schema(description = "短信渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ALIYUN") @ExcelProperty("短信渠道编码") private String channelCode; @Schema(description = "模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") @ExcelProperty("模板编号") private Long templateId; @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test-01") @ExcelProperty("模板编码") private String templateCode; @Schema(description = "短信类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty(value = "短信类型", converter = DictConvert.class) @DictFormat(DictTypeConstants.SMS_TEMPLATE_TYPE) private Integer templateType; @Schema(description = "短信内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你的验证码是 1024") @ExcelProperty("短信内容") private String templateContent; @Schema(description = "短信参数", requiredMode = Schema.RequiredMode.REQUIRED, example = "name,code") @ExcelProperty(value = "短信参数", converter = JsonConvert.class) private Map templateParams; @Schema(description = "短信 API 的模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "SMS_207945135") @ExcelProperty("短信 API 的模板编号") private String apiTemplateId; @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300") @ExcelProperty("手机号") private String mobile; @Schema(description = "用户编号", example = "10") @ExcelProperty("用户编号") private Long userId; @Schema(description = "用户类型", example = "1") @ExcelProperty(value = "用户类型", converter = DictConvert.class) @DictFormat(DictTypeConstants.USER_TYPE) private Integer userType; @Schema(description = "发送状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty(value = "发送状态", converter = DictConvert.class) @DictFormat(DictTypeConstants.SMS_SEND_STATUS) private Integer sendStatus; @Schema(description = "发送时间") @ExcelProperty("发送时间") private LocalDateTime sendTime; @Schema(description = "短信 API 发送结果的编码", example = "SUCCESS") @ExcelProperty("短信 API 发送结果的编码") private String apiSendCode; @Schema(description = "短信 API 发送失败的提示", example = "成功") @ExcelProperty("短信 API 发送失败的提示") private String apiSendMsg; @Schema(description = "短信 API 发送返回的唯一请求 ID", example = "3837C6D3-B96F-428C-BBB2-86135D4B5B99") @ExcelProperty("短信 API 发送返回的唯一请求 ID") private String apiRequestId; @Schema(description = "短信 API 发送返回的序号", example = "62923244790") @ExcelProperty("短信 API 发送返回的序号") private String apiSerialNo; @Schema(description = "接收状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") @ExcelProperty(value = "接收状态", converter = DictConvert.class) @DictFormat(DictTypeConstants.SMS_RECEIVE_STATUS) private Integer receiveStatus; @Schema(description = "接收时间") @ExcelProperty("接收时间") private LocalDateTime receiveTime; @Schema(description = "API 接收结果的编码", example = "DELIVRD") @ExcelProperty("API 接收结果的编码") private String apiReceiveCode; @Schema(description = "API 接收结果的说明", example = "用户接收成功") @ExcelProperty("API 接收结果的说明") private String apiReceiveMsg; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("创建时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/sms/vo/template/SmsTemplatePageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.sms.vo.template; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 短信模板分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class SmsTemplatePageReqVO extends PageParam { @Schema(description = "短信签名", example = "1") private Integer type; @Schema(description = "开启状态", example = "1") private Integer status; @Schema(description = "模板编码,模糊匹配", example = "test_01") private String code; @Schema(description = "模板内容,模糊匹配", example = "你好,{name}。你长的太{like}啦!") private String content; @Schema(description = "短信 API 的模板编号,模糊匹配", example = "4383920") private String apiTemplateId; @Schema(description = "短信渠道编号", example = "10") private Long channelId; @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @Schema(description = "创建时间") private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/sms/vo/template/SmsTemplateRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.sms.vo.template; import co.yixiang.yshop.framework.excel.core.annotations.DictFormat; import co.yixiang.yshop.framework.excel.core.convert.DictConvert; import co.yixiang.yshop.module.system.enums.DictTypeConstants; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; import java.util.List; @Schema(description = "管理后台 - 短信模板 Response VO") @Data @ExcelIgnoreUnannotated public class SmsTemplateRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @ExcelProperty("编号") private Long id; @Schema(description = "短信类型,参见 SmsTemplateTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty(value = "短信签名", converter = DictConvert.class) @DictFormat(DictTypeConstants.SMS_TEMPLATE_TYPE) private Integer type; @Schema(description = "开启状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty(value = "开启状态", converter = DictConvert.class) @DictFormat(DictTypeConstants.COMMON_STATUS) private Integer status; @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01") @ExcelProperty("模板编码") private String code; @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @ExcelProperty("模板名称") private String name; @Schema(description = "模板内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,{name}。你长的太{like}啦!") @ExcelProperty("模板内容") private String content; @Schema(description = "参数数组", example = "name,code") private List params; @Schema(description = "备注", example = "哈哈哈") @ExcelProperty("备注") private String remark; @Schema(description = "短信 API 的模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4383920") @ExcelProperty("短信 API 的模板编号") private String apiTemplateId; @Schema(description = "短信渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") @ExcelProperty("短信渠道编号") private Long channelId; @Schema(description = "短信渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ALIYUN") @ExcelProperty(value = "短信渠道编码", converter = DictConvert.class) @DictFormat(DictTypeConstants.SMS_CHANNEL_CODE) private String channelCode; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("创建时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/sms/vo/template/SmsTemplateSaveReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.sms.vo.template; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 短信模板创建/修改 Request VO") @Data public class SmsTemplateSaveReqVO { @Schema(description = "编号", example = "1024") private Long id; @Schema(description = "短信类型,参见 SmsTemplateTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "短信类型不能为空") private Integer type; @Schema(description = "开启状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "开启状态不能为空") private Integer status; @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01") @NotNull(message = "模板编码不能为空") private String code; @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @NotNull(message = "模板名称不能为空") private String name; @Schema(description = "模板内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,{name}。你长的太{like}啦!") @NotNull(message = "模板内容不能为空") private String content; @Schema(description = "备注", example = "哈哈哈") private String remark; @Schema(description = "短信 API 的模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4383920") @NotNull(message = "短信 API 的模板编号不能为空") private String apiTemplateId; @Schema(description = "短信渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") @NotNull(message = "短信渠道编号不能为空") private Long channelId; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/sms/vo/template/SmsTemplateSendReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.sms.vo.template; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotNull; import java.util.Map; @Schema(description = "管理后台 - 短信模板的发送 Request VO") @Data public class SmsTemplateSendReqVO { @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300") @NotNull(message = "手机号不能为空") private String mobile; @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01") @NotNull(message = "模板编码不能为空") private String templateCode; @Schema(description = "模板参数") private Map templateParams; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/socail/SocialClientController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.socail; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO; import co.yixiang.yshop.module.system.controller.admin.socail.vo.client.SocialClientRespVO; import co.yixiang.yshop.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.social.SocialClientDO; import co.yixiang.yshop.module.system.service.social.SocialClientService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 社交客户端") @RestController @RequestMapping("/system/social-client") @Validated public class SocialClientController { @Resource private SocialClientService socialClientService; @PostMapping("/create") @Operation(summary = "创建社交客户端") @PreAuthorize("@ss.hasPermission('system:social-client:create')") public CommonResult createSocialClient(@Valid @RequestBody SocialClientSaveReqVO createReqVO) { return success(socialClientService.createSocialClient(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新社交客户端") @PreAuthorize("@ss.hasPermission('system:social-client:update')") public CommonResult updateSocialClient(@Valid @RequestBody SocialClientSaveReqVO updateReqVO) { socialClientService.updateSocialClient(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除社交客户端") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('system:social-client:delete')") public CommonResult deleteSocialClient(@RequestParam("id") Long id) { socialClientService.deleteSocialClient(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得社交客户端") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:social-client:query')") public CommonResult getSocialClient(@RequestParam("id") Long id) { SocialClientDO client = socialClientService.getSocialClient(id); return success(BeanUtils.toBean(client, SocialClientRespVO.class)); } @GetMapping("/page") @Operation(summary = "获得社交客户端分页") @PreAuthorize("@ss.hasPermission('system:social-client:query')") public CommonResult> getSocialClientPage(@Valid SocialClientPageReqVO pageVO) { PageResult pageResult = socialClientService.getSocialClientPage(pageVO); return success(BeanUtils.toBean(pageResult, SocialClientRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/socail/SocialUserController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.socail; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.socail.vo.user.SocialUserBindReqVO; import co.yixiang.yshop.module.system.controller.admin.socail.vo.user.SocialUserUnbindReqVO; import co.yixiang.yshop.module.system.controller.admin.socail.vo.user.SocialUserPageReqVO; import co.yixiang.yshop.module.system.controller.admin.socail.vo.user.SocialUserRespVO; import co.yixiang.yshop.module.system.convert.social.SocialUserConvert; import co.yixiang.yshop.module.system.dal.dataobject.social.SocialUserDO; import co.yixiang.yshop.module.system.service.social.SocialUserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @Tag(name = "管理后台 - 社交用户") @RestController @RequestMapping("/system/social-user") @Validated public class SocialUserController { @Resource private SocialUserService socialUserService; @PostMapping("/bind") @Operation(summary = "社交绑定,使用 code 授权码") public CommonResult socialBind(@RequestBody @Valid SocialUserBindReqVO reqVO) { socialUserService.bindSocialUser(SocialUserConvert.INSTANCE.convert( getLoginUserId(), UserTypeEnum.ADMIN.getValue(), reqVO)); return CommonResult.success(true); } @DeleteMapping("/unbind") @Operation(summary = "取消社交绑定") public CommonResult socialUnbind(@RequestBody SocialUserUnbindReqVO reqVO) { socialUserService.unbindSocialUser(getLoginUserId(), UserTypeEnum.ADMIN.getValue(), reqVO.getType(), reqVO.getOpenid()); return CommonResult.success(true); } // ==================== 社交用户 CRUD ==================== @GetMapping("/get") @Operation(summary = "获得社交用户") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:social-user:query')") public CommonResult getSocialUser(@RequestParam("id") Long id) { SocialUserDO socialUser = socialUserService.getSocialUser(id); return success(BeanUtils.toBean(socialUser, SocialUserRespVO.class)); } @GetMapping("/page") @Operation(summary = "获得社交用户分页") @PreAuthorize("@ss.hasPermission('system:social-user:query')") public CommonResult> getSocialUserPage(@Valid SocialUserPageReqVO pageVO) { PageResult pageResult = socialUserService.getSocialUserPage(pageVO); return success(BeanUtils.toBean(pageResult, SocialUserRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/socail/vo/client/SocialClientPageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.socail.vo.client; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @Schema(description = "管理后台 - 社交客户端分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class SocialClientPageReqVO extends PageParam { @Schema(description = "应用名", example = "yshop商城") private String name; @Schema(description = "社交平台的类型", example = "31") private Integer socialType; @Schema(description = "用户类型", example = "2") private Integer userType; @Schema(description = "客户端编号", example = "145442115") private String clientId; @Schema(description = "状态", example = "1") private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/socail/vo/client/SocialClientRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.socail.vo.client; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 社交客户端 Response VO") @Data public class SocialClientRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "27162") private Long id; @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop商城") private String name; @Schema(description = "社交平台的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "31") private Integer socialType; @Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") private Integer userType; @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "wwd411c69a39ad2e54") private String clientId; @Schema(description = "客户端密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "peter") private String clientSecret; @Schema(description = "授权方的网页应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000045") private String agentId; @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer status; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/socail/vo/client/SocialClientSaveReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.socail.vo.client; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.validation.InEnum; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotNull; import java.util.Objects; @Schema(description = "管理后台 - 社交客户端创建/修改 Request VO") @Data public class SocialClientSaveReqVO { @Schema(description = "编号", example = "27162") private Long id; @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop商城") @NotNull(message = "应用名不能为空") private String name; @Schema(description = "社交平台的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "31") @NotNull(message = "社交平台的类型不能为空") @InEnum(SocialTypeEnum.class) private Integer socialType; @Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") @NotNull(message = "用户类型不能为空") @InEnum(UserTypeEnum.class) private Integer userType; @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "wwd411c69a39ad2e54") @NotNull(message = "客户端编号不能为空") private String clientId; @Schema(description = "客户端密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "peter") @NotNull(message = "客户端密钥不能为空") private String clientSecret; @Schema(description = "授权方的网页应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000045") private String agentId; @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "状态不能为空") @InEnum(CommonStatusEnum.class) private Integer status; @AssertTrue(message = "agentId 不能为空") @JsonIgnore public boolean isAgentIdValid() { // 如果是企业微信,必须填写 agentId 属性 return !Objects.equals(socialType, SocialTypeEnum.WECHAT_ENTERPRISE.getType()) || !StrUtil.isEmpty(agentId); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/socail/vo/user/SocialUserBindReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.socail.vo.user; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import co.yixiang.yshop.framework.common.validation.InEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 社交绑定 Request VO,使用 code 授权码") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class SocialUserBindReqVO { @Schema(description = "社交平台的类型,参见 UserSocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") @InEnum(SocialTypeEnum.class) @NotNull(message = "社交平台的类型不能为空") private Integer type; @Schema(description = "授权码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotEmpty(message = "授权码不能为空") private String code; @Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") @NotEmpty(message = "state 不能为空") private String state; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/socail/vo/user/SocialUserPageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.socail.vo.user; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 社交用户分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class SocialUserPageReqVO extends PageParam { @Schema(description = "社交平台的类型", example = "30") private Integer type; @Schema(description = "用户昵称", example = "李四") private String nickname; @Schema(description = "社交 openid", example = "oz-Jdt0kd_jdhUxJHQdBJMlOFN7w") private String openid; @Schema(description = "创建时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/socail/vo/user/SocialUserRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.socail.vo.user; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 社交用户 Response VO") @Data public class SocialUserRespVO { @Schema(description = "主键(自增策略)", requiredMode = Schema.RequiredMode.REQUIRED, example = "14569") private Long id; @Schema(description = "社交平台的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "30") private Integer type; @Schema(description = "社交 openid", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") private String openid; @Schema(description = "社交 token", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") private String token; @Schema(description = "原始 Token 数据,一般是 JSON 格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}") private String rawTokenInfo; @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String nickname; @Schema(description = "用户头像", example = "https://www.yixiang.co/xxx.png") private String avatar; @Schema(description = "原始用户数据,一般是 JSON 格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}") private String rawUserInfo; @Schema(description = "最后一次的认证 code", requiredMode = Schema.RequiredMode.REQUIRED, example = "666666") private String code; @Schema(description = "最后一次的认证 state", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") private String state; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime updateTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/socail/vo/user/SocialUserUnbindReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.socail.vo.user; import co.yixiang.yshop.framework.common.validation.InEnum; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 取消社交绑定 Request VO") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class SocialUserUnbindReqVO { @Schema(description = "社交平台的类型,参见 UserSocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") @InEnum(SocialTypeEnum.class) @NotNull(message = "社交平台的类型不能为空") private Integer type; @Schema(description = "社交用户的 openid", requiredMode = Schema.RequiredMode.REQUIRED, example = "IPRmJ0wvBptiPIlGEZiPewGwiEiE") @NotEmpty(message = "社交用户的 openid 不能为空") private String openid; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/tenant/TenantController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.tenant; import co.yixiang.yshop.framework.apilog.core.annotation.ApiAccessLog; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; import co.yixiang.yshop.module.system.controller.admin.tenant.vo.tenant.TenantRespVO; import co.yixiang.yshop.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO; import co.yixiang.yshop.module.system.controller.admin.tenant.vo.tenant.TenantSimpleRespVO; import co.yixiang.yshop.module.system.dal.dataobject.tenant.TenantDO; import co.yixiang.yshop.module.system.service.tenant.TenantService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.annotation.security.PermitAll; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.io.IOException; import java.util.List; import static co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 租户") @RestController @RequestMapping("/system/tenant") public class TenantController { @Resource private TenantService tenantService; @GetMapping("/get-id-by-name") @PermitAll @Operation(summary = "使用租户名,获得租户编号", description = "登录界面,根据用户的租户名,获得租户编号") @Parameter(name = "name", description = "租户名", required = true, example = "1024") public CommonResult getTenantIdByName(@RequestParam("name") String name) { TenantDO tenant = tenantService.getTenantByName(name); return success(tenant != null ? tenant.getId() : null); } @GetMapping("/get-by-website") @PermitAll @Operation(summary = "使用域名,获得租户信息", description = "登录界面,根据用户的域名,获得租户信息") @Parameter(name = "website", description = "域名", required = true, example = "www.yixiang.co") public CommonResult getTenantByWebsite(@RequestParam("website") String website) { TenantDO tenant = tenantService.getTenantByWebsite(website); return success(BeanUtils.toBean(tenant, TenantSimpleRespVO.class)); } @PostMapping("/create") @Operation(summary = "创建租户") @PreAuthorize("@ss.hasPermission('system:tenant:create')") public CommonResult createTenant(@Valid @RequestBody TenantSaveReqVO createReqVO) { return success(tenantService.createTenant(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新租户") @PreAuthorize("@ss.hasPermission('system:tenant:update')") public CommonResult updateTenant(@Valid @RequestBody TenantSaveReqVO updateReqVO) { tenantService.updateTenant(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除租户") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:tenant:delete')") public CommonResult deleteTenant(@RequestParam("id") Long id) { tenantService.deleteTenant(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得租户") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:tenant:query')") public CommonResult getTenant(@RequestParam("id") Long id) { TenantDO tenant = tenantService.getTenant(id); return success(BeanUtils.toBean(tenant, TenantRespVO.class)); } @GetMapping("/page") @Operation(summary = "获得租户分页") @PreAuthorize("@ss.hasPermission('system:tenant:query')") public CommonResult> getTenantPage(@Valid TenantPageReqVO pageVO) { PageResult pageResult = tenantService.getTenantPage(pageVO); return success(BeanUtils.toBean(pageResult, TenantRespVO.class)); } @GetMapping("/export-excel") @Operation(summary = "导出租户 Excel") @PreAuthorize("@ss.hasPermission('system:tenant:export')") @ApiAccessLog(operateType = EXPORT) public void exportTenantExcel(@Valid TenantPageReqVO exportReqVO, HttpServletResponse response) throws IOException { exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = tenantService.getTenantPage(exportReqVO).getList(); // 导出 Excel ExcelUtils.write(response, "租户.xls", "数据", TenantRespVO.class, BeanUtils.toBean(list, TenantRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/tenant/TenantPackageController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.tenant; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.tenant.vo.packages.*; import co.yixiang.yshop.module.system.dal.dataobject.tenant.TenantPackageDO; import co.yixiang.yshop.module.system.service.tenant.TenantPackageService; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Operation; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 租户套餐") @RestController @RequestMapping("/system/tenant-package") @Validated public class TenantPackageController { @Resource private TenantPackageService tenantPackageService; @PostMapping("/create") @Operation(summary = "创建租户套餐") @PreAuthorize("@ss.hasPermission('system:tenant-package:create')") public CommonResult createTenantPackage(@Valid @RequestBody TenantPackageSaveReqVO createReqVO) { return success(tenantPackageService.createTenantPackage(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新租户套餐") @PreAuthorize("@ss.hasPermission('system:tenant-package:update')") public CommonResult updateTenantPackage(@Valid @RequestBody TenantPackageSaveReqVO updateReqVO) { tenantPackageService.updateTenantPackage(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除租户套餐") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('system:tenant-package:delete')") public CommonResult deleteTenantPackage(@RequestParam("id") Long id) { tenantPackageService.deleteTenantPackage(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得租户套餐") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:tenant-package:query')") public CommonResult getTenantPackage(@RequestParam("id") Long id) { TenantPackageDO tenantPackage = tenantPackageService.getTenantPackage(id); return success(BeanUtils.toBean(tenantPackage, TenantPackageRespVO.class)); } @GetMapping("/page") @Operation(summary = "获得租户套餐分页") @PreAuthorize("@ss.hasPermission('system:tenant-package:query')") public CommonResult> getTenantPackagePage(@Valid TenantPackagePageReqVO pageVO) { PageResult pageResult = tenantPackageService.getTenantPackagePage(pageVO); return success(BeanUtils.toBean(pageResult, TenantPackageRespVO.class)); } @GetMapping({"/get-simple-list", "simple-list"}) @Operation(summary = "获取租户套餐精简信息列表", description = "只包含被开启的租户套餐,主要用于前端的下拉选项") public CommonResult> getTenantPackageList() { List list = tenantPackageService.getTenantPackageListByStatus(CommonStatusEnum.ENABLE.getStatus()); return success(BeanUtils.toBean(list, TenantPackageSimpleRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/tenant/vo/packages/TenantPackagePageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.tenant.vo.packages; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 租户套餐分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class TenantPackagePageReqVO extends PageParam { @Schema(description = "套餐名", example = "VIP") private String name; @Schema(description = "状态", example = "1") private Integer status; @Schema(description = "备注", example = "好") private String remark; @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @Schema(description = "创建时间") private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/tenant/vo/packages/TenantPackageRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.tenant.vo.packages; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; import java.util.Set; @Schema(description = "管理后台 - 租户套餐 Response VO") @Data public class TenantPackageRespVO { @Schema(description = "套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "套餐名", requiredMode = Schema.RequiredMode.REQUIRED, example = "VIP") private String name; @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer status; @Schema(description = "备注", example = "好") private String remark; @Schema(description = "关联的菜单编号", requiredMode = Schema.RequiredMode.REQUIRED) private Set menuIds; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/tenant/vo/packages/TenantPackageSaveReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.tenant.vo.packages; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.validation.InEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.util.Set; @Schema(description = "管理后台 - 租户套餐创建/修改 Request VO") @Data public class TenantPackageSaveReqVO { @Schema(description = "套餐编号", example = "1024") private Long id; @Schema(description = "套餐名", requiredMode = Schema.RequiredMode.REQUIRED, example = "VIP") @NotEmpty(message = "套餐名不能为空") private String name; @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "状态不能为空") @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}") private Integer status; @Schema(description = "备注", example = "好") private String remark; @Schema(description = "关联的菜单编号", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "关联的菜单编号不能为空") private Set menuIds; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/tenant/vo/packages/TenantPackageSimpleRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.tenant.vo.packages; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 租户套餐精简 Response VO") @Data public class TenantPackageSimpleRespVO { @Schema(description = "套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "套餐编号不能为空") private Long id; @Schema(description = "套餐名", requiredMode = Schema.RequiredMode.REQUIRED, example = "VIP") @NotNull(message = "套餐名不能为空") private String name; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/tenant/vo/tenant/TenantPageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.tenant.vo.tenant; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 租户分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class TenantPageReqVO extends PageParam { @Schema(description = "租户名", example = "yshop") private String name; @Schema(description = "联系人", example = "yshop") private String contactName; @Schema(description = "联系手机", example = "15601691300") private String contactMobile; @Schema(description = "租户状态(0正常 1停用)", example = "1") private Integer status; @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @Schema(description = "创建时间") private LocalDateTime[] createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/tenant/vo/tenant/TenantRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.tenant.vo.tenant; import co.yixiang.yshop.framework.excel.core.annotations.DictFormat; import co.yixiang.yshop.framework.excel.core.convert.DictConvert; import co.yixiang.yshop.module.system.enums.DictTypeConstants; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管理后台 - 租户 Response VO") @Data @ExcelIgnoreUnannotated public class TenantRespVO { @Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @ExcelProperty("租户编号") private Long id; @Schema(description = "租户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @ExcelProperty("租户名") private String name; @Schema(description = "联系人", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @ExcelProperty("联系人") private String contactName; @Schema(description = "联系手机", example = "15601691300") @ExcelProperty("联系手机") private String contactMobile; @Schema(description = "租户状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty(value = "状态", converter = DictConvert.class) @DictFormat(DictTypeConstants.COMMON_STATUS) private Integer status; @Schema(description = "绑定域名", example = "https://www.yixiang.co") private String website; @Schema(description = "租户套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long packageId; @Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime expireTime; @Schema(description = "账号数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Integer accountCount; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("创建时间") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/tenant/vo/tenant/TenantSaveReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.tenant.vo.tenant; import cn.hutool.core.util.ObjectUtil; import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.hibernate.validator.constraints.Length; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; import java.time.LocalDateTime; @Schema(description = "管理后台 - 租户创建/修改 Request VO") @Data public class TenantSaveReqVO { @Schema(description = "租户编号", example = "1024") private Long id; @Schema(description = "租户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @NotNull(message = "租户名不能为空") private String name; @Schema(description = "联系人", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @NotNull(message = "联系人不能为空") private String contactName; @Schema(description = "联系手机", example = "15601691300") private String contactMobile; @Schema(description = "租户状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "租户状态") private Integer status; @Schema(description = "绑定域名", example = "https://www.yixiang.co") private String website; @Schema(description = "租户套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "租户套餐编号不能为空") private Long packageId; @Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "过期时间不能为空") private LocalDateTime expireTime; @Schema(description = "账号数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "账号数量不能为空") private Integer accountCount; // ========== 仅【创建】时,需要传递的字段 ========== @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @Pattern(regexp = "^[a-zA-Z0-9]{4,30}$", message = "用户账号由 数字、字母 组成") @Size(min = 4, max = 30, message = "用户账号长度为 4-30 个字符") private String username; @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") @Length(min = 4, max = 16, message = "密码长度为 4-16 位") private String password; @AssertTrue(message = "用户账号、密码不能为空") @JsonIgnore public boolean isUsernameValid() { return id != null // 修改时,不需要传递 || (ObjectUtil.isAllNotEmpty(username, password)); // 新增时,必须都传递 username、password } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/tenant/vo/tenant/TenantSimpleRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.tenant.vo.tenant; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "管理后台 - 租户精简 Response VO") @Data public class TenantSimpleRespVO { @Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "租户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String name; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/user/UserController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.user; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.apilog.core.annotation.ApiAccessLog; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.pojo.PageParam; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.excel.core.util.ExcelUtils; import co.yixiang.yshop.module.system.controller.admin.user.vo.user.*; import co.yixiang.yshop.module.system.convert.user.UserConvert; import co.yixiang.yshop.module.system.dal.dataobject.dept.DeptDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.module.system.enums.common.SexEnum; import co.yixiang.yshop.module.system.service.dept.DeptService; import co.yixiang.yshop.module.system.service.user.AdminUserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.Map; import static co.yixiang.yshop.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertList; @Tag(name = "管理后台 - 用户") @RestController @RequestMapping("/system/user") @Validated public class UserController { @Resource private AdminUserService userService; @Resource private DeptService deptService; @PostMapping("/create") @Operation(summary = "新增用户") @PreAuthorize("@ss.hasPermission('system:user:create')") public CommonResult createUser(@Valid @RequestBody UserSaveReqVO reqVO) { Long id = userService.createUser(reqVO); return success(id); } @PutMapping("update") @Operation(summary = "修改用户") @PreAuthorize("@ss.hasPermission('system:user:update')") public CommonResult updateUser(@Valid @RequestBody UserSaveReqVO reqVO) { userService.updateUser(reqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除用户") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:user:delete')") public CommonResult deleteUser(@RequestParam("id") Long id) { userService.deleteUser(id); return success(true); } @PutMapping("/update-password") @Operation(summary = "重置用户密码") @PreAuthorize("@ss.hasPermission('system:user:update-password')") public CommonResult updateUserPassword(@Valid @RequestBody UserUpdatePasswordReqVO reqVO) { userService.updateUserPassword(reqVO.getId(), reqVO.getPassword()); return success(true); } @PutMapping("/update-status") @Operation(summary = "修改用户状态") @PreAuthorize("@ss.hasPermission('system:user:update')") public CommonResult updateUserStatus(@Valid @RequestBody UserUpdateStatusReqVO reqVO) { userService.updateUserStatus(reqVO.getId(), reqVO.getStatus()); return success(true); } @GetMapping("/page") @Operation(summary = "获得用户分页列表") @PreAuthorize("@ss.hasPermission('system:user:list')") public CommonResult> getUserPage(@Valid UserPageReqVO pageReqVO) { // 获得用户分页列表 PageResult pageResult = userService.getUserPage(pageReqVO); if (CollUtil.isEmpty(pageResult.getList())) { return success(new PageResult<>(pageResult.getTotal())); } // 拼接数据 Map deptMap = deptService.getDeptMap( convertList(pageResult.getList(), AdminUserDO::getDeptId)); return success(new PageResult<>(UserConvert.INSTANCE.convertList(pageResult.getList(), deptMap), pageResult.getTotal())); } @GetMapping({"/list-all-simple", "/simple-list"}) @Operation(summary = "获取用户精简信息列表", description = "只包含被开启的用户,主要用于前端的下拉选项") public CommonResult> getSimpleUserList() { List list = userService.getUserListByStatus(CommonStatusEnum.ENABLE.getStatus()); // 拼接数据 Map deptMap = deptService.getDeptMap( convertList(list, AdminUserDO::getDeptId)); return success(UserConvert.INSTANCE.convertSimpleList(list, deptMap)); } @GetMapping("/get") @Operation(summary = "获得用户详情") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('system:user:query')") public CommonResult getUser(@RequestParam("id") Long id) { AdminUserDO user = userService.getUser(id); if (user == null) { return success(null); } // 拼接数据 DeptDO dept = deptService.getDept(user.getDeptId()); return success(UserConvert.INSTANCE.convert(user, dept)); } @GetMapping("/export") @Operation(summary = "导出用户") @PreAuthorize("@ss.hasPermission('system:user:export')") @ApiAccessLog(operateType = EXPORT) public void exportUserList(@Validated UserPageReqVO exportReqVO, HttpServletResponse response) throws IOException { exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = userService.getUserPage(exportReqVO).getList(); // 输出 Excel Map deptMap = deptService.getDeptMap( convertList(list, AdminUserDO::getDeptId)); ExcelUtils.write(response, "用户数据.xls", "数据", UserRespVO.class, UserConvert.INSTANCE.convertList(list, deptMap)); } @GetMapping("/get-import-template") @Operation(summary = "获得导入用户模板") public void importTemplate(HttpServletResponse response) throws IOException { // 手动创建导出 demo List list = Arrays.asList( UserImportExcelVO.builder().username("yshop").deptId(1L).email("yshop@yixiang.co").mobile("15601691300") .nickname("yshop").status(CommonStatusEnum.ENABLE.getStatus()).sex(SexEnum.MALE.getSex()).build(), UserImportExcelVO.builder().username("yuanma").deptId(2L).email("yuanma@yixiang.co").mobile("15601701300") .nickname("源码").status(CommonStatusEnum.DISABLE.getStatus()).sex(SexEnum.FEMALE.getSex()).build() ); // 输出 ExcelUtils.write(response, "用户导入模板.xls", "用户列表", UserImportExcelVO.class, list); } @PostMapping("/import") @Operation(summary = "导入用户") @Parameters({ @Parameter(name = "file", description = "Excel 文件", required = true), @Parameter(name = "updateSupport", description = "是否支持更新,默认为 false", example = "true") }) @PreAuthorize("@ss.hasPermission('system:user:import')") public CommonResult importExcel(@RequestParam("file") MultipartFile file, @RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) throws Exception { List list = ExcelUtils.read(file, UserImportExcelVO.class); return success(userService.importUserList(list, updateSupport)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/user/UserProfileController.java ================================================ package co.yixiang.yshop.module.system.controller.admin.user; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.datapermission.core.annotation.DataPermission; import co.yixiang.yshop.module.system.controller.admin.user.vo.profile.UserProfileRespVO; import co.yixiang.yshop.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; import co.yixiang.yshop.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; import co.yixiang.yshop.module.system.convert.user.UserConvert; import co.yixiang.yshop.module.system.dal.dataobject.dept.DeptDO; import co.yixiang.yshop.module.system.dal.dataobject.dept.PostDO; import co.yixiang.yshop.module.system.dal.dataobject.permission.RoleDO; import co.yixiang.yshop.module.system.dal.dataobject.social.SocialUserDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.module.system.service.dept.DeptService; import co.yixiang.yshop.module.system.service.dept.PostService; import co.yixiang.yshop.module.system.service.permission.PermissionService; import co.yixiang.yshop.module.system.service.permission.RoleService; import co.yixiang.yshop.module.system.service.social.SocialUserService; import co.yixiang.yshop.module.system.service.user.AdminUserService; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Operation; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import jakarta.annotation.Resource; import jakarta.validation.Valid; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; import static co.yixiang.yshop.module.infra.enums.ErrorCodeConstants.FILE_IS_EMPTY; @Tag(name = "管理后台 - 用户个人中心") @RestController @RequestMapping("/system/user/profile") @Validated @Slf4j public class UserProfileController { @Resource private AdminUserService userService; @Resource private DeptService deptService; @Resource private PostService postService; @Resource private PermissionService permissionService; @Resource private RoleService roleService; @Resource private SocialUserService socialService; @GetMapping("/get") @Operation(summary = "获得登录用户信息") @DataPermission(enable = false) // 关闭数据权限,避免只查看自己时,查询不到部门。 public CommonResult getUserProfile() { // 获得用户基本信息 AdminUserDO user = userService.getUser(getLoginUserId()); // 获得用户角色 List userRoles = roleService.getRoleListFromCache(permissionService.getUserRoleIdListByUserId(user.getId())); // 获得部门信息 DeptDO dept = user.getDeptId() != null ? deptService.getDept(user.getDeptId()) : null; // 获得岗位信息 List posts = CollUtil.isNotEmpty(user.getPostIds()) ? postService.getPostList(user.getPostIds()) : null; // 获得社交用户信息 List socialUsers = socialService.getSocialUserList(user.getId(), UserTypeEnum.ADMIN.getValue()); return success(UserConvert.INSTANCE.convert(user, userRoles, dept, posts, socialUsers)); } @PutMapping("/update") @Operation(summary = "修改用户个人信息") public CommonResult updateUserProfile(@Valid @RequestBody UserProfileUpdateReqVO reqVO) { userService.updateUserProfile(getLoginUserId(), reqVO); return success(true); } @PutMapping("/update-password") @Operation(summary = "修改用户个人密码") public CommonResult updateUserProfilePassword(@Valid @RequestBody UserProfileUpdatePasswordReqVO reqVO) { userService.updateUserPassword(getLoginUserId(), reqVO); return success(true); } @RequestMapping(value = "/update-avatar", method = {RequestMethod.POST, RequestMethod.PUT}) // 解决 uni-app 不支持 Put 上传文件的问题 @Operation(summary = "上传用户个人头像") public CommonResult updateUserAvatar(@RequestParam("avatarFile") MultipartFile file) throws Exception { if (file.isEmpty()) { throw exception(FILE_IS_EMPTY); } String avatar = userService.updateUserAvatar(getLoginUserId(), file.getInputStream()); return success(avatar); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/user/vo/profile/UserProfileRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.user.vo.profile; import co.yixiang.yshop.module.system.controller.admin.dept.vo.dept.DeptSimpleRespVO; import co.yixiang.yshop.module.system.controller.admin.dept.vo.post.PostSimpleRespVO; import co.yixiang.yshop.module.system.controller.admin.permission.vo.role.RoleSimpleRespVO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; import java.util.List; @Data @Schema(description = "管理后台 - 用户个人中心信息 Response VO") public class UserProfileRespVO { @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long id; @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String username; @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String nickname; @Schema(description = "用户邮箱", example = "yshop@yixiang.co") private String email; @Schema(description = "手机号码", example = "15601691300") private String mobile; @Schema(description = "用户性别,参见 SexEnum 枚举类", example = "1") private Integer sex; @Schema(description = "用户头像", example = "https://www.yixiang.co/xxx.png") private String avatar; @Schema(description = "最后登录 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "192.168.1.1") private String loginIp; @Schema(description = "最后登录时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") private LocalDateTime loginDate; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") private LocalDateTime createTime; /** * 所属角色 */ private List roles; /** * 所在部门 */ private DeptSimpleRespVO dept; /** * 所属岗位数组 */ private List posts; /** * 社交用户数组 */ private List socialUsers; @Schema(description = "社交用户") @Data public static class SocialUser { @Schema(description = "社交平台的类型,参见 SocialTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") private Integer type; @Schema(description = "社交用户的 openid", requiredMode = Schema.RequiredMode.REQUIRED, example = "IPRmJ0wvBptiPIlGEZiPewGwiEiE") private String openid; } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/user/vo/profile/UserProfileUpdatePasswordReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.user.vo.profile; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.hibernate.validator.constraints.Length; import jakarta.validation.constraints.NotEmpty; @Schema(description = "管理后台 - 用户个人中心更新密码 Request VO") @Data public class UserProfileUpdatePasswordReqVO { @Schema(description = "旧密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") @NotEmpty(message = "旧密码不能为空") @Length(min = 4, max = 16, message = "密码长度为 4-16 位") private String oldPassword; @Schema(description = "新密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "654321") @NotEmpty(message = "新密码不能为空") @Length(min = 4, max = 16, message = "密码长度为 4-16 位") private String newPassword; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/user/vo/profile/UserProfileUpdateReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.user.vo.profile; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.hibernate.validator.constraints.Length; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Size; @Schema(description = "管理后台 - 用户个人信息更新 Request VO") @Data public class UserProfileUpdateReqVO { @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @Size(max = 30, message = "用户昵称长度不能超过 30 个字符") private String nickname; @Schema(description = "用户邮箱", example = "yshop@yixiang.co") @Email(message = "邮箱格式不正确") @Size(max = 50, message = "邮箱长度不能超过 50 个字符") private String email; @Schema(description = "手机号码", example = "15601691300") @Length(min = 11, max = 11, message = "手机号长度必须 11 位") private String mobile; @Schema(description = "用户性别,参见 SexEnum 枚举类", example = "1") private Integer sex; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/user/vo/user/UserImportExcelVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.user.vo.user; import co.yixiang.yshop.framework.excel.core.annotations.DictFormat; import co.yixiang.yshop.framework.excel.core.convert.DictConvert; import co.yixiang.yshop.module.system.enums.DictTypeConstants; import com.alibaba.excel.annotation.ExcelProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; /** * 用户 Excel 导入 VO */ @Data @Builder @AllArgsConstructor @NoArgsConstructor @Accessors(chain = false) // 设置 chain = false,避免用户导入有问题 public class UserImportExcelVO { @ExcelProperty("登录名称") private String username; @ExcelProperty("用户名称") private String nickname; @ExcelProperty("部门编号") private Long deptId; @ExcelProperty("用户邮箱") private String email; @ExcelProperty("手机号码") private String mobile; @ExcelProperty(value = "用户性别", converter = DictConvert.class) @DictFormat(DictTypeConstants.USER_SEX) private Integer sex; @ExcelProperty(value = "账号状态", converter = DictConvert.class) @DictFormat(DictTypeConstants.COMMON_STATUS) private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/user/vo/user/UserImportRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.user.vo.user; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Data; import java.util.List; import java.util.Map; @Schema(description = "管理后台 - 用户导入 Response VO") @Data @Builder public class UserImportRespVO { @Schema(description = "创建成功的用户名数组", requiredMode = Schema.RequiredMode.REQUIRED) private List createUsernames; @Schema(description = "更新成功的用户名数组", requiredMode = Schema.RequiredMode.REQUIRED) private List updateUsernames; @Schema(description = "导入失败的用户集合,key 为用户名,value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED) private Map failureUsernames; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/user/vo/user/UserPageReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.user.vo.user; import co.yixiang.yshop.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 用户分页 Request VO") @Data @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(callSuper = true) public class UserPageReqVO extends PageParam { @Schema(description = "用户账号,模糊匹配", example = "yshop") private String username; @Schema(description = "手机号码,模糊匹配", example = "yshop") private String mobile; @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") private Integer status; @Schema(description = "创建时间", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; @Schema(description = "部门编号,同时筛选子部门", example = "1024") private Long deptId; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/user/vo/user/UserRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.user.vo.user; import co.yixiang.yshop.framework.excel.core.annotations.DictFormat; import co.yixiang.yshop.framework.excel.core.convert.DictConvert; import co.yixiang.yshop.module.system.enums.DictTypeConstants; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; import java.util.Set; @Schema(description = "管理后台 - 用户信息 Response VO") @Data @ExcelIgnoreUnannotated public class UserRespVO{ @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty("用户编号") private Long id; @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @ExcelProperty("用户名称") private String username; @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @ExcelProperty("用户昵称") private String nickname; @Schema(description = "备注", example = "我是一个用户") private String remark; @Schema(description = "部门ID", example = "我是一个用户") private Long deptId; @Schema(description = "部门名称", example = "IT 部") @ExcelProperty("部门名称") private String deptName; @Schema(description = "岗位编号数组", example = "1") private Set postIds; @Schema(description = "用户邮箱", example = "yshop@yixiang.co") @ExcelProperty("用户邮箱") private String email; @Schema(description = "手机号码", example = "15601691300") @ExcelProperty("手机号码") private String mobile; @Schema(description = "用户性别,参见 SexEnum 枚举类", example = "1") @ExcelProperty(value = "用户性别", converter = DictConvert.class) @DictFormat(DictTypeConstants.USER_SEX) private Integer sex; @Schema(description = "用户头像", example = "https://www.yixiang.co/xxx.png") private String avatar; @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty(value = "帐号状态", converter = DictConvert.class) @DictFormat(DictTypeConstants.COMMON_STATUS) private Integer status; @Schema(description = "最后登录 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "192.168.1.1") @ExcelProperty("最后登录IP") private String loginIp; @Schema(description = "最后登录时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") @ExcelProperty("最后登录时间") private LocalDateTime loginDate; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") private LocalDateTime createTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/user/vo/user/UserSaveReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.user.vo.user; import cn.hutool.core.util.ObjectUtil; import co.yixiang.yshop.framework.common.validation.Mobile; import co.yixiang.yshop.module.system.framework.operatelog.core.DeptParseFunction; import co.yixiang.yshop.module.system.framework.operatelog.core.PostParseFunction; import co.yixiang.yshop.module.system.framework.operatelog.core.SexParseFunction; import com.fasterxml.jackson.annotation.JsonIgnore; import com.mzt.logapi.starter.annotation.DiffLogField; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; import lombok.Data; import org.hibernate.validator.constraints.Length; import java.util.Set; @Schema(description = "管理后台 - 用户创建/修改 Request VO") @Data public class UserSaveReqVO { @Schema(description = "用户编号", example = "1024") private Long id; @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @NotBlank(message = "用户账号不能为空") @Pattern(regexp = "^[a-zA-Z0-9]{4,30}$", message = "用户账号由 数字、字母 组成") @Size(min = 4, max = 30, message = "用户账号长度为 4-30 个字符") @DiffLogField(name = "用户账号") private String username; @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") @Size(max = 30, message = "用户昵称长度不能超过30个字符") @DiffLogField(name = "用户昵称") private String nickname; @Schema(description = "备注", example = "我是一个用户") @DiffLogField(name = "备注") private String remark; @Schema(description = "部门编号", example = "我是一个用户") @DiffLogField(name = "部门", function = DeptParseFunction.NAME) private Long deptId; @Schema(description = "岗位编号数组", example = "1") @DiffLogField(name = "岗位", function = PostParseFunction.NAME) private Set postIds; @Schema(description = "用户邮箱", example = "yshop@yixiang.co") @Email(message = "邮箱格式不正确") @Size(max = 50, message = "邮箱长度不能超过 50 个字符") @DiffLogField(name = "用户邮箱") private String email; @Schema(description = "手机号码", example = "15601691300") @Mobile @DiffLogField(name = "手机号码") private String mobile; @Schema(description = "用户性别,参见 SexEnum 枚举类", example = "1") @DiffLogField(name = "用户性别", function = SexParseFunction.NAME) private Integer sex; @Schema(description = "用户头像", example = "https://www.yixiang.co/xxx.png") @DiffLogField(name = "用户头像") private String avatar; // ========== 仅【创建】时,需要传递的字段 ========== @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") @Length(min = 4, max = 16, message = "密码长度为 4-16 位") private String password; @AssertTrue(message = "密码不能为空") @JsonIgnore public boolean isPasswordValid() { return id != null // 修改时,不需要传递 || (ObjectUtil.isAllNotEmpty(password)); // 新增时,必须都传递 password } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/user/vo/user/UserSimpleRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.user.vo.user; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Schema(description = "管理后台 - 用户精简信息 Response VO") @Data @NoArgsConstructor @AllArgsConstructor public class UserSimpleRespVO { @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String nickname; @Schema(description = "部门ID", example = "我是一个用户") private Long deptId; @Schema(description = "部门名称", example = "IT 部") private String deptName; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/user/vo/user/UserUpdatePasswordReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.user.vo.user; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.hibernate.validator.constraints.Length; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 用户更新密码 Request VO") @Data public class UserUpdatePasswordReqVO { @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "用户编号不能为空") private Long id; @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") @NotEmpty(message = "密码不能为空") @Length(min = 4, max = 16, message = "密码长度为 4-16 位") private String password; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/admin/user/vo/user/UserUpdateStatusReqVO.java ================================================ package co.yixiang.yshop.module.system.controller.admin.user.vo.user; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.validation.InEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.NotNull; @Schema(description = "管理后台 - 用户更新状态 Request VO") @Data public class UserUpdateStatusReqVO { @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "角色编号不能为空") private Long id; @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "状态不能为空") @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/app/dict/AppDictDataController.java ================================================ package co.yixiang.yshop.module.system.controller.app.dict; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.app.dict.vo.AppDictDataRespVO; import co.yixiang.yshop.module.system.dal.dataobject.dict.DictDataDO; import co.yixiang.yshop.module.system.service.dict.DictDataService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import jakarta.annotation.Resource; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "用户 App - 字典数据") @RestController @RequestMapping("/system/dict-data") @Validated public class AppDictDataController { @Resource private DictDataService dictDataService; @GetMapping("/type") @Operation(summary = "根据字典类型查询字典数据信息") @Parameter(name = "type", description = "字典类型", required = true, example = "common_status") public CommonResult> getDictDataListByType(@RequestParam("type") String type) { List list = dictDataService.getDictDataList( CommonStatusEnum.ENABLE.getStatus(), type); return success(BeanUtils.toBean(list, AppDictDataRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/app/dict/vo/AppDictDataRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.app.dict.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Schema(description = "用户 App - 字典数据信息 Response VO") @Data @NoArgsConstructor @AllArgsConstructor public class AppDictDataRespVO { @Schema(description = "字典数据编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "字典标签", requiredMode = Schema.RequiredMode.REQUIRED, example = "yshop") private String label; @Schema(description = "字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "yixiang.co") private String value; @Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "sys_common_sex") private String dictType; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/app/ip/AppAreaController.java ================================================ package co.yixiang.yshop.module.system.controller.app.ip; import cn.hutool.core.lang.Assert; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.ip.core.Area; import co.yixiang.yshop.framework.ip.core.utils.AreaUtils; import co.yixiang.yshop.module.system.controller.app.ip.vo.AppAreaNodeRespVO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; @Tag(name = "用户 App - 地区") @RestController @RequestMapping("/system/area") @Validated public class AppAreaController { @GetMapping("/tree") @Operation(summary = "获得地区树") public CommonResult> getAreaTree() { Area area = AreaUtils.getArea(Area.ID_CHINA); Assert.notNull(area, "获取不到中国"); return success(BeanUtils.toBean(area.getChildren(), AppAreaNodeRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/app/ip/vo/AppAreaNodeRespVO.java ================================================ package co.yixiang.yshop.module.system.controller.app.ip.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.List; @Schema(description = "用户 App - 地区节点 Response VO") @Data public class AppAreaNodeRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "110000") private Integer id; @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "北京") private String name; /** * 子节点 */ private List children; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/controller/app/notice/AppNoticeController.java ================================================ /** * Copyright (C) 2018-2022 * All rights reserved, Designed By www.yixiang.co * 注意: * 本软件为www.yixiang.co开发研制,未经购买不得使用 * 购买后可获得全部源代码(禁止转卖、分享、上传到码云、github等开源平台) * 一经发现盗用、分享等行为,将追究法律责任,后果自负 */ package co.yixiang.yshop.module.system.controller.app.notice; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.notice.vo.NoticePageReqVO; import co.yixiang.yshop.module.system.controller.admin.notice.vo.NoticeRespVO; import co.yixiang.yshop.module.system.dal.dataobject.notice.NoticeDO; import co.yixiang.yshop.module.system.service.notice.NoticeService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; import static co.yixiang.yshop.framework.common.pojo.CommonResult.success; /** *

* 新闻控制器 *

* * @author hupeng * @since 2024-5-6 */ @Slf4j @RestController @Tag(name = "用户 APP - 新闻") @RequiredArgsConstructor(onConstructor = @__(@Autowired)) @RequestMapping("/news") public class AppNoticeController { private final NoticeService noticeService; @GetMapping("/list") @Operation(summary = "列表") public CommonResult> getList() { NoticePageReqVO noticePageReqVO = new NoticePageReqVO(); noticePageReqVO.setPageSize(2); List noticeDOS = noticeService.getNoticePage(noticePageReqVO).getList(); return success(BeanUtils.toBean(noticeDOS, NoticeRespVO.class)); } @GetMapping("/detail/{id}") @Operation(summary = "详情") public CommonResult getDetail(@PathVariable Long id) { return success(BeanUtils.toBean(noticeService.getNotice(id), NoticeRespVO.class)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/convert/auth/AuthConvert.java ================================================ package co.yixiang.yshop.module.system.convert.auth; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeSendReqDTO; import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeUseReqDTO; import co.yixiang.yshop.module.system.api.social.dto.SocialUserBindReqDTO; import co.yixiang.yshop.module.system.controller.admin.auth.vo.*; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import co.yixiang.yshop.module.system.dal.dataobject.permission.MenuDO; import co.yixiang.yshop.module.system.dal.dataobject.permission.RoleDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.module.system.enums.permission.MenuTypeEnum; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import org.slf4j.LoggerFactory; import java.util.*; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertSet; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.filterList; import static co.yixiang.yshop.module.system.dal.dataobject.permission.MenuDO.ID_ROOT; @Mapper public interface AuthConvert { AuthConvert INSTANCE = Mappers.getMapper(AuthConvert.class); AuthLoginRespVO convert(OAuth2AccessTokenDO bean); default AuthPermissionInfoRespVO convert(AdminUserDO user, List roleList, List menuList) { Long shopId = SecurityFrameworkUtils.getLoginUser().getShopId(); return AuthPermissionInfoRespVO.builder() .user(BeanUtils.toBean(user, AuthPermissionInfoRespVO.UserVO.class).setShopId(shopId)) .roles(convertSet(roleList, RoleDO::getCode)) // 权限标识信息 .permissions(convertSet(menuList, MenuDO::getPermission)) // 菜单树 .menus(buildMenuTree(menuList)) .build(); } AuthPermissionInfoRespVO.MenuVO convertTreeNode(MenuDO menu); /** * 将菜单列表,构建成菜单树 * * @param menuList 菜单列表 * @return 菜单树 */ default List buildMenuTree(List menuList) { if (CollUtil.isEmpty(menuList)) { return Collections.emptyList(); } // 移除按钮 menuList.removeIf(menu -> menu.getType().equals(MenuTypeEnum.BUTTON.getType())); // 排序,保证菜单的有序性 menuList.sort(Comparator.comparing(MenuDO::getSort)); // 构建菜单树 // 使用 LinkedHashMap 的原因,是为了排序 。实际也可以用 Stream API ,就是太丑了。 Map treeNodeMap = new LinkedHashMap<>(); menuList.forEach(menu -> treeNodeMap.put(menu.getId(), AuthConvert.INSTANCE.convertTreeNode(menu))); // 处理父子关系 treeNodeMap.values().stream().filter(node -> !node.getParentId().equals(ID_ROOT)).forEach(childNode -> { // 获得父节点 AuthPermissionInfoRespVO.MenuVO parentNode = treeNodeMap.get(childNode.getParentId()); if (parentNode == null) { LoggerFactory.getLogger(getClass()).error("[buildRouterTree][resource({}) 找不到父资源({})]", childNode.getId(), childNode.getParentId()); return; } // 将自己添加到父节点中 if (parentNode.getChildren() == null) { parentNode.setChildren(new ArrayList<>()); } parentNode.getChildren().add(childNode); }); // 获得到所有的根节点 return filterList(treeNodeMap.values(), node -> ID_ROOT.equals(node.getParentId())); } SocialUserBindReqDTO convert(Long userId, Integer userType, AuthSocialLoginReqVO reqVO); SmsCodeSendReqDTO convert(AuthSmsSendReqVO reqVO); SmsCodeUseReqDTO convert(AuthSmsLoginReqVO reqVO, Integer scene, String usedIp); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/convert/oauth2/OAuth2OpenConvert.java ================================================ package co.yixiang.yshop.module.system.convert.oauth2; import cn.hutool.core.date.LocalDateTimeUtil; import co.yixiang.yshop.framework.common.core.KeyValue; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAuthorizeInfoRespVO; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2ClientDO; import co.yixiang.yshop.module.system.util.oauth2.OAuth2Utils; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import java.util.ArrayList; import java.util.List; import java.util.Map; @Mapper public interface OAuth2OpenConvert { OAuth2OpenConvert INSTANCE = Mappers.getMapper(OAuth2OpenConvert.class); default OAuth2OpenAccessTokenRespVO convert(OAuth2AccessTokenDO bean) { OAuth2OpenAccessTokenRespVO respVO = BeanUtils.toBean(bean, OAuth2OpenAccessTokenRespVO.class); respVO.setTokenType(SecurityFrameworkUtils.AUTHORIZATION_BEARER.toLowerCase()); respVO.setExpiresIn(OAuth2Utils.getExpiresIn(bean.getExpiresTime())); respVO.setScope(OAuth2Utils.buildScopeStr(bean.getScopes())); return respVO; } default OAuth2OpenCheckTokenRespVO convert2(OAuth2AccessTokenDO bean) { OAuth2OpenCheckTokenRespVO respVO = BeanUtils.toBean(bean, OAuth2OpenCheckTokenRespVO.class); respVO.setExp(LocalDateTimeUtil.toEpochMilli(bean.getExpiresTime()) / 1000L); respVO.setUserType(UserTypeEnum.ADMIN.getValue()); return respVO; } default OAuth2OpenAuthorizeInfoRespVO convert(OAuth2ClientDO client, List approves) { // 构建 scopes List> scopes = new ArrayList<>(client.getScopes().size()); Map approveMap = CollectionUtils.convertMap(approves, OAuth2ApproveDO::getScope); client.getScopes().forEach(scope -> { OAuth2ApproveDO approve = approveMap.get(scope); scopes.add(new KeyValue<>(scope, approve != null ? approve.getApproved() : false)); }); // 拼接返回 return new OAuth2OpenAuthorizeInfoRespVO( new OAuth2OpenAuthorizeInfoRespVO.Client(client.getName(), client.getLogo()), scopes); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/convert/social/SocialUserConvert.java ================================================ package co.yixiang.yshop.module.system.convert.social; import co.yixiang.yshop.module.system.api.social.dto.SocialUserBindReqDTO; import co.yixiang.yshop.module.system.controller.admin.socail.vo.user.SocialUserBindReqVO; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.factory.Mappers; @Mapper public interface SocialUserConvert { SocialUserConvert INSTANCE = Mappers.getMapper(SocialUserConvert.class); @Mapping(source = "reqVO.type", target = "socialType") SocialUserBindReqDTO convert(Long userId, Integer userType, SocialUserBindReqVO reqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/convert/tenant/TenantConvert.java ================================================ package co.yixiang.yshop.module.system.convert.tenant; import co.yixiang.yshop.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO; import co.yixiang.yshop.module.system.controller.admin.user.vo.user.UserSaveReqVO; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; /** * 租户 Convert * * @author yshop */ @Mapper public interface TenantConvert { TenantConvert INSTANCE = Mappers.getMapper(TenantConvert.class); default UserSaveReqVO convert02(TenantSaveReqVO bean) { UserSaveReqVO reqVO = new UserSaveReqVO(); reqVO.setUsername(bean.getUsername()); reqVO.setPassword(bean.getPassword()); reqVO.setNickname(bean.getContactName()).setMobile(bean.getContactMobile()); return reqVO; } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/convert/user/UserConvert.java ================================================ package co.yixiang.yshop.module.system.convert.user; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.framework.common.util.collection.MapUtils; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.dept.vo.dept.DeptSimpleRespVO; import co.yixiang.yshop.module.system.controller.admin.dept.vo.post.PostSimpleRespVO; import co.yixiang.yshop.module.system.controller.admin.permission.vo.role.RoleSimpleRespVO; import co.yixiang.yshop.module.system.controller.admin.user.vo.profile.UserProfileRespVO; import co.yixiang.yshop.module.system.controller.admin.user.vo.user.UserRespVO; import co.yixiang.yshop.module.system.controller.admin.user.vo.user.UserSimpleRespVO; import co.yixiang.yshop.module.system.dal.dataobject.dept.DeptDO; import co.yixiang.yshop.module.system.dal.dataobject.dept.PostDO; import co.yixiang.yshop.module.system.dal.dataobject.permission.RoleDO; import co.yixiang.yshop.module.system.dal.dataobject.social.SocialUserDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import java.util.List; import java.util.Map; @Mapper public interface UserConvert { UserConvert INSTANCE = Mappers.getMapper(UserConvert.class); default List convertList(List list, Map deptMap) { return CollectionUtils.convertList(list, user -> convert(user, deptMap.get(user.getDeptId()))); } default UserRespVO convert(AdminUserDO user, DeptDO dept) { UserRespVO userVO = BeanUtils.toBean(user, UserRespVO.class); if (dept != null) { userVO.setDeptName(dept.getName()); } return userVO; } default List convertSimpleList(List list, Map deptMap) { return CollectionUtils.convertList(list, user -> { UserSimpleRespVO userVO = BeanUtils.toBean(user, UserSimpleRespVO.class); MapUtils.findAndThen(deptMap, user.getDeptId(), dept -> userVO.setDeptName(dept.getName())); return userVO; }); } default UserProfileRespVO convert(AdminUserDO user, List userRoles, DeptDO dept, List posts, List socialUsers) { UserProfileRespVO userVO = BeanUtils.toBean(user, UserProfileRespVO.class); userVO.setRoles(BeanUtils.toBean(userRoles, RoleSimpleRespVO.class)); userVO.setDept(BeanUtils.toBean(dept, DeptSimpleRespVO.class)); userVO.setPosts(BeanUtils.toBean(posts, PostSimpleRespVO.class)); userVO.setSocialUsers(BeanUtils.toBean(socialUsers, UserProfileRespVO.SocialUser.class)); return userVO; } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/dept/DeptDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.dept; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.tenant.core.db.TenantBaseDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; /** * 部门表 * * @author ruoyi * @author yshop */ @TableName("system_dept") @KeySequence("system_dept_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) public class DeptDO extends TenantBaseDO { public static final Long PARENT_ID_ROOT = 0L; /** * 部门ID */ @TableId private Long id; /** * 部门名称 */ private String name; /** * 父部门ID * * 关联 {@link #id} */ private Long parentId; /** * 显示顺序 */ private Integer sort; /** * 负责人 * * 关联 {@link AdminUserDO#getId()} */ private Long leaderUserId; /** * 联系电话 */ private String phone; /** * 邮箱 */ private String email; /** * 部门状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/dept/PostDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.dept; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; /** * 岗位表 * * @author ruoyi */ @TableName("system_post") @KeySequence("system_post_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) public class PostDO extends BaseDO { /** * 岗位序号 */ @TableId private Long id; /** * 岗位名称 */ private String name; /** * 岗位编码 */ private String code; /** * 岗位排序 */ private Integer sort; /** * 状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; /** * 备注 */ private String remark; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/dept/UserPostDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.dept; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; /** * 用户和岗位关联 * * @author ruoyi */ @TableName("system_user_post") @KeySequence("system_user_post_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) public class UserPostDO extends BaseDO { /** * 自增主键 */ @TableId private Long id; /** * 用户 ID * * 关联 {@link AdminUserDO#getId()} */ private Long userId; /** * 角色 ID * * 关联 {@link PostDO#getId()} */ private Long postId; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/dict/DictDataDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.dict; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import lombok.EqualsAndHashCode; /** * 字典数据表 * * @author ruoyi */ @TableName("system_dict_data") @KeySequence("system_dict_data_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) public class DictDataDO extends BaseDO { /** * 字典数据编号 */ @TableId private Long id; /** * 字典排序 */ private Integer sort; /** * 字典标签 */ private String label; /** * 字典值 */ private String value; /** * 字典类型 * * 冗余 {@link DictDataDO#getDictType()} */ private String dictType; /** * 状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; /** * 颜色类型 * * 对应到 element-ui 为 default、primary、success、info、warning、danger */ private String colorType; /** * css 样式 */ @TableField(updateStrategy = FieldStrategy.ALWAYS) private String cssClass; /** * 备注 */ private String remark; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/dict/DictTypeDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.dict; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; import java.time.LocalDateTime; /** * 字典类型表 * * @author ruoyi */ @TableName("system_dict_type") @KeySequence("system_dict_type_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class DictTypeDO extends BaseDO { /** * 字典主键 */ @TableId private Long id; /** * 字典名称 */ private String name; /** * 字典类型 */ private String type; /** * 状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; /** * 备注 */ private String remark; /** * 删除时间 */ private LocalDateTime deletedTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/logger/LoginLogDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.logger; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.system.enums.logger.LoginLogTypeEnum; import co.yixiang.yshop.module.system.enums.logger.LoginResultEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; /** * 登录日志表 * * 注意,包括登录和登出两种行为 * * @author yshop */ @TableName("system_login_log") @KeySequence("system_login_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class LoginLogDO extends BaseDO { /** * 日志主键 */ private Long id; /** * 日志类型 * * 枚举 {@link LoginLogTypeEnum} */ private Integer logType; /** * 链路追踪编号 */ private String traceId; /** * 用户编号 */ private Long userId; /** * 用户类型 * * 枚举 {@link UserTypeEnum} */ private Integer userType; /** * 用户账号 * * 冗余,因为账号可以变更 */ private String username; /** * 登录结果 * * 枚举 {@link LoginResultEnum} */ private Integer result; /** * 用户 IP */ private String userIp; /** * 浏览器 UA */ private String userAgent; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/logger/OperateLogDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.logger; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; /** * 操作日志表 * * @author yshop */ @TableName(value = "system_operate_log", autoResultMap = true) @KeySequence("system_operate_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data public class OperateLogDO extends BaseDO { /** * 日志主键 */ @TableId private Long id; /** * 链路追踪编号 * * 一般来说,通过链路追踪编号,可以将访问日志,错误日志,链路追踪日志,logger 打印日志等,结合在一起,从而进行排错。 */ private String traceId; /** * 用户编号 * * 关联 MemberUserDO 的 id 属性,或者 AdminUserDO 的 id 属性 */ private Long userId; /** * 用户类型 * * 关联 {@link UserTypeEnum} */ private Integer userType; /** * 操作模块类型 */ private String type; /** * 操作名 */ private String subType; /** * 操作模块业务编号 */ private Long bizId; /** * 日志内容,记录整个操作的明细 * * 例如说,修改编号为 1 的用户信息,将性别从男改成女,将姓名从yshop改成源码。 */ private String action; /** * 拓展字段,有些复杂的业务,需要记录一些字段 ( JSON 格式 ) * * 例如说,记录订单编号,{ orderId: "1"} */ private String extra; /** * 请求方法名 */ private String requestMethod; /** * 请求地址 */ private String requestUrl; /** * 用户 IP */ private String userIp; /** * 浏览器 UA */ private String userAgent; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/mail/MailAccountDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.mail; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; /** * 邮箱账号 DO * * 用途:配置发送邮箱的账号 * * @author wangjingyi * @since 2022-03-21 */ @TableName(value = "system_mail_account", autoResultMap = true) @Data @EqualsAndHashCode(callSuper = true) public class MailAccountDO extends BaseDO { /** * 主键 */ @TableId private Long id; /** * 邮箱 */ private String mail; /** * 用户名 */ private String username; /** * 密码 */ private String password; /** * SMTP 服务器域名 */ private String host; /** * SMTP 服务器端口 */ private Integer port; /** * 是否开启 SSL */ private Boolean sslEnable; /** * 是否开启 STARTTLS */ private Boolean starttlsEnable; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/mail/MailLogDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.mail; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.system.enums.mail.MailSendStatusEnum; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import lombok.*; import java.io.Serializable; import java.time.LocalDateTime; import java.util.Map; /** * 邮箱日志 DO * 记录每一次邮件的发送 * * @author wangjingyi * @since 2022-03-21 */ @TableName(value = "system_mail_log", autoResultMap = true) @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @AllArgsConstructor @NoArgsConstructor public class MailLogDO extends BaseDO implements Serializable { /** * 日志编号,自增 */ private Long id; /** * 用户编码 */ private Long userId; /** * 用户类型 * * 枚举 {@link UserTypeEnum} */ private Integer userType; /** * 接收邮箱地址 */ private String toMail; /** * 邮箱账号编号 * * 关联 {@link MailAccountDO#getId()} */ private Long accountId; /** * 发送邮箱地址 * * 冗余 {@link MailAccountDO#getMail()} */ private String fromMail; // ========= 模板相关字段 ========= /** * 模版编号 * * 关联 {@link MailTemplateDO#getId()} */ private Long templateId; /** * 模版编码 * * 冗余 {@link MailTemplateDO#getCode()} */ private String templateCode; /** * 模版发送人名称 * * 冗余 {@link MailTemplateDO#getNickname()} */ private String templateNickname; /** * 模版标题 */ private String templateTitle; /** * 模版内容 * * 基于 {@link MailTemplateDO#getContent()} 格式化后的内容 */ private String templateContent; /** * 模版参数 * * 基于 {@link MailTemplateDO#getParams()} 输入后的参数 */ @TableField(typeHandler = JacksonTypeHandler.class) private Map templateParams; // ========= 发送相关字段 ========= /** * 发送状态 * * 枚举 {@link MailSendStatusEnum} */ private Integer sendStatus; /** * 发送时间 */ private LocalDateTime sendTime; /** * 发送返回的消息 ID */ private String sendMessageId; /** * 发送异常 */ private String sendException; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/mail/MailTemplateDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.mail; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import lombok.Data; import lombok.EqualsAndHashCode; import java.util.List; /** * 邮件模版 DO * * @author wangjingyi * @since 2022-03-21 */ @TableName(value = "system_mail_template", autoResultMap = true) @Data @EqualsAndHashCode(callSuper = true) public class MailTemplateDO extends BaseDO { /** * 主键 */ private Long id; /** * 模版名称 */ private String name; /** * 模版编号 */ private String code; /** * 发送的邮箱账号编号 * * 关联 {@link MailAccountDO#getId()} */ private Long accountId; /** * 发送人名称 */ private String nickname; /** * 标题 */ private String title; /** * 内容 */ private String content; /** * 参数数组(自动根据内容生成) */ @TableField(typeHandler = JacksonTypeHandler.class) private List params; /** * 状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; /** * 备注 */ private String remark; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/notice/NoticeDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.notice; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.system.enums.notice.NoticeTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; /** * 通知公告表 * * @author ruoyi */ @TableName("system_notice") @KeySequence("system_notice_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) public class NoticeDO extends BaseDO { /** * 公告ID */ private Long id; /** * 公告标题 */ private String title; /** * 公告类型 * * 枚举 {@link NoticeTypeEnum} */ private Integer type; /** * 公告内容 */ private String content; /** * 公告状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/notify/NotifyMessageDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.notify; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailTemplateDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import lombok.*; import java.time.LocalDateTime; import java.util.Date; import java.util.Map; /** * 站内信 DO * * @author xrcoder */ @TableName(value = "system_notify_message", autoResultMap = true) @KeySequence("system_notify_message_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class NotifyMessageDO extends BaseDO { /** * 站内信编号,自增 */ @TableId private Long id; /** * 用户编号 * * 关联 MemberUserDO 的 id 字段、或者 AdminUserDO 的 id 字段 */ private Long userId; /** * 用户类型 * * 枚举 {@link UserTypeEnum} */ private Integer userType; // ========= 模板相关字段 ========= /** * 模版编号 * * 关联 {@link NotifyTemplateDO#getId()} */ private Long templateId; /** * 模版编码 * * 关联 {@link NotifyTemplateDO#getCode()} */ private String templateCode; /** * 模版类型 * * 冗余 {@link NotifyTemplateDO#getType()} */ private Integer templateType; /** * 模版发送人名称 * * 冗余 {@link NotifyTemplateDO#getNickname()} */ private String templateNickname; /** * 模版内容 * * 基于 {@link NotifyTemplateDO#getContent()} 格式化后的内容 */ private String templateContent; /** * 模版参数 * * 基于 {@link NotifyTemplateDO#getParams()} 输入后的参数 */ @TableField(typeHandler = JacksonTypeHandler.class) private Map templateParams; // ========= 读取相关字段 ========= /** * 是否已读 */ private Boolean readStatus; /** * 阅读时间 */ private LocalDateTime readTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/notify/NotifyTemplateDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.notify; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import lombok.*; import java.util.List; /** * 站内信模版 DO * * @author xrcoder */ @TableName(value = "system_notify_template", autoResultMap = true) @KeySequence("system_notify_template_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class NotifyTemplateDO extends BaseDO { /** * ID */ @TableId private Long id; /** * 模版名称 */ private String name; /** * 模版编码 */ private String code; /** * 模版类型 * * 对应 system_notify_template_type 字典 */ private Integer type; /** * 发送人名称 */ private String nickname; /** * 模版内容 */ private String content; /** * 参数数组 */ @TableField(typeHandler = JacksonTypeHandler.class) private List params; /** * 状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; /** * 备注 */ private String remark; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/oauth2/OAuth2AccessTokenDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.oauth2; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.tenant.core.db.TenantBaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import lombok.Data; import lombok.EqualsAndHashCode; import java.time.LocalDateTime; import java.util.List; import java.util.Map; /** * OAuth2 访问令牌 DO * * 如下字段,暂时未使用,暂时不支持: * user_name、authentication(用户信息) * * @author yshop */ @TableName(value = "system_oauth2_access_token", autoResultMap = true) @KeySequence("system_oauth2_access_token_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) public class OAuth2AccessTokenDO extends TenantBaseDO { /** * 编号,数据库递增 */ @TableId private Long id; /** * 访问令牌 */ private String accessToken; /** * 刷新令牌 */ private String refreshToken; /** * 用户编号 */ private Long userId; /** * 用户类型 * * 枚举 {@link UserTypeEnum} */ private Integer userType; /** * 用户信息 */ @TableField(typeHandler = JacksonTypeHandler.class) private Map userInfo; /** * 客户端编号 * * 关联 {@link OAuth2ClientDO#getId()} */ private String clientId; /** * 授权范围 */ @TableField(typeHandler = JacksonTypeHandler.class) private List scopes; /** * 过期时间 */ private LocalDateTime expiresTime; /** * 门店id */ private Long shopId; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/oauth2/OAuth2ApproveDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.oauth2; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; import java.time.LocalDateTime; /** * OAuth2 批准 DO * * 用户在 sso.vue 界面时,记录接受的 scope 列表 * * @author yshop */ @TableName(value = "system_oauth2_approve", autoResultMap = true) @KeySequence("system_oauth2_approve_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) public class OAuth2ApproveDO extends BaseDO { /** * 编号,数据库自增 */ @TableId private Long id; /** * 用户编号 */ private Long userId; /** * 用户类型 * * 枚举 {@link UserTypeEnum} */ private Integer userType; /** * 客户端编号 * * 关联 {@link OAuth2ClientDO#getId()} */ private String clientId; /** * 授权范围 */ private String scope; /** * 是否接受 * * true - 接受 * false - 拒绝 */ private Boolean approved; /** * 过期时间 */ private LocalDateTime expiresTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/oauth2/OAuth2ClientDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.oauth2; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.system.enums.oauth2.OAuth2GrantTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import lombok.Data; import lombok.EqualsAndHashCode; import java.util.List; /** * OAuth2 客户端 DO * * @author yshop */ @TableName(value = "system_oauth2_client", autoResultMap = true) @KeySequence("system_oauth2_client_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) public class OAuth2ClientDO extends BaseDO { /** * 编号,数据库自增 * * 由于 SQL Server 在存储 String 主键有点问题,所以暂时使用 Long 类型 */ @TableId private Long id; /** * 客户端编号 */ private String clientId; /** * 客户端密钥 */ private String secret; /** * 应用名 */ private String name; /** * 应用图标 */ private String logo; /** * 应用描述 */ private String description; /** * 状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; /** * 访问令牌的有效期 */ private Integer accessTokenValiditySeconds; /** * 刷新令牌的有效期 */ private Integer refreshTokenValiditySeconds; /** * 可重定向的 URI 地址 */ @TableField(typeHandler = JacksonTypeHandler.class) private List redirectUris; /** * 授权类型(模式) * * 枚举 {@link OAuth2GrantTypeEnum} */ @TableField(typeHandler = JacksonTypeHandler.class) private List authorizedGrantTypes; /** * 授权范围 */ @TableField(typeHandler = JacksonTypeHandler.class) private List scopes; /** * 自动授权的 Scope * * code 授权时,如果 scope 在这个范围内,则自动通过 */ @TableField(typeHandler = JacksonTypeHandler.class) private List autoApproveScopes; /** * 权限 */ @TableField(typeHandler = JacksonTypeHandler.class) private List authorities; /** * 资源 */ @TableField(typeHandler = JacksonTypeHandler.class) private List resourceIds; /** * 附加信息,JSON 格式 */ private String additionalInformation; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/oauth2/OAuth2CodeDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.oauth2; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import lombok.Data; import lombok.EqualsAndHashCode; import java.time.LocalDateTime; import java.util.List; /** * OAuth2 授权码 DO * * @author yshop */ @TableName(value = "system_oauth2_code", autoResultMap = true) @KeySequence("system_oauth2_code_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) public class OAuth2CodeDO extends BaseDO { /** * 编号,数据库递增 */ private Long id; /** * 授权码 */ private String code; /** * 用户编号 */ private Long userId; /** * 用户类型 * * 枚举 {@link UserTypeEnum} */ private Integer userType; /** * 客户端编号 * * 关联 {@link OAuth2ClientDO#getClientId()} */ private String clientId; /** * 授权范围 */ @TableField(typeHandler = JacksonTypeHandler.class) private List scopes; /** * 重定向地址 */ private String redirectUri; /** * 状态 */ private String state; /** * 过期时间 */ private LocalDateTime expiresTime; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/oauth2/OAuth2RefreshTokenDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.oauth2; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; import java.time.LocalDateTime; import java.util.List; /** * OAuth2 刷新令牌 * * @author yshop */ @TableName(value = "system_oauth2_refresh_token", autoResultMap = true) // 由于 Oracle 的 SEQ 的名字长度有限制,所以就先用 system_oauth2_access_token_seq 吧,反正也没啥问题 @KeySequence("system_oauth2_access_token_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @Accessors(chain = true) public class OAuth2RefreshTokenDO extends BaseDO { /** * 编号,数据库字典 */ private Long id; /** * 刷新令牌 */ private String refreshToken; /** * 用户编号 */ private Long userId; /** * 用户类型 * * 枚举 {@link UserTypeEnum} */ private Integer userType; /** * 客户端编号 * * 关联 {@link OAuth2ClientDO#getId()} */ private String clientId; /** * 授权范围 */ @TableField(typeHandler = JacksonTypeHandler.class) private List scopes; /** * 过期时间 */ private LocalDateTime expiresTime; private Long shopId; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/permission/MenuDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.permission; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.system.enums.permission.MenuTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; /** * 菜单 DO * * @author ruoyi */ @TableName("system_menu") @KeySequence("system_menu_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) public class MenuDO extends BaseDO { /** * 菜单编号 - 根节点 */ public static final Long ID_ROOT = 0L; /** * 菜单编号 */ @TableId private Long id; /** * 菜单名称 */ private String name; /** * 权限标识 * * 一般格式为:${系统}:${模块}:${操作} * 例如说:system:admin:add,即 system 服务的添加管理员。 * * 当我们把该 MenuDO 赋予给角色后,意味着该角色有该资源: * - 对于后端,配合 @PreAuthorize 注解,配置 API 接口需要该权限,从而对 API 接口进行权限控制。 * - 对于前端,配合前端标签,配置按钮是否展示,避免用户没有该权限时,结果可以看到该操作。 */ private String permission; /** * 菜单类型 * * 枚举 {@link MenuTypeEnum} */ private Integer type; /** * 显示顺序 */ private Integer sort; /** * 父菜单ID */ private Long parentId; /** * 路由地址 * * 如果 path 为 http(s) 时,则它是外链 */ private String path; /** * 菜单图标 */ private String icon; /** * 组件路径 */ private String component; /** * 组件名 */ private String componentName; /** * 状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; /** * 是否可见 * * 只有菜单、目录使用 * 当设置为 true 时,该菜单不会展示在侧边栏,但是路由还是存在。例如说,一些独立的编辑页面 /edit/1024 等等 */ private Boolean visible; /** * 是否缓存 * * 只有菜单、目录使用,否使用 Vue 路由的 keep-alive 特性 * 注意:如果开启缓存,则必须填写 {@link #componentName} 属性,否则无法缓存 */ private Boolean keepAlive; /** * 是否总是显示 * * 如果为 false 时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单 */ private Boolean alwaysShow; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/permission/RoleDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.permission; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.mybatis.core.type.JsonLongSetTypeHandler; import co.yixiang.yshop.module.system.enums.permission.DataScopeEnum; import co.yixiang.yshop.framework.tenant.core.db.TenantBaseDO; import co.yixiang.yshop.module.system.enums.permission.RoleTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; import java.util.Set; /** * 角色 DO * * @author ruoyi */ @TableName(value = "system_role", autoResultMap = true) @KeySequence("system_role_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) public class RoleDO extends TenantBaseDO { /** * 角色ID */ @TableId private Long id; /** * 角色名称 */ private String name; /** * 角色标识 * * 枚举 */ private String code; /** * 角色排序 */ private Integer sort; /** * 角色状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; /** * 角色类型 * * 枚举 {@link RoleTypeEnum} */ private Integer type; /** * 备注 */ private String remark; /** * 数据范围 * * 枚举 {@link DataScopeEnum} */ private Integer dataScope; /** * 数据范围(指定部门数组) * * 适用于 {@link #dataScope} 的值为 {@link DataScopeEnum#DEPT_CUSTOM} 时 */ @TableField(typeHandler = JsonLongSetTypeHandler.class) private Set dataScopeDeptIds; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/permission/RoleMenuDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.permission; import co.yixiang.yshop.framework.tenant.core.db.TenantBaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; /** * 角色和菜单关联 * * @author ruoyi */ @TableName("system_role_menu") @KeySequence("system_role_menu_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) public class RoleMenuDO extends TenantBaseDO { /** * 自增主键 */ @TableId private Long id; /** * 角色ID */ private Long roleId; /** * 菜单ID */ private Long menuId; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/permission/UserRoleDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.permission; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; /** * 用户和角色关联 * * @author ruoyi */ @TableName("system_user_role") @KeySequence("system_user_role_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) public class UserRoleDO extends BaseDO { /** * 自增主键 */ @TableId private Long id; /** * 用户 ID */ private Long userId; /** * 角色 ID */ private Long roleId; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/sms/SmsChannelDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.sms; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.system.framework.sms.core.enums.SmsChannelEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; /** * 短信渠道 DO * * @author zzf * @since 2021-01-25 */ @TableName(value = "system_sms_channel", autoResultMap = true) @KeySequence("system_sms_channel_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class SmsChannelDO extends BaseDO { /** * 渠道编号 */ private Long id; /** * 短信签名 */ private String signature; /** * 渠道编码 * * 枚举 {@link SmsChannelEnum} */ private String code; /** * 启用状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; /** * 备注 */ private String remark; /** * 短信 API 的账号 */ private String apiKey; /** * 短信 API 的密钥 */ private String apiSecret; /** * 短信发送回调 URL */ private String callbackUrl; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/sms/SmsCodeDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.sms; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; import java.time.LocalDateTime; /** * 手机验证码 DO * * idx_mobile 索引:基于 {@link #mobile} 字段 * * @author yshop */ @TableName("system_sms_code") @KeySequence("system_sms_code_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class SmsCodeDO extends BaseDO { /** * 编号 */ private Long id; /** * 手机号 */ private String mobile; /** * 验证码 */ private String code; /** * 发送场景 * * 枚举 {@link SmsCodeDO} */ private Integer scene; /** * 创建 IP */ private String createIp; /** * 今日发送的第几条 */ private Integer todayIndex; /** * 是否使用 */ private Boolean used; /** * 使用时间 */ private LocalDateTime usedTime; /** * 使用 IP */ private String usedIp; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/sms/SmsLogDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.sms; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.system.enums.sms.SmsReceiveStatusEnum; import co.yixiang.yshop.module.system.enums.sms.SmsSendStatusEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import lombok.*; import java.time.LocalDateTime; import java.util.Map; /** * 短信日志 DO * * @author zzf * @since 2021-01-25 */ @TableName(value = "system_sms_log", autoResultMap = true) @KeySequence("system_sms_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @AllArgsConstructor @NoArgsConstructor public class SmsLogDO extends BaseDO { /** * 自增编号 */ private Long id; // ========= 渠道相关字段 ========= /** * 短信渠道编号 * * 关联 {@link SmsChannelDO#getId()} */ private Long channelId; /** * 短信渠道编码 * * 冗余 {@link SmsChannelDO#getCode()} */ private String channelCode; // ========= 模板相关字段 ========= /** * 模板编号 * * 关联 {@link SmsTemplateDO#getId()} */ private Long templateId; /** * 模板编码 * * 冗余 {@link SmsTemplateDO#getCode()} */ private String templateCode; /** * 短信类型 * * 冗余 {@link SmsTemplateDO#getType()} */ private Integer templateType; /** * 基于 {@link SmsTemplateDO#getContent()} 格式化后的内容 */ private String templateContent; /** * 基于 {@link SmsTemplateDO#getParams()} 输入后的参数 */ @TableField(typeHandler = JacksonTypeHandler.class) private Map templateParams; /** * 短信 API 的模板编号 * * 冗余 {@link SmsTemplateDO#getApiTemplateId()} */ private String apiTemplateId; // ========= 手机相关字段 ========= /** * 手机号 */ private String mobile; /** * 用户编号 */ private Long userId; /** * 用户类型 * * 枚举 {@link UserTypeEnum} */ private Integer userType; // ========= 发送相关字段 ========= /** * 发送状态 * * 枚举 {@link SmsSendStatusEnum} */ private Integer sendStatus; /** * 发送时间 */ private LocalDateTime sendTime; /** * 短信 API 发送结果的编码 * * 由于第三方的错误码可能是字符串,所以使用 String 类型 */ private String apiSendCode; /** * 短信 API 发送失败的提示 */ private String apiSendMsg; /** * 短信 API 发送返回的唯一请求 ID * * 用于和短信 API 进行定位于排错 */ private String apiRequestId; /** * 短信 API 发送返回的序号 * * 用于和短信 API 平台的发送记录关联 */ private String apiSerialNo; // ========= 接收相关字段 ========= /** * 接收状态 * * 枚举 {@link SmsReceiveStatusEnum} */ private Integer receiveStatus; /** * 接收时间 */ private LocalDateTime receiveTime; /** * 短信 API 接收结果的编码 */ private String apiReceiveCode; /** * 短信 API 接收结果的提示 */ private String apiReceiveMsg; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/sms/SmsTemplateDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.sms; import co.yixiang.yshop.module.system.enums.sms.SmsTemplateTypeEnum; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import java.util.List; /** * 短信模板 DO * * @author zzf * @since 2021-01-25 */ @TableName(value = "system_sms_template", autoResultMap = true) @KeySequence("system_sms_template_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class SmsTemplateDO extends BaseDO { /** * 自增编号 */ private Long id; // ========= 模板相关字段 ========= /** * 短信类型 * * 枚举 {@link SmsTemplateTypeEnum} */ private Integer type; /** * 启用状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; /** * 模板编码,保证唯一 */ private String code; /** * 模板名称 */ private String name; /** * 模板内容 * * 内容的参数,使用 {} 包括,例如说 {name} */ private String content; /** * 参数数组(自动根据内容生成) */ @TableField(typeHandler = JacksonTypeHandler.class) private List params; /** * 备注 */ private String remark; /** * 短信 API 的模板编号 */ private String apiTemplateId; // ========= 渠道相关字段 ========= /** * 短信渠道编号 * * 关联 {@link SmsChannelDO#getId()} */ private Long channelId; /** * 短信渠道编码 * * 冗余 {@link SmsChannelDO#getCode()} */ private String channelCode; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/social/SocialClientDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.social; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.tenant.core.db.TenantBaseDO; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.xingyuv.jushauth.config.AuthConfig; import lombok.*; /** * 社交客户端 DO * * 对应 {@link AuthConfig} 配置,满足不同租户,有自己的客户端配置,实现社交(三方)登录 * * @author yshop */ @TableName(value = "system_social_client", autoResultMap = true) @KeySequence("system_social_client_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class SocialClientDO extends TenantBaseDO { /** * 编号,自增 */ @TableId private Long id; /** * 应用名 */ private String name; /** * 社交类型 * * 枚举 {@link SocialTypeEnum} */ private Integer socialType; /** * 用户类型 * * 目的:不同用户类型,对应不同的小程序,需要自己的配置 * * 枚举 {@link UserTypeEnum} */ private Integer userType; /** * 状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; /** * 客户端 id */ private String clientId; /** * 客户端 Secret */ private String clientSecret; /** * 代理编号 * * 目前只有部分“社交类型”在使用: * 1. 企业微信:对应授权方的网页应用 ID */ private String agentId; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/social/SocialUserBindDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.social; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; /** * 社交用户的绑定 * 即 {@link SocialUserDO} 与 UserDO 的关联表 * * @author yshop */ @TableName(value = "system_social_user_bind", autoResultMap = true) @KeySequence("system_social_user_bind_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class SocialUserBindDO extends BaseDO { /** * 编号 */ @TableId private Long id; /** * 关联的用户编号 * * 关联 UserDO 的编号 */ private Long userId; /** * 用户类型 * * 枚举 {@link UserTypeEnum} */ private Integer userType; /** * 社交平台的用户编号 * * 关联 {@link SocialUserDO#getId()} */ private Long socialUserId; /** * 社交平台的类型 * * 冗余 {@link SocialUserDO#getType()} */ private Integer socialType; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/social/SocialUserDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.social; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; /** * 社交(三方)用户 * * @author weir */ @TableName(value = "system_social_user", autoResultMap = true) @KeySequence("system_social_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class SocialUserDO extends BaseDO { /** * 自增主键 */ @TableId private Long id; /** * 社交平台的类型 * * 枚举 {@link SocialTypeEnum} */ private Integer type; /** * 社交 openid */ private String openid; /** * 社交 token */ private String token; /** * 原始 Token 数据,一般是 JSON 格式 */ private String rawTokenInfo; /** * 用户昵称 */ private String nickname; /** * 用户头像 */ private String avatar; /** * 原始用户数据,一般是 JSON 格式 */ private String rawUserInfo; /** * 最后一次的认证 code */ private String code; /** * 最后一次的认证 state */ private String state; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/tenant/TenantDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.tenant; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; import java.time.LocalDateTime; /** * 租户 DO * * @author yshop */ @TableName(value = "system_tenant", autoResultMap = true) @KeySequence("system_tenant_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @AllArgsConstructor @NoArgsConstructor public class TenantDO extends BaseDO { /** * 套餐编号 - 系统 */ public static final Long PACKAGE_ID_SYSTEM = 0L; /** * 租户编号,自增 */ private Long id; /** * 租户名,唯一 */ private String name; /** * 联系人的用户编号 * * 关联 {@link AdminUserDO#getId()} */ private Long contactUserId; /** * 联系人 */ private String contactName; /** * 联系手机 */ private String contactMobile; /** * 租户状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; /** * 绑定域名 */ @TableField(value = "domain") private String website; /** * 租户套餐编号 * * 关联 {@link TenantPackageDO#getId()} * 特殊逻辑:系统内置租户,不使用套餐,暂时使用 {@link #PACKAGE_ID_SYSTEM} 标识 */ private Long packageId; /** * 过期时间 */ private LocalDateTime expireTime; /** * 账号数量 */ private Integer accountCount; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/tenant/TenantPackageDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.tenant; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.framework.mybatis.core.type.JsonLongSetTypeHandler; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; import java.util.Set; /** * 租户套餐 DO * * @author yshop */ @TableName(value = "system_tenant_package", autoResultMap = true) @KeySequence("system_tenant_package_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @AllArgsConstructor @NoArgsConstructor public class TenantPackageDO extends BaseDO { /** * 套餐编号,自增 */ private Long id; /** * 套餐名,唯一 */ private String name; /** * 租户套餐状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; /** * 备注 */ private String remark; /** * 关联的菜单编号 */ @TableField(typeHandler = JsonLongSetTypeHandler.class) private Set menuIds; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/dataobject/user/AdminUserDO.java ================================================ package co.yixiang.yshop.module.system.dal.dataobject.user; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.mybatis.core.type.JsonLongSetTypeHandler; import co.yixiang.yshop.framework.tenant.core.db.TenantBaseDO; import co.yixiang.yshop.module.system.enums.common.SexEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import java.time.LocalDateTime; import java.util.Set; /** * 管理后台的用户 DO * * @author yshop */ @TableName(value = "system_users", autoResultMap = true) // 由于 SQL Server 的 system_user 是关键字,所以使用 system_users @KeySequence("system_users_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class AdminUserDO extends TenantBaseDO { /** * 用户ID */ @TableId private Long id; /** * 用户账号 */ private String username; /** * 加密后的密码 * * 因为目前使用 {@link BCryptPasswordEncoder} 加密器,所以无需自己处理 salt 盐 */ private String password; /** * 用户昵称 */ private String nickname; /** * 备注 */ private String remark; /** * 部门 ID */ private Long deptId; /** * 岗位编号数组 */ @TableField(typeHandler = JsonLongSetTypeHandler.class) private Set postIds; /** * 用户邮箱 */ private String email; /** * 手机号码 */ private String mobile; /** * 用户性别 * * 枚举类 {@link SexEnum} */ private Integer sex; /** * 用户头像 */ private String avatar; /** * 帐号状态 * * 枚举 {@link CommonStatusEnum} */ private Integer status; /** * 最后登录IP */ private String loginIp; /** * 最后登录时间 */ private LocalDateTime loginDate; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/dept/DeptMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.dept; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.dept.vo.dept.DeptListReqVO; import co.yixiang.yshop.module.system.dal.dataobject.dept.DeptDO; import org.apache.ibatis.annotations.Mapper; import java.util.Collection; import java.util.List; @Mapper public interface DeptMapper extends BaseMapperX { default List selectList(DeptListReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .likeIfPresent(DeptDO::getName, reqVO.getName()) .eqIfPresent(DeptDO::getStatus, reqVO.getStatus())); } default DeptDO selectByParentIdAndName(Long parentId, String name) { return selectOne(DeptDO::getParentId, parentId, DeptDO::getName, name); } default Long selectCountByParentId(Long parentId) { return selectCount(DeptDO::getParentId, parentId); } default List selectListByParentId(Collection parentIds) { return selectList(DeptDO::getParentId, parentIds); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/dept/PostMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.dept; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.dept.vo.post.PostPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.dept.PostDO; import org.apache.ibatis.annotations.Mapper; import java.util.Collection; import java.util.List; @Mapper public interface PostMapper extends BaseMapperX { default List selectList(Collection ids, Collection statuses) { return selectList(new LambdaQueryWrapperX() .inIfPresent(PostDO::getId, ids) .inIfPresent(PostDO::getStatus, statuses)); } default PageResult selectPage(PostPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(PostDO::getCode, reqVO.getCode()) .likeIfPresent(PostDO::getName, reqVO.getName()) .eqIfPresent(PostDO::getStatus, reqVO.getStatus()) .orderByDesc(PostDO::getId)); } default PostDO selectByName(String name) { return selectOne(PostDO::getName, name); } default PostDO selectByCode(String code) { return selectOne(PostDO::getCode, code); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/dept/UserPostMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.dept; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.dal.dataobject.dept.UserPostDO; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import org.apache.ibatis.annotations.Mapper; import java.util.Collection; import java.util.List; @Mapper public interface UserPostMapper extends BaseMapperX { default List selectListByUserId(Long userId) { return selectList(UserPostDO::getUserId, userId); } default void deleteByUserIdAndPostId(Long userId, Collection postIds) { delete(new LambdaQueryWrapperX() .eq(UserPostDO::getUserId, userId) .in(UserPostDO::getPostId, postIds)); } default List selectListByPostIds(Collection postIds) { return selectList(UserPostDO::getPostId, postIds); } default void deleteByUserId(Long userId) { delete(Wrappers.lambdaUpdate(UserPostDO.class).eq(UserPostDO::getUserId, userId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/dict/DictDataMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.dict; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.dict.vo.data.DictDataPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.dict.DictDataDO; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.apache.ibatis.annotations.Mapper; import java.util.Arrays; import java.util.Collection; import java.util.List; @Mapper public interface DictDataMapper extends BaseMapperX { default DictDataDO selectByDictTypeAndValue(String dictType, String value) { return selectOne(DictDataDO::getDictType, dictType, DictDataDO::getValue, value); } default DictDataDO selectByDictTypeAndLabel(String dictType, String label) { return selectOne(DictDataDO::getDictType, dictType, DictDataDO::getLabel, label); } default List selectByDictTypeAndValues(String dictType, Collection values) { return selectList(new LambdaQueryWrapper().eq(DictDataDO::getDictType, dictType) .in(DictDataDO::getValue, values)); } default long selectCountByDictType(String dictType) { return selectCount(DictDataDO::getDictType, dictType); } default PageResult selectPage(DictDataPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(DictDataDO::getLabel, reqVO.getLabel()) .eqIfPresent(DictDataDO::getDictType, reqVO.getDictType()) .eqIfPresent(DictDataDO::getStatus, reqVO.getStatus()) .orderByDesc(Arrays.asList(DictDataDO::getDictType, DictDataDO::getSort))); } default List selectListByStatusAndDictType(Integer status, String dictType) { return selectList(new LambdaQueryWrapperX() .eqIfPresent(DictDataDO::getStatus, status) .eqIfPresent(DictDataDO::getDictType, dictType)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/dict/DictTypeMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.dict; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.dict.vo.type.DictTypePageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.dict.DictTypeDO; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; import java.time.LocalDateTime; @Mapper public interface DictTypeMapper extends BaseMapperX { default PageResult selectPage(DictTypePageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(DictTypeDO::getName, reqVO.getName()) .likeIfPresent(DictTypeDO::getType, reqVO.getType()) .eqIfPresent(DictTypeDO::getStatus, reqVO.getStatus()) .betweenIfPresent(DictTypeDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(DictTypeDO::getId)); } default DictTypeDO selectByType(String type) { return selectOne(DictTypeDO::getType, type); } default DictTypeDO selectByName(String name) { return selectOne(DictTypeDO::getName, name); } @Update("UPDATE system_dict_type SET deleted = 1, deleted_time = #{deletedTime} WHERE id = #{id}") void updateToDelete(@Param("id") Long id, @Param("deletedTime") LocalDateTime deletedTime); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/logger/LoginLogMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.logger; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.logger.vo.loginlog.LoginLogPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.logger.LoginLogDO; import co.yixiang.yshop.module.system.enums.logger.LoginResultEnum; import org.apache.ibatis.annotations.Mapper; @Mapper public interface LoginLogMapper extends BaseMapperX { default PageResult selectPage(LoginLogPageReqVO reqVO) { LambdaQueryWrapperX query = new LambdaQueryWrapperX() .likeIfPresent(LoginLogDO::getUserIp, reqVO.getUserIp()) .likeIfPresent(LoginLogDO::getUsername, reqVO.getUsername()) .betweenIfPresent(LoginLogDO::getCreateTime, reqVO.getCreateTime()); if (Boolean.TRUE.equals(reqVO.getStatus())) { query.eq(LoginLogDO::getResult, LoginResultEnum.SUCCESS.getResult()); } else if (Boolean.FALSE.equals(reqVO.getStatus())) { query.gt(LoginLogDO::getResult, LoginResultEnum.SUCCESS.getResult()); } query.orderByDesc(LoginLogDO::getId); // 降序 return selectPage(reqVO, query); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/logger/OperateLogMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.logger; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.api.logger.dto.OperateLogPageReqDTO; import co.yixiang.yshop.module.system.controller.admin.logger.vo.operatelog.OperateLogPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.logger.OperateLogDO; import org.apache.ibatis.annotations.Mapper; @Mapper public interface OperateLogMapper extends BaseMapperX { default PageResult selectPage(OperateLogPageReqVO pageReqDTO) { return selectPage(pageReqDTO, new LambdaQueryWrapperX() .eqIfPresent(OperateLogDO::getUserId, pageReqDTO.getUserId()) .eqIfPresent(OperateLogDO::getBizId, pageReqDTO.getBizId()) .likeIfPresent(OperateLogDO::getType, pageReqDTO.getType()) .likeIfPresent(OperateLogDO::getSubType, pageReqDTO.getSubType()) .likeIfPresent(OperateLogDO::getAction, pageReqDTO.getAction()) .betweenIfPresent(OperateLogDO::getCreateTime, pageReqDTO.getCreateTime()) .orderByDesc(OperateLogDO::getId)); } default PageResult selectPage(OperateLogPageReqDTO pageReqDTO) { return selectPage(pageReqDTO, new LambdaQueryWrapperX() .eqIfPresent(OperateLogDO::getType, pageReqDTO.getType()) .eqIfPresent(OperateLogDO::getBizId, pageReqDTO.getBizId()) .eqIfPresent(OperateLogDO::getUserId, pageReqDTO.getUserId()) .orderByDesc(OperateLogDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/mail/MailAccountMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.mail; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.query.QueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.mail.vo.account.MailAccountPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailAccountDO; import org.apache.ibatis.annotations.Mapper; @Mapper public interface MailAccountMapper extends BaseMapperX { default PageResult selectPage(MailAccountPageReqVO pageReqVO) { return selectPage(pageReqVO, new LambdaQueryWrapperX() .likeIfPresent(MailAccountDO::getMail, pageReqVO.getMail()) .likeIfPresent(MailAccountDO::getUsername , pageReqVO.getUsername())); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/mail/MailLogMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.mail; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.mail.vo.log.MailLogPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailLogDO; import org.apache.ibatis.annotations.Mapper; @Mapper public interface MailLogMapper extends BaseMapperX { default PageResult selectPage(MailLogPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(MailLogDO::getUserId, reqVO.getUserId()) .eqIfPresent(MailLogDO::getUserType, reqVO.getUserType()) .likeIfPresent(MailLogDO::getToMail, reqVO.getToMail()) .eqIfPresent(MailLogDO::getAccountId, reqVO.getAccountId()) .eqIfPresent(MailLogDO::getTemplateId, reqVO.getTemplateId()) .eqIfPresent(MailLogDO::getSendStatus, reqVO.getSendStatus()) .betweenIfPresent(MailLogDO::getSendTime, reqVO.getSendTime()) .orderByDesc(MailLogDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/mail/MailTemplateMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.mail; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.query.QueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.mail.vo.template.MailTemplatePageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailTemplateDO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsTemplateDO; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import java.util.Date; @Mapper public interface MailTemplateMapper extends BaseMapperX { default PageResult selectPage(MailTemplatePageReqVO pageReqVO){ return selectPage(pageReqVO , new LambdaQueryWrapperX() .eqIfPresent(MailTemplateDO::getStatus, pageReqVO.getStatus()) .likeIfPresent(MailTemplateDO::getCode, pageReqVO.getCode()) .likeIfPresent(MailTemplateDO::getName, pageReqVO.getName()) .eqIfPresent(MailTemplateDO::getAccountId, pageReqVO.getAccountId()) .betweenIfPresent(MailTemplateDO::getCreateTime, pageReqVO.getCreateTime())); } default Long selectCountByAccountId(Long accountId) { return selectCount(MailTemplateDO::getAccountId, accountId); } default MailTemplateDO selectByCode(String code) { return selectOne(MailTemplateDO::getCode, code); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/notice/NoticeMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.notice; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.notice.vo.NoticePageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.notice.NoticeDO; import org.apache.ibatis.annotations.Mapper; @Mapper public interface NoticeMapper extends BaseMapperX { default PageResult selectPage(NoticePageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(NoticeDO::getTitle, reqVO.getTitle()) .eqIfPresent(NoticeDO::getStatus, reqVO.getStatus()) .orderByDesc(NoticeDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/notify/NotifyMessageMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.notify; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.mybatis.core.query.QueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.notify.vo.message.NotifyMessageMyPageReqVO; import co.yixiang.yshop.module.system.controller.admin.notify.vo.message.NotifyMessagePageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.notify.NotifyMessageDO; import org.apache.ibatis.annotations.Mapper; import java.time.LocalDateTime; import java.util.Collection; import java.util.List; @Mapper public interface NotifyMessageMapper extends BaseMapperX { default PageResult selectPage(NotifyMessagePageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(NotifyMessageDO::getUserId, reqVO.getUserId()) .eqIfPresent(NotifyMessageDO::getUserType, reqVO.getUserType()) .likeIfPresent(NotifyMessageDO::getTemplateCode, reqVO.getTemplateCode()) .eqIfPresent(NotifyMessageDO::getTemplateType, reqVO.getTemplateType()) .betweenIfPresent(NotifyMessageDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(NotifyMessageDO::getId)); } default PageResult selectPage(NotifyMessageMyPageReqVO reqVO, Long userId, Integer userType) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(NotifyMessageDO::getReadStatus, reqVO.getReadStatus()) .betweenIfPresent(NotifyMessageDO::getCreateTime, reqVO.getCreateTime()) .eq(NotifyMessageDO::getUserId, userId) .eq(NotifyMessageDO::getUserType, userType) .orderByDesc(NotifyMessageDO::getId)); } default int updateListRead(Collection ids, Long userId, Integer userType) { return update(new NotifyMessageDO().setReadStatus(true).setReadTime(LocalDateTime.now()), new LambdaQueryWrapperX() .in(NotifyMessageDO::getId, ids) .eq(NotifyMessageDO::getUserId, userId) .eq(NotifyMessageDO::getUserType, userType) .eq(NotifyMessageDO::getReadStatus, false)); } default int updateListRead(Long userId, Integer userType) { return update(new NotifyMessageDO().setReadStatus(true).setReadTime(LocalDateTime.now()), new LambdaQueryWrapperX() .eq(NotifyMessageDO::getUserId, userId) .eq(NotifyMessageDO::getUserType, userType) .eq(NotifyMessageDO::getReadStatus, false)); } default List selectUnreadListByUserIdAndUserType(Long userId, Integer userType, Integer size) { return selectList(new QueryWrapperX() // 由于要使用 limitN 语句,所以只能用 QueryWrapperX .eq("user_id", userId) .eq("user_type", userType) .eq("read_status", false) .orderByDesc("id").limitN(size)); } default Long selectUnreadCountByUserIdAndUserType(Long userId, Integer userType) { return selectCount(new LambdaQueryWrapperX() .eq(NotifyMessageDO::getReadStatus, false) .eq(NotifyMessageDO::getUserId, userId) .eq(NotifyMessageDO::getUserType, userType)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/notify/NotifyTemplateMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.notify; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.notify.vo.template.NotifyTemplatePageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.notify.NotifyTemplateDO; import org.apache.ibatis.annotations.Mapper; @Mapper public interface NotifyTemplateMapper extends BaseMapperX { default NotifyTemplateDO selectByCode(String code) { return selectOne(NotifyTemplateDO::getCode, code); } default PageResult selectPage(NotifyTemplatePageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(NotifyTemplateDO::getCode, reqVO.getCode()) .likeIfPresent(NotifyTemplateDO::getName, reqVO.getName()) .eqIfPresent(NotifyTemplateDO::getStatus, reqVO.getStatus()) .betweenIfPresent(NotifyTemplateDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(NotifyTemplateDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/oauth2/OAuth2AccessTokenMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.oauth2; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.framework.tenant.core.aop.TenantIgnore; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import org.apache.ibatis.annotations.Mapper; import java.time.LocalDateTime; import java.util.List; @Mapper public interface OAuth2AccessTokenMapper extends BaseMapperX { @TenantIgnore // 获取 token 的时候,需要忽略租户编号。原因是:一些场景下,可能不会传递 tenant-id 请求头,例如说文件上传、积木报表等等 default OAuth2AccessTokenDO selectByAccessToken(String accessToken) { return selectOne(OAuth2AccessTokenDO::getAccessToken, accessToken); } default List selectListByRefreshToken(String refreshToken) { return selectList(OAuth2AccessTokenDO::getRefreshToken, refreshToken); } default PageResult selectPage(OAuth2AccessTokenPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(OAuth2AccessTokenDO::getUserId, reqVO.getUserId()) .eqIfPresent(OAuth2AccessTokenDO::getUserType, reqVO.getUserType()) .likeIfPresent(OAuth2AccessTokenDO::getClientId, reqVO.getClientId()) .gt(OAuth2AccessTokenDO::getExpiresTime, LocalDateTime.now()) .orderByDesc(OAuth2AccessTokenDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/oauth2/OAuth2ApproveMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.oauth2; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface OAuth2ApproveMapper extends BaseMapperX { default int update(OAuth2ApproveDO updateObj) { return update(updateObj, new LambdaQueryWrapperX() .eq(OAuth2ApproveDO::getUserId, updateObj.getUserId()) .eq(OAuth2ApproveDO::getUserType, updateObj.getUserType()) .eq(OAuth2ApproveDO::getClientId, updateObj.getClientId()) .eq(OAuth2ApproveDO::getScope, updateObj.getScope())); } default List selectListByUserIdAndUserTypeAndClientId(Long userId, Integer userType, String clientId) { return selectList(new LambdaQueryWrapperX() .eq(OAuth2ApproveDO::getUserId, userId) .eq(OAuth2ApproveDO::getUserType, userType) .eq(OAuth2ApproveDO::getClientId, clientId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/oauth2/OAuth2ClientMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.oauth2; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2ClientDO; import org.apache.ibatis.annotations.Mapper; /** * OAuth2 客户端 Mapper * * @author yshop */ @Mapper public interface OAuth2ClientMapper extends BaseMapperX { default PageResult selectPage(OAuth2ClientPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(OAuth2ClientDO::getName, reqVO.getName()) .eqIfPresent(OAuth2ClientDO::getStatus, reqVO.getStatus()) .orderByDesc(OAuth2ClientDO::getId)); } default OAuth2ClientDO selectByClientId(String clientId) { return selectOne(OAuth2ClientDO::getClientId, clientId); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/oauth2/OAuth2CodeMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.oauth2; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2CodeDO; import org.apache.ibatis.annotations.Mapper; @Mapper public interface OAuth2CodeMapper extends BaseMapperX { default OAuth2CodeDO selectByCode(String code) { return selectOne(OAuth2CodeDO::getCode, code); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/oauth2/OAuth2RefreshTokenMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.oauth2; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2RefreshTokenDO; import org.apache.ibatis.annotations.Mapper; @Mapper public interface OAuth2RefreshTokenMapper extends BaseMapperX { default int deleteByRefreshToken(String refreshToken) { return delete(new LambdaQueryWrapperX() .eq(OAuth2RefreshTokenDO::getRefreshToken, refreshToken)); } default OAuth2RefreshTokenDO selectByRefreshToken(String refreshToken) { return selectOne(OAuth2RefreshTokenDO::getRefreshToken, refreshToken); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/permission/MenuMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.permission; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.permission.vo.menu.MenuListReqVO; import co.yixiang.yshop.module.system.dal.dataobject.permission.MenuDO; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface MenuMapper extends BaseMapperX { default MenuDO selectByParentIdAndName(Long parentId, String name) { return selectOne(MenuDO::getParentId, parentId, MenuDO::getName, name); } default Long selectCountByParentId(Long parentId) { return selectCount(MenuDO::getParentId, parentId); } default List selectList(MenuListReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .likeIfPresent(MenuDO::getName, reqVO.getName()) .eqIfPresent(MenuDO::getStatus, reqVO.getStatus())); } default List selectListByPermission(String permission) { return selectList(MenuDO::getPermission, permission); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/permission/RoleMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.permission; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.permission.vo.role.RolePageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.permission.RoleDO; import org.apache.ibatis.annotations.Mapper; import org.springframework.lang.Nullable; import java.util.Collection; import java.util.List; @Mapper public interface RoleMapper extends BaseMapperX { default PageResult selectPage(RolePageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(RoleDO::getName, reqVO.getName()) .likeIfPresent(RoleDO::getCode, reqVO.getCode()) .eqIfPresent(RoleDO::getStatus, reqVO.getStatus()) .betweenIfPresent(BaseDO::getCreateTime, reqVO.getCreateTime()) .orderByAsc(RoleDO::getSort)); } default RoleDO selectByName(String name) { return selectOne(RoleDO::getName, name); } default RoleDO selectByCode(String code) { return selectOne(RoleDO::getCode, code); } default List selectListByStatus(@Nullable Collection statuses) { return selectList(RoleDO::getStatus, statuses); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/permission/RoleMenuMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.permission; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.system.dal.dataobject.permission.RoleMenuDO; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.apache.ibatis.annotations.Mapper; import java.util.Collection; import java.util.List; @Mapper public interface RoleMenuMapper extends BaseMapperX { default List selectListByRoleId(Long roleId) { return selectList(RoleMenuDO::getRoleId, roleId); } default List selectListByRoleId(Collection roleIds) { return selectList(RoleMenuDO::getRoleId, roleIds); } default List selectListByMenuId(Long menuId) { return selectList(RoleMenuDO::getMenuId, menuId); } default void deleteListByRoleIdAndMenuIds(Long roleId, Collection menuIds) { delete(new LambdaQueryWrapper() .eq(RoleMenuDO::getRoleId, roleId) .in(RoleMenuDO::getMenuId, menuIds)); } default void deleteListByMenuId(Long menuId) { delete(new LambdaQueryWrapper().eq(RoleMenuDO::getMenuId, menuId)); } default void deleteListByRoleId(Long roleId) { delete(new LambdaQueryWrapper().eq(RoleMenuDO::getRoleId, roleId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/permission/UserRoleMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.permission; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.module.system.dal.dataobject.permission.UserRoleDO; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.apache.ibatis.annotations.Mapper; import java.util.Collection; import java.util.List; @Mapper public interface UserRoleMapper extends BaseMapperX { default List selectListByUserId(Long userId) { return selectList(UserRoleDO::getUserId, userId); } default void deleteListByUserIdAndRoleIdIds(Long userId, Collection roleIds) { delete(new LambdaQueryWrapper() .eq(UserRoleDO::getUserId, userId) .in(UserRoleDO::getRoleId, roleIds)); } default void deleteListByUserId(Long userId) { delete(new LambdaQueryWrapper().eq(UserRoleDO::getUserId, userId)); } default void deleteListByRoleId(Long roleId) { delete(new LambdaQueryWrapper().eq(UserRoleDO::getRoleId, roleId)); } default List selectListByRoleIds(Collection roleIds) { return selectList(UserRoleDO::getRoleId, roleIds); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/sms/SmsChannelMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.sms; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsChannelDO; import org.apache.ibatis.annotations.Mapper; @Mapper public interface SmsChannelMapper extends BaseMapperX { default PageResult selectPage(SmsChannelPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(SmsChannelDO::getSignature, reqVO.getSignature()) .eqIfPresent(SmsChannelDO::getStatus, reqVO.getStatus()) .betweenIfPresent(SmsChannelDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(SmsChannelDO::getId)); } default SmsChannelDO selectByCode(String code) { return selectOne(SmsChannelDO::getCode, code); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/sms/SmsCodeMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.sms; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.QueryWrapperX; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsCodeDO; import org.apache.ibatis.annotations.Mapper; @Mapper public interface SmsCodeMapper extends BaseMapperX { /** * 获得手机号的最后一个手机验证码 * * @param mobile 手机号 * @param scene 发送场景,选填 * @param code 验证码 选填 * @return 手机验证码 */ default SmsCodeDO selectLastByMobile(String mobile, String code, Integer scene) { return selectOne(new QueryWrapperX() .eq("mobile", mobile) .eqIfPresent("scene", scene) .eqIfPresent("code", code) .orderByDesc("id") .limitN(1)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/sms/SmsLogMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.sms; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsLogDO; import org.apache.ibatis.annotations.Mapper; @Mapper public interface SmsLogMapper extends BaseMapperX { default PageResult selectPage(SmsLogPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(SmsLogDO::getChannelId, reqVO.getChannelId()) .eqIfPresent(SmsLogDO::getTemplateId, reqVO.getTemplateId()) .likeIfPresent(SmsLogDO::getMobile, reqVO.getMobile()) .eqIfPresent(SmsLogDO::getSendStatus, reqVO.getSendStatus()) .betweenIfPresent(SmsLogDO::getSendTime, reqVO.getSendTime()) .eqIfPresent(SmsLogDO::getReceiveStatus, reqVO.getReceiveStatus()) .betweenIfPresent(SmsLogDO::getReceiveTime, reqVO.getReceiveTime()) .orderByDesc(SmsLogDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/sms/SmsTemplateMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.sms; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.sms.vo.template.SmsTemplatePageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsTemplateDO; import org.apache.ibatis.annotations.Mapper; @Mapper public interface SmsTemplateMapper extends BaseMapperX { default SmsTemplateDO selectByCode(String code) { return selectOne(SmsTemplateDO::getCode, code); } default PageResult selectPage(SmsTemplatePageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(SmsTemplateDO::getType, reqVO.getType()) .eqIfPresent(SmsTemplateDO::getStatus, reqVO.getStatus()) .likeIfPresent(SmsTemplateDO::getCode, reqVO.getCode()) .likeIfPresent(SmsTemplateDO::getContent, reqVO.getContent()) .likeIfPresent(SmsTemplateDO::getApiTemplateId, reqVO.getApiTemplateId()) .eqIfPresent(SmsTemplateDO::getChannelId, reqVO.getChannelId()) .betweenIfPresent(SmsTemplateDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(SmsTemplateDO::getId)); } default Long selectCountByChannelId(Long channelId) { return selectCount(SmsTemplateDO::getChannelId, channelId); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/social/SocialClientMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.social; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.social.SocialClientDO; import org.apache.ibatis.annotations.Mapper; @Mapper public interface SocialClientMapper extends BaseMapperX { default SocialClientDO selectBySocialTypeAndUserType(Integer socialType, Integer userType) { return selectOne(SocialClientDO::getSocialType, socialType, SocialClientDO::getUserType, userType); } default PageResult selectPage(SocialClientPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(SocialClientDO::getName, reqVO.getName()) .eqIfPresent(SocialClientDO::getSocialType, reqVO.getSocialType()) .eqIfPresent(SocialClientDO::getUserType, reqVO.getUserType()) .likeIfPresent(SocialClientDO::getClientId, reqVO.getClientId()) .eqIfPresent(SocialClientDO::getStatus, reqVO.getStatus()) .orderByDesc(SocialClientDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/social/SocialUserBindMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.social; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.dal.dataobject.social.SocialUserBindDO; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface SocialUserBindMapper extends BaseMapperX { default void deleteByUserTypeAndUserIdAndSocialType(Integer userType, Long userId, Integer socialType) { delete(new LambdaQueryWrapperX() .eq(SocialUserBindDO::getUserType, userType) .eq(SocialUserBindDO::getUserId, userId) .eq(SocialUserBindDO::getSocialType, socialType)); } default void deleteByUserTypeAndSocialUserId(Integer userType, Long socialUserId) { delete(new LambdaQueryWrapperX() .eq(SocialUserBindDO::getUserType, userType) .eq(SocialUserBindDO::getSocialUserId, socialUserId)); } default SocialUserBindDO selectByUserTypeAndSocialUserId(Integer userType, Long socialUserId) { return selectOne(SocialUserBindDO::getUserType, userType, SocialUserBindDO::getSocialUserId, socialUserId); } default List selectListByUserIdAndUserType(Long userId, Integer userType) { return selectList(new LambdaQueryWrapperX() .eq(SocialUserBindDO::getUserId, userId) .eq(SocialUserBindDO::getUserType, userType)); } default SocialUserBindDO selectByUserIdAndUserTypeAndSocialType(Long userId, Integer userType, Integer socialType) { return selectOne(new LambdaQueryWrapperX() .eq(SocialUserBindDO::getUserId, userId) .eq(SocialUserBindDO::getUserType, userType) .eq(SocialUserBindDO::getSocialType, socialType)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/social/SocialUserMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.social; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.socail.vo.user.SocialUserPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.social.SocialUserDO; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface SocialUserMapper extends BaseMapperX { default SocialUserDO selectByTypeAndCodeAnState(Integer type, String code, String state) { return selectOne(new LambdaQueryWrapper() .eq(SocialUserDO::getType, type) .eq(SocialUserDO::getCode, code) .eq(SocialUserDO::getState, state)); } default SocialUserDO selectByTypeAndOpenid(Integer type, String openid) { return selectOne(new LambdaQueryWrapper() .eq(SocialUserDO::getType, type) .eq(SocialUserDO::getOpenid, openid)); } default PageResult selectPage(SocialUserPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(SocialUserDO::getType, reqVO.getType()) .likeIfPresent(SocialUserDO::getNickname, reqVO.getNickname()) .likeIfPresent(SocialUserDO::getOpenid, reqVO.getOpenid()) .betweenIfPresent(SocialUserDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(SocialUserDO::getId)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/tenant/TenantMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.tenant; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.tenant.TenantDO; import org.apache.ibatis.annotations.Mapper; import java.util.List; /** * 租户 Mapper * * @author yshop */ @Mapper public interface TenantMapper extends BaseMapperX { default PageResult selectPage(TenantPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(TenantDO::getName, reqVO.getName()) .likeIfPresent(TenantDO::getContactName, reqVO.getContactName()) .likeIfPresent(TenantDO::getContactMobile, reqVO.getContactMobile()) .eqIfPresent(TenantDO::getStatus, reqVO.getStatus()) .betweenIfPresent(TenantDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(TenantDO::getId)); } default TenantDO selectByName(String name) { return selectOne(TenantDO::getName, name); } default TenantDO selectByWebsite(String website) { return selectOne(TenantDO::getWebsite, website); } default Long selectCountByPackageId(Long packageId) { return selectCount(TenantDO::getPackageId, packageId); } default List selectListByPackageId(Long packageId) { return selectList(TenantDO::getPackageId, packageId); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/tenant/TenantPackageMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.tenant; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.tenant.vo.packages.TenantPackagePageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.tenant.TenantPackageDO; import org.apache.ibatis.annotations.Mapper; import java.util.List; /** * 租户套餐 Mapper * * @author yshop */ @Mapper public interface TenantPackageMapper extends BaseMapperX { default PageResult selectPage(TenantPackagePageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(TenantPackageDO::getName, reqVO.getName()) .eqIfPresent(TenantPackageDO::getStatus, reqVO.getStatus()) .likeIfPresent(TenantPackageDO::getRemark, reqVO.getRemark()) .betweenIfPresent(TenantPackageDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(TenantPackageDO::getId)); } default List selectListByStatus(Integer status) { return selectList(TenantPackageDO::getStatus, status); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/mysql/user/AdminUserMapper.java ================================================ package co.yixiang.yshop.module.system.dal.mysql.user; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX; import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX; import co.yixiang.yshop.module.system.controller.admin.user.vo.user.UserPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import org.apache.ibatis.annotations.Mapper; import java.util.Collection; import java.util.List; @Mapper public interface AdminUserMapper extends BaseMapperX { default AdminUserDO selectByUsername(String username) { return selectOne(AdminUserDO::getUsername, username); } default AdminUserDO selectByEmail(String email) { return selectOne(AdminUserDO::getEmail, email); } default AdminUserDO selectByMobile(String mobile) { return selectOne(AdminUserDO::getMobile, mobile); } default PageResult selectPage(UserPageReqVO reqVO, Collection deptIds) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername()) .likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile()) .eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus()) .betweenIfPresent(AdminUserDO::getCreateTime, reqVO.getCreateTime()) .inIfPresent(AdminUserDO::getDeptId, deptIds) .orderByDesc(AdminUserDO::getId)); } default List selectListByNickname(String nickname) { return selectList(new LambdaQueryWrapperX().like(AdminUserDO::getNickname, nickname)); } default List selectListByStatus(Integer status) { return selectList(AdminUserDO::getStatus, status); } default List selectListByDeptIds(Collection deptIds) { return selectList(AdminUserDO::getDeptId, deptIds); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/redis/RedisKeyConstants.java ================================================ package co.yixiang.yshop.module.system.dal.redis; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; /** * System Redis Key 枚举类 * * @author yshop */ public interface RedisKeyConstants { /** * 指定部门的所有子部门编号数组的缓存 *

* KEY 格式:dept_children_ids:{id} * VALUE 数据类型:String 子部门编号集合 */ String DEPT_CHILDREN_ID_LIST = "dept_children_ids"; /** * 角色的缓存 *

* KEY 格式:role:{id} * VALUE 数据类型:String 角色信息 */ String ROLE = "role"; /** * 用户拥有的角色编号的缓存 *

* KEY 格式:user_role_ids:{userId} * VALUE 数据类型:String 角色编号集合 */ String USER_ROLE_ID_LIST = "user_role_ids"; /** * 拥有指定菜单的角色编号的缓存 *

* KEY 格式:user_role_ids:{menuId} * VALUE 数据类型:String 角色编号集合 */ String MENU_ROLE_ID_LIST = "menu_role_ids"; /** * 拥有权限对应的菜单编号数组的缓存 *

* KEY 格式:permission_menu_ids:{permission} * VALUE 数据类型:String 菜单编号数组 */ String PERMISSION_MENU_ID_LIST = "permission_menu_ids"; /** * OAuth2 客户端的缓存 *

* KEY 格式:oauth_client:{id} * VALUE 数据类型:String 客户端信息 */ String OAUTH_CLIENT = "oauth_client"; /** * 访问令牌的缓存 *

* KEY 格式:oauth2_access_token:{token} * VALUE 数据类型:String 访问令牌信息 {@link OAuth2AccessTokenDO} *

* 由于动态过期时间,使用 RedisTemplate 操作 */ String OAUTH2_ACCESS_TOKEN = "oauth2_access_token:%s"; /** * 站内信模版的缓存 *

* KEY 格式:notify_template:{code} * VALUE 数据格式:String 模版信息 */ String NOTIFY_TEMPLATE = "notify_template"; /** * 邮件账号的缓存 *

* KEY 格式:mail_account:{id} * VALUE 数据格式:String 账号信息 */ String MAIL_ACCOUNT = "mail_account"; /** * 邮件模版的缓存 *

* KEY 格式:mail_template:{code} * VALUE 数据格式:String 模版信息 */ String MAIL_TEMPLATE = "mail_template"; /** * 短信模版的缓存 *

* KEY 格式:sms_template:{id} * VALUE 数据格式:String 模版信息 */ String SMS_TEMPLATE = "sms_template"; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/dal/redis/oauth2/OAuth2AccessTokenRedisDAO.java ================================================ package co.yixiang.yshop.module.system.dal.redis.oauth2; import cn.hutool.core.date.LocalDateTimeUtil; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Repository; import jakarta.annotation.Resource; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.Collection; import java.util.List; import java.util.concurrent.TimeUnit; import static co.yixiang.yshop.module.system.dal.redis.RedisKeyConstants.OAUTH2_ACCESS_TOKEN; /** * {@link OAuth2AccessTokenDO} 的 RedisDAO * * @author yshop */ @Repository public class OAuth2AccessTokenRedisDAO { @Resource private StringRedisTemplate stringRedisTemplate; public OAuth2AccessTokenDO get(String accessToken) { String redisKey = formatKey(accessToken); return JsonUtils.parseObject(stringRedisTemplate.opsForValue().get(redisKey), OAuth2AccessTokenDO.class); } public void set(OAuth2AccessTokenDO accessTokenDO) { String redisKey = formatKey(accessTokenDO.getAccessToken()); // 清理多余字段,避免缓存 accessTokenDO.setUpdater(null).setUpdateTime(null).setCreateTime(null).setCreator(null).setDeleted(null); long time = LocalDateTimeUtil.between(LocalDateTime.now(), accessTokenDO.getExpiresTime(), ChronoUnit.SECONDS); if (time > 0) { stringRedisTemplate.opsForValue().set(redisKey, JsonUtils.toJsonString(accessTokenDO), time, TimeUnit.SECONDS); } } public void delete(String accessToken) { String redisKey = formatKey(accessToken); stringRedisTemplate.delete(redisKey); } public void deleteList(Collection accessTokens) { List redisKeys = CollectionUtils.convertList(accessTokens, OAuth2AccessTokenRedisDAO::formatKey); stringRedisTemplate.delete(redisKeys); } private static String formatKey(String accessToken) { return String.format(OAUTH2_ACCESS_TOKEN, accessToken); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/captcha/config/YshopCaptchaConfiguration.java ================================================ package co.yixiang.yshop.module.system.framework.captcha.config; import co.yixiang.yshop.module.system.framework.captcha.core.RedisCaptchaServiceImpl; import com.xingyuv.captcha.properties.AjCaptchaProperties; import com.xingyuv.captcha.service.CaptchaCacheService; import com.xingyuv.captcha.service.impl.CaptchaServiceFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.StringRedisTemplate; /** * 验证码的配置类 * * @author yshop */ @Configuration(proxyBeanMethods = false) public class YshopCaptchaConfiguration { @Bean public CaptchaCacheService captchaCacheService(AjCaptchaProperties config, StringRedisTemplate stringRedisTemplate) { CaptchaCacheService captchaCacheService = CaptchaServiceFactory.getCache(config.getCacheType().name()); if (captchaCacheService instanceof RedisCaptchaServiceImpl) { ((RedisCaptchaServiceImpl) captchaCacheService).setStringRedisTemplate(stringRedisTemplate); } return captchaCacheService; } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/captcha/core/RedisCaptchaServiceImpl.java ================================================ package co.yixiang.yshop.module.system.framework.captcha.core; import com.xingyuv.captcha.service.CaptchaCacheService; import lombok.Setter; import org.springframework.data.redis.core.StringRedisTemplate; import java.util.concurrent.TimeUnit; /** * 基于 Redis 实现验证码的存储 * * @author 星语 */ @Setter public class RedisCaptchaServiceImpl implements CaptchaCacheService { private StringRedisTemplate stringRedisTemplate; @Override public String type() { return "redis"; } @Override public void set(String key, String value, long expiresInSeconds) { stringRedisTemplate.opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS); } @Override public boolean exists(String key) { return Boolean.TRUE.equals(stringRedisTemplate.hasKey(key)); } @Override public void delete(String key) { stringRedisTemplate.delete(key); } @Override public String get(String key) { return stringRedisTemplate.opsForValue().get(key); } @Override public Long increment(String key, long val) { return stringRedisTemplate.opsForValue().increment(key,val); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/datapermission/config/DataPermissionConfiguration.java ================================================ package co.yixiang.yshop.module.system.framework.datapermission.config; import co.yixiang.yshop.module.system.dal.dataobject.dept.DeptDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * system 模块的数据权限 Configuration * * @author yshop */ @Configuration(proxyBeanMethods = false) public class DataPermissionConfiguration { @Bean public DeptDataPermissionRuleCustomizer sysDeptDataPermissionRuleCustomizer() { return rule -> { // dept rule.addDeptColumn(AdminUserDO.class); rule.addDeptColumn(DeptDO.class, "id"); // user rule.addUserColumn(AdminUserDO.class, "id"); }; } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/operatelog/core/AdminUserParseFunction.java ================================================ package co.yixiang.yshop.module.system.framework.operatelog.core; import cn.hutool.core.convert.Convert; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.module.system.service.user.AdminUserService; import com.mzt.logapi.service.IParseFunction; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; /** * 管理员名字的 {@link IParseFunction} 实现类 * * @author HUIHUI */ @Slf4j @Component public class AdminUserParseFunction implements IParseFunction { public static final String NAME = "getAdminUserById"; @Resource private AdminUserService adminUserService; @Override public String functionName() { return NAME; } @Override public String apply(Object value) { if (StrUtil.isEmptyIfStr(value)) { return ""; } // 获取用户信息 AdminUserDO user = adminUserService.getUser(Convert.toLong(value)); if (user == null) { log.warn("[apply][获取用户{{}}为空", value); return ""; } // 返回格式 yshop(13888888888) String nickname = user.getNickname(); if (StrUtil.isEmpty(user.getMobile())) { return nickname; } return StrUtil.format("{}({})", nickname, user.getMobile()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/operatelog/core/AreaParseFunction.java ================================================ package co.yixiang.yshop.module.system.framework.operatelog.core; import cn.hutool.core.convert.Convert; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.ip.core.utils.AreaUtils; import com.mzt.logapi.service.IParseFunction; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; /** * 地名的 {@link IParseFunction} 实现类 * * @author HUIHUI */ @Slf4j @Component public class AreaParseFunction implements IParseFunction { public static final String NAME = "getArea"; @Override public boolean executeBefore() { return true; // 先转换值后对比 } @Override public String functionName() { return NAME; } @Override public String apply(Object value) { if (StrUtil.isEmptyIfStr(value)) { return ""; } return AreaUtils.format(Convert.toInt(value)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/operatelog/core/BooleanParseFunction.java ================================================ package co.yixiang.yshop.module.system.framework.operatelog.core; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.dict.core.DictFrameworkUtils; import co.yixiang.yshop.module.infra.enums.DictTypeConstants; import com.mzt.logapi.service.IParseFunction; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; /** * 是否类型的 {@link IParseFunction} 实现类 * * @author HUIHUI */ @Component @Slf4j public class BooleanParseFunction implements IParseFunction { public static final String NAME = "getBoolean"; @Override public boolean executeBefore() { return true; // 先转换值后对比 } @Override public String functionName() { return NAME; } @Override public String apply(Object value) { if (StrUtil.isEmptyIfStr(value)) { return ""; } return DictFrameworkUtils.getDictDataLabel(DictTypeConstants.BOOLEAN_STRING, value.toString()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/operatelog/core/DeptParseFunction.java ================================================ package co.yixiang.yshop.module.system.framework.operatelog.core; import cn.hutool.core.convert.Convert; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.module.system.dal.dataobject.dept.DeptDO; import co.yixiang.yshop.module.system.service.dept.DeptService; import com.mzt.logapi.service.IParseFunction; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; /** * 部门名字的 {@link IParseFunction} 实现类 * * @author HUIHUI */ @Slf4j @Component public class DeptParseFunction implements IParseFunction { public static final String NAME = "getDeptById"; @Resource private DeptService deptService; @Override public String functionName() { return NAME; } @Override public String apply(Object value) { if (StrUtil.isEmptyIfStr(value)) { return ""; } // 获取部门信息 DeptDO dept = deptService.getDept(Convert.toLong(value)); if (dept == null) { log.warn("[apply][获取部门{{}}为空", value); return ""; } return dept.getName(); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/operatelog/core/PostParseFunction.java ================================================ package co.yixiang.yshop.module.system.framework.operatelog.core; import cn.hutool.core.convert.Convert; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.module.system.dal.dataobject.dept.PostDO; import co.yixiang.yshop.module.system.service.dept.PostService; import com.mzt.logapi.service.IParseFunction; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; /** * 岗位名字的 {@link IParseFunction} 实现类 * * @author HUIHUI */ @Slf4j @Component public class PostParseFunction implements IParseFunction { public static final String NAME = "getPostById"; @Resource private PostService postService; @Override public String functionName() { return NAME; } @Override public String apply(Object value) { if (StrUtil.isEmptyIfStr(value)) { return ""; } // 获取岗位信息 PostDO post = postService.getPost(Convert.toLong(value)); if (post == null) { log.warn("[apply][获取岗位{{}}为空", value); return ""; } return post.getName(); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/operatelog/core/SexParseFunction.java ================================================ package co.yixiang.yshop.module.system.framework.operatelog.core; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.dict.core.DictFrameworkUtils; import co.yixiang.yshop.module.system.enums.DictTypeConstants; import com.mzt.logapi.service.IParseFunction; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; /** * 行业的 {@link IParseFunction} 实现类 * * @author HUIHUI */ @Component @Slf4j public class SexParseFunction implements IParseFunction { public static final String NAME = "getSex"; @Override public boolean executeBefore() { return true; // 先转换值后对比 } @Override public String functionName() { return NAME; } @Override public String apply(Object value) { if (StrUtil.isEmptyIfStr(value)) { return ""; } return DictFrameworkUtils.getDictDataLabel(DictTypeConstants.USER_SEX, value.toString()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/sms/config/SmsCodeProperties.java ================================================ package co.yixiang.yshop.module.system.framework.sms.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; import jakarta.validation.constraints.NotNull; import java.time.Duration; @ConfigurationProperties(prefix = "yshop.sms-code") @Validated @Data public class SmsCodeProperties { /** * 过期时间 */ @NotNull(message = "过期时间不能为空") private Duration expireTimes; /** * 短信发送频率 */ @NotNull(message = "短信发送频率不能为空") private Duration sendFrequency; /** * 每日发送最大数量 */ @NotNull(message = "每日发送最大数量不能为空") private Integer sendMaximumQuantityPerDay; /** * 验证码最小值 */ @NotNull(message = "验证码最小值不能为空") private Integer beginCode; /** * 验证码最大值 */ @NotNull(message = "验证码最大值不能为空") private Integer endCode; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/sms/config/SmsConfiguration.java ================================================ package co.yixiang.yshop.module.system.framework.sms.config; import co.yixiang.yshop.module.system.framework.sms.core.client.SmsClientFactory; import co.yixiang.yshop.module.system.framework.sms.core.client.impl.SmsClientFactoryImpl; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 短信配置类,包括短信客户端、短信验证码两部分 * * @author yshop */ @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(SmsCodeProperties.class) public class SmsConfiguration { @Bean public SmsClientFactory smsClientFactory() { return new SmsClientFactoryImpl(); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/sms/core/client/SmsClient.java ================================================ package co.yixiang.yshop.module.system.framework.sms.core.client; import co.yixiang.yshop.framework.common.core.KeyValue; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsSendRespDTO; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; import java.util.List; /** * 短信客户端,用于对接各短信平台的 SDK,实现短信发送等功能 * * @author zzf * @since 2021/1/25 14:14 */ public interface SmsClient { /** * 获得渠道编号 * * @return 渠道编号 */ Long getId(); /** * 发送消息 * * @param logId 日志编号 * @param mobile 手机号 * @param apiTemplateId 短信 API 的模板编号 * @param templateParams 短信模板参数。通过 List 数组,保证参数的顺序 * @return 短信发送结果 */ SmsSendRespDTO sendSms(Long logId, String mobile, String apiTemplateId, List> templateParams) throws Throwable; /** * 解析接收短信的接收结果 * * @param text 结果 * @return 结果内容 * @throws Throwable 当解析 text 发生异常时,则会抛出异常 */ List parseSmsReceiveStatus(String text) throws Throwable; /** * 查询指定的短信模板 * * @param apiTemplateId 短信 API 的模板编号 * @return 短信模板 */ SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/sms/core/client/SmsClientFactory.java ================================================ package co.yixiang.yshop.module.system.framework.sms.core.client; import co.yixiang.yshop.module.system.framework.sms.core.property.SmsChannelProperties; /** * 短信客户端的工厂接口 * * @author zzf * @since 2021/1/28 14:01 */ public interface SmsClientFactory { /** * 获得短信 Client * * @param channelId 渠道编号 * @return 短信 Client */ SmsClient getSmsClient(Long channelId); /** * 获得短信 Client * * @param channelCode 渠道编码 * @return 短信 Client */ SmsClient getSmsClient(String channelCode); /** * 创建短信 Client * * @param properties 配置对象 */ void createOrUpdateSmsClient(SmsChannelProperties properties); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/sms/core/client/dto/SmsReceiveRespDTO.java ================================================ package co.yixiang.yshop.module.system.framework.sms.core.client.dto; import lombok.Data; import java.time.LocalDateTime; /** * 消息接收 Response DTO * * @author yshop */ @Data public class SmsReceiveRespDTO { /** * 是否接收成功 */ private Boolean success; /** * API 接收结果的编码 */ private String errorCode; /** * API 接收结果的说明 */ private String errorMsg; /** * 手机号 */ private String mobile; /** * 用户接收时间 */ private LocalDateTime receiveTime; /** * 短信 API 发送返回的序号 */ private String serialNo; /** * 短信日志编号 * * 对应 SysSmsLogDO 的编号 */ private Long logId; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/sms/core/client/dto/SmsSendRespDTO.java ================================================ package co.yixiang.yshop.module.system.framework.sms.core.client.dto; import lombok.Data; /** * 短信发送 Response DTO * * @author yshop */ @Data public class SmsSendRespDTO { /** * 是否成功 */ private Boolean success; /** * API 请求编号 */ private String apiRequestId; // ==================== 成功时字段 ==================== /** * 短信 API 发送返回的序号 */ private String serialNo; // ==================== 失败时字段 ==================== /** * API 返回错误码 * * 由于第三方的错误码可能是字符串,所以使用 String 类型 */ private String apiCode; /** * API 返回提示 */ private String apiMsg; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/sms/core/client/dto/SmsTemplateRespDTO.java ================================================ package co.yixiang.yshop.module.system.framework.sms.core.client.dto; import co.yixiang.yshop.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; import lombok.Data; /** * 短信模板 Response DTO * * @author yshop */ @Data public class SmsTemplateRespDTO { /** * 模板编号 */ private String id; /** * 短信内容 */ private String content; /** * 审核状态 * * 枚举 {@link SmsTemplateAuditStatusEnum} */ private Integer auditStatus; /** * 审核未通过的理由 */ private String auditReason; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/sms/core/client/impl/AbstractSmsClient.java ================================================ package co.yixiang.yshop.module.system.framework.sms.core.client.impl; import co.yixiang.yshop.module.system.framework.sms.core.client.SmsClient; import co.yixiang.yshop.module.system.framework.sms.core.property.SmsChannelProperties; import lombok.extern.slf4j.Slf4j; /** * 短信客户端的抽象类,提供模板方法,减少子类的冗余代码 * * @author zzf * @since 2021/2/1 9:28 */ @Slf4j public abstract class AbstractSmsClient implements SmsClient { /** * 短信渠道配置 */ protected volatile SmsChannelProperties properties; public AbstractSmsClient(SmsChannelProperties properties) { this.properties = properties; } /** * 初始化 */ public final void init() { doInit(); log.debug("[init][配置({}) 初始化完成]", properties); } /** * 自定义初始化 */ protected abstract void doInit(); public final void refresh(SmsChannelProperties properties) { // 判断是否更新 if (properties.equals(this.properties)) { return; } log.info("[refresh][配置({})发生变化,重新初始化]", properties); this.properties = properties; // 初始化 this.init(); } @Override public Long getId() { return properties.getId(); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/sms/core/client/impl/AliyunSmsClient.java ================================================ package co.yixiang.yshop.module.system.framework.sms.core.client.impl; import cn.hutool.core.lang.Assert; import co.yixiang.yshop.framework.common.core.KeyValue; import co.yixiang.yshop.framework.common.util.collection.MapUtils; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsSendRespDTO; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; import co.yixiang.yshop.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; import co.yixiang.yshop.module.system.framework.sms.core.property.SmsChannelProperties; import com.aliyuncs.DefaultAcsClient; import com.aliyuncs.IAcsClient; import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest; import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateResponse; import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse; import com.aliyuncs.profile.DefaultProfile; import com.aliyuncs.profile.IClientProfile; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.annotations.VisibleForTesting; import lombok.Data; import lombok.extern.slf4j.Slf4j; import java.time.LocalDateTime; import java.util.List; import java.util.Objects; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertList; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; import static co.yixiang.yshop.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT; /** * 阿里短信客户端的实现类 * * @author zzf * @since 2021/1/25 14:17 */ @Slf4j public class AliyunSmsClient extends AbstractSmsClient { /** * 调用成功 code */ public static final String API_CODE_SUCCESS = "OK"; /** * REGION, 使用杭州 */ private static final String ENDPOINT = "cn-hangzhou"; /** * 阿里云客户端 */ private volatile IAcsClient client; public AliyunSmsClient(SmsChannelProperties properties) { super(properties); Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); } @Override protected void doInit() { IClientProfile profile = DefaultProfile.getProfile(ENDPOINT, properties.getApiKey(), properties.getApiSecret()); client = new DefaultAcsClient(profile); } @Override public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, List> templateParams) throws Throwable { // 构建请求 SendSmsRequest request = new SendSmsRequest(); request.setPhoneNumbers(mobile); request.setSignName(properties.getSignature()); request.setTemplateCode(apiTemplateId); request.setTemplateParam(JsonUtils.toJsonString(MapUtils.convertMap(templateParams))); request.setOutId(String.valueOf(sendLogId)); // 执行请求 SendSmsResponse response = client.getAcsResponse(request); return new SmsSendRespDTO().setSuccess(Objects.equals(response.getCode(), API_CODE_SUCCESS)).setSerialNo(response.getBizId()) .setApiRequestId(response.getRequestId()).setApiCode(response.getCode()).setApiMsg(response.getMessage()); } @Override public List parseSmsReceiveStatus(String text) { List statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class); return convertList(statuses, status -> new SmsReceiveRespDTO().setSuccess(status.getSuccess()) .setErrorCode(status.getErrCode()).setErrorMsg(status.getErrMsg()) .setMobile(status.getPhoneNumber()).setReceiveTime(status.getReportTime()) .setSerialNo(status.getBizId()).setLogId(Long.valueOf(status.getOutId()))); } @Override public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable { // 构建请求 QuerySmsTemplateRequest request = new QuerySmsTemplateRequest(); request.setTemplateCode(apiTemplateId); // 执行请求 QuerySmsTemplateResponse response = client.getAcsResponse(request); if (response.getTemplateStatus() == null) { return null; } return new SmsTemplateRespDTO().setId(response.getTemplateCode()).setContent(response.getTemplateContent()) .setAuditStatus(convertSmsTemplateAuditStatus(response.getTemplateStatus())).setAuditReason(response.getReason()); } @VisibleForTesting Integer convertSmsTemplateAuditStatus(Integer templateStatus) { switch (templateStatus) { case 0: return SmsTemplateAuditStatusEnum.CHECKING.getStatus(); case 1: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); case 2: return SmsTemplateAuditStatusEnum.FAIL.getStatus(); default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus)); } } /** * 短信接收状态 * * 参见 文档 * * @author yshop */ @Data public static class SmsReceiveStatus { /** * 手机号 */ @JsonProperty("phone_number") private String phoneNumber; /** * 发送时间 */ @JsonProperty("send_time") @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) private LocalDateTime sendTime; /** * 状态报告时间 */ @JsonProperty("report_time") @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) private LocalDateTime reportTime; /** * 是否接收成功 */ private Boolean success; /** * 状态报告说明 */ @JsonProperty("err_msg") private String errMsg; /** * 状态报告编码 */ @JsonProperty("err_code") private String errCode; /** * 发送序列号 */ @JsonProperty("biz_id") private String bizId; /** * 用户序列号 * * 这里我们传递的是 SysSmsLogDO 的日志编号 */ @JsonProperty("out_id") private String outId; /** * 短信长度,例如说 1、2、3 * * 140 字节算一条短信,短信长度超过 140 字节时会拆分成多条短信发送 */ @JsonProperty("sms_size") private Integer smsSize; } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/sms/core/client/impl/DebugDingTalkSmsClient.java ================================================ package co.yixiang.yshop.module.system.framework.sms.core.client.impl; import cn.hutool.core.codec.Base64; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.digest.DigestUtil; import cn.hutool.crypto.digest.HmacAlgorithm; import cn.hutool.http.HttpUtil; import co.yixiang.yshop.framework.common.core.KeyValue; import co.yixiang.yshop.framework.common.util.collection.MapUtils; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsSendRespDTO; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; import co.yixiang.yshop.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; import co.yixiang.yshop.module.system.framework.sms.core.property.SmsChannelProperties; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; /** * 基于钉钉 WebHook 实现的调试的短信客户端实现类 * * 考虑到省钱,我们使用钉钉 WebHook 模拟发送短信,方便调试。 * * @author yshop */ public class DebugDingTalkSmsClient extends AbstractSmsClient { public DebugDingTalkSmsClient(SmsChannelProperties properties) { super(properties); Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); } @Override protected void doInit() { } @Override public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, List> templateParams) throws Throwable { // 构建请求 String url = buildUrl("robot/send"); Map params = new HashMap<>(); params.put("msgtype", "text"); String content = String.format("【模拟短信】\n手机号:%s\n短信日志编号:%d\n模板参数:%s", mobile, sendLogId, MapUtils.convertMap(templateParams)); params.put("text", MapUtil.builder().put("content", content).build()); // 执行请求 String responseText = HttpUtil.post(url, JsonUtils.toJsonString(params)); // 解析结果 Map responseObj = JsonUtils.parseObject(responseText, Map.class); String errorCode = MapUtil.getStr(responseObj, "errcode"); return new SmsSendRespDTO().setSuccess(Objects.equals(errorCode, "0")).setSerialNo(StrUtil.uuid()) .setApiCode(errorCode).setApiMsg(MapUtil.getStr(responseObj, "errorMsg")); } /** * 构建请求地址 * * 参见 文档 * * @param path 请求路径 * @return 请求地址 */ @SuppressWarnings("SameParameterValue") private String buildUrl(String path) { // 生成 timestamp long timestamp = System.currentTimeMillis(); // 生成 sign String secret = properties.getApiSecret(); String stringToSign = timestamp + "\n" + secret; byte[] signData = DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.bytes(secret)).digest(stringToSign); String sign = Base64.encode(signData); // 构建最终 URL return String.format("https://oapi.dingtalk.com/%s?access_token=%s×tamp=%d&sign=%s", path, properties.getApiKey(), timestamp, sign); } @Override public List parseSmsReceiveStatus(String text) { throw new UnsupportedOperationException("模拟短信客户端,暂时无需解析回调"); } @Override public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) { return new SmsTemplateRespDTO().setId(apiTemplateId).setContent("") .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(""); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java ================================================ package co.yixiang.yshop.module.system.framework.sms.core.client.impl; import co.yixiang.yshop.module.system.framework.sms.core.client.SmsClient; import co.yixiang.yshop.module.system.framework.sms.core.client.SmsClientFactory; import co.yixiang.yshop.module.system.framework.sms.core.enums.SmsChannelEnum; import co.yixiang.yshop.module.system.framework.sms.core.property.SmsChannelProperties; import lombok.extern.slf4j.Slf4j; import org.springframework.util.Assert; import org.springframework.validation.annotation.Validated; import java.util.Arrays; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * 短信客户端工厂接口 * * @author zzf */ @Validated @Slf4j public class SmsClientFactoryImpl implements SmsClientFactory { /** * 短信客户端 Map * key:渠道编号,使用 {@link SmsChannelProperties#getId()} */ private final ConcurrentMap channelIdClients = new ConcurrentHashMap<>(); /** * 短信客户端 Map * key:渠道编码,使用 {@link SmsChannelProperties#getCode()} ()} * * 注意,一些场景下,需要获得某个渠道类型的客户端,所以需要使用它。 * 例如说,解析短信接收结果,是相对通用的,不需要使用某个渠道编号的 {@link #channelIdClients} */ private final ConcurrentMap channelCodeClients = new ConcurrentHashMap<>(); public SmsClientFactoryImpl() { // 初始化 channelCodeClients 集合 Arrays.stream(SmsChannelEnum.values()).forEach(channel -> { // 创建一个空的 SmsChannelProperties 对象 SmsChannelProperties properties = new SmsChannelProperties().setCode(channel.getCode()) .setApiKey("default default").setApiSecret("default"); // 创建 Sms 客户端 AbstractSmsClient smsClient = createSmsClient(properties); channelCodeClients.put(channel.getCode(), smsClient); }); } @Override public SmsClient getSmsClient(Long channelId) { return channelIdClients.get(channelId); } @Override public SmsClient getSmsClient(String channelCode) { return channelCodeClients.get(channelCode); } @Override public void createOrUpdateSmsClient(SmsChannelProperties properties) { AbstractSmsClient client = channelIdClients.get(properties.getId()); if (client == null) { client = this.createSmsClient(properties); client.init(); channelIdClients.put(client.getId(), client); } else { client.refresh(properties); } } private AbstractSmsClient createSmsClient(SmsChannelProperties properties) { SmsChannelEnum channelEnum = SmsChannelEnum.getByCode(properties.getCode()); Assert.notNull(channelEnum, String.format("渠道类型(%s) 为空", channelEnum)); // 创建客户端 switch (channelEnum) { case ALIYUN: return new AliyunSmsClient(properties); case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties); case TENCENT: return new TencentSmsClient(properties); } // 创建失败,错误日志 + 抛出异常 log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties); throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", properties)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/sms/core/client/impl/TencentSmsClient.java ================================================ package co.yixiang.yshop.module.system.framework.sms.core.client.impl; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.core.KeyValue; import co.yixiang.yshop.framework.common.util.collection.ArrayUtils; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsSendRespDTO; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; import co.yixiang.yshop.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; import co.yixiang.yshop.module.system.framework.sms.core.property.SmsChannelProperties; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.annotations.VisibleForTesting; import com.tencentcloudapi.common.Credential; import com.tencentcloudapi.sms.v20210111.SmsClient; import com.tencentcloudapi.sms.v20210111.models.*; import lombok.Data; import java.time.LocalDateTime; import java.util.List; import java.util.Objects; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertList; import static co.yixiang.yshop.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; import static co.yixiang.yshop.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT; /** * 腾讯云短信功能实现 * * 参见 文档 * * @author shiwp */ public class TencentSmsClient extends AbstractSmsClient { /** * 调用成功 code */ public static final String API_CODE_SUCCESS = "Ok"; /** * REGION,使用南京 */ private static final String ENDPOINT = "ap-nanjing"; /** * 是否国际/港澳台短信: * * 0:表示国内短信。 * 1:表示国际/港澳台短信。 */ private static final long INTERNATIONAL_CHINA = 0L; private SmsClient client; public TencentSmsClient(SmsChannelProperties properties) { super(properties); Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); validateSdkAppId(properties); } @Override protected void doInit() { // 实例化一个认证对象,入参需要传入腾讯云账户密钥对 secretId,secretKey Credential credential = new Credential(getApiKey(), properties.getApiSecret()); client = new SmsClient(credential, ENDPOINT); } /** * 参数校验腾讯云的 SDK AppId * * 原因是:腾讯云发放短信的时候,需要额外的参数 sdkAppId * * 解决方案:考虑到不破坏原有的 apiKey + apiSecret 的结构,所以将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。 * * @param properties 配置 */ private static void validateSdkAppId(SmsChannelProperties properties) { String combineKey = properties.getApiKey(); Assert.notEmpty(combineKey, "apiKey 不能为空"); String[] keys = combineKey.trim().split(" "); Assert.isTrue(keys.length == 2, "腾讯云短信 apiKey 配置格式错误,请配置 为[secretId sdkAppId]"); } private String getSdkAppId() { return StrUtil.subAfter(properties.getApiKey(), " ", true); } private String getApiKey() { return StrUtil.subBefore(properties.getApiKey(), " ", true); } @Override public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, List> templateParams) throws Throwable { // 构建请求 SendSmsRequest request = new SendSmsRequest(); request.setSmsSdkAppId(getSdkAppId()); request.setPhoneNumberSet(new String[]{mobile}); request.setSignName(properties.getSignature()); request.setTemplateId(apiTemplateId); request.setTemplateParamSet(ArrayUtils.toArray(templateParams, e -> String.valueOf(e.getValue()))); request.setSessionContext(JsonUtils.toJsonString(new SessionContext().setLogId(sendLogId))); // 执行请求 SendSmsResponse response = client.SendSms(request); SendStatus status = response.getSendStatusSet()[0]; return new SmsSendRespDTO().setSuccess(Objects.equals(status.getCode(), API_CODE_SUCCESS)).setSerialNo(status.getSerialNo()) .setApiRequestId(response.getRequestId()).setApiCode(status.getCode()).setApiMsg(status.getMessage()); } @Override public List parseSmsReceiveStatus(String text) { List callback = JsonUtils.parseArray(text, SmsReceiveStatus.class); return convertList(callback, status -> new SmsReceiveRespDTO() .setSuccess(SmsReceiveStatus.SUCCESS_CODE.equalsIgnoreCase(status.getStatus())) .setErrorCode(status.getErrCode()).setErrorMsg(status.getDescription()) .setMobile(status.getMobile()).setReceiveTime(status.getReceiveTime()) .setSerialNo(status.getSerialNo()).setLogId(status.getSessionContext().getLogId())); } @Override public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable { // 构建请求 DescribeSmsTemplateListRequest request = new DescribeSmsTemplateListRequest(); request.setTemplateIdSet(new Long[]{Long.parseLong(apiTemplateId)}); request.setInternational(INTERNATIONAL_CHINA); // 执行请求 DescribeSmsTemplateListResponse response = client.DescribeSmsTemplateList(request); DescribeTemplateListStatus status = response.getDescribeTemplateStatusSet()[0]; if (status == null || status.getStatusCode() == null) { return null; } return new SmsTemplateRespDTO().setId(status.getTemplateId().toString()).setContent(status.getTemplateContent()) .setAuditStatus(convertSmsTemplateAuditStatus(status.getStatusCode().intValue())).setAuditReason(status.getReviewReply()); } @VisibleForTesting Integer convertSmsTemplateAuditStatus(int templateStatus) { switch (templateStatus) { case 1: return SmsTemplateAuditStatusEnum.CHECKING.getStatus(); case 0: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); case -1: return SmsTemplateAuditStatusEnum.FAIL.getStatus(); default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus)); } } @Data private static class SmsReceiveStatus { /** * 短信接受成功 code */ public static final String SUCCESS_CODE = "SUCCESS"; /** * 用户实际接收到短信的时间 */ @JsonProperty("user_receive_time") @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) private LocalDateTime receiveTime; /** * 国家(或地区)码 */ @JsonProperty("nationcode") private String nationCode; /** * 手机号码 */ private String mobile; /** * 实际是否收到短信接收状态,SUCCESS(成功)、FAIL(失败) */ @JsonProperty("report_status") private String status; /** * 用户接收短信状态码错误信息 */ @JsonProperty("errmsg") private String errCode; /** * 用户接收短信状态描述 */ @JsonProperty("description") private String description; /** * 本次发送标识 ID(与发送接口返回的SerialNo对应) */ @JsonProperty("sid") private String serialNo; /** * 用户的 session 内容(与发送接口的请求参数 SessionContext 一致) */ @JsonProperty("ext") private SessionContext sessionContext; } @VisibleForTesting @Data static class SessionContext { /** * 发送短信记录id */ private Long logId; } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/sms/core/enums/SmsChannelEnum.java ================================================ package co.yixiang.yshop.module.system.framework.sms.core.enums; import cn.hutool.core.util.ArrayUtil; import lombok.AllArgsConstructor; import lombok.Getter; /** * 短信渠道枚举 * * @author zzf * @since 2021/1/25 10:56 */ @Getter @AllArgsConstructor public enum SmsChannelEnum { DEBUG_DING_TALK("DEBUG_DING_TALK", "调试(钉钉)"), ALIYUN("ALIYUN", "阿里云"), TENCENT("TENCENT", "腾讯云"), // HUA_WEI("HUA_WEI", "华为云"), ; /** * 编码 */ private final String code; /** * 名字 */ private final String name; public static SmsChannelEnum getByCode(String code) { return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java ================================================ package co.yixiang.yshop.module.system.framework.sms.core.enums; import lombok.AllArgsConstructor; import lombok.Getter; /** * 短信模板的审核状态枚举 * * @author yshop */ @AllArgsConstructor @Getter public enum SmsTemplateAuditStatusEnum { CHECKING(1), SUCCESS(2), FAIL(3); private final Integer status; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/sms/core/property/SmsChannelProperties.java ================================================ package co.yixiang.yshop.module.system.framework.sms.core.property; import co.yixiang.yshop.module.system.framework.sms.core.enums.SmsChannelEnum; import lombok.Data; import org.springframework.validation.annotation.Validated; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; /** * 短信渠道配置类 * * @author zzf * @since 2021/1/25 17:01 */ @Data @Validated public class SmsChannelProperties { /** * 渠道编号 */ @NotNull(message = "短信渠道 ID 不能为空") private Long id; /** * 短信签名 */ @NotEmpty(message = "短信签名不能为空") private String signature; /** * 渠道编码 * * 枚举 {@link SmsChannelEnum} */ @NotEmpty(message = "渠道编码不能为空") private String code; /** * 短信 API 的账号 */ @NotEmpty(message = "短信 API 的账号不能为空") private String apiKey; /** * 短信 API 的密钥 */ @NotEmpty(message = "短信 API 的密钥不能为空") private String apiSecret; /** * 短信发送回调 URL */ private String callbackUrl; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/framework/web/config/SystemWebConfiguration.java ================================================ package co.yixiang.yshop.module.system.framework.web.config; import co.yixiang.yshop.framework.swagger.config.YshopSwaggerAutoConfiguration; import org.springdoc.core.models.GroupedOpenApi; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * system 模块的 web 组件的 Configuration * * @author yshop */ @Configuration(proxyBeanMethods = false) public class SystemWebConfiguration { /** * system 模块的 API 分组 */ @Bean public GroupedOpenApi systemGroupedOpenApi() { return YshopSwaggerAutoConfiguration.buildGroupedOpenApi("system"); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/job/DemoJob.java ================================================ package co.yixiang.yshop.module.system.job; import co.yixiang.yshop.framework.quartz.core.handler.JobHandler; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import co.yixiang.yshop.framework.tenant.core.job.TenantJob; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.module.system.dal.mysql.user.AdminUserMapper; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; import java.util.List; @Component public class DemoJob implements JobHandler { @Resource private AdminUserMapper adminUserMapper; @Override @TenantJob // 标记多租户 public String execute(String param) { System.out.println("当前租户:" + TenantContextHolder.getTenantId()); List users = adminUserMapper.selectList(); return "用户数量:" + users.size(); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/mq/consumer/mail/MailSendConsumer.java ================================================ package co.yixiang.yshop.module.system.mq.consumer.mail; import co.yixiang.yshop.module.system.mq.message.mail.MailSendMessage; import co.yixiang.yshop.module.system.service.mail.MailSendService; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; /** * 针对 {@link MailSendMessage} 的消费者 * * @author yshop */ @Component @Slf4j public class MailSendConsumer { @Resource private MailSendService mailSendService; @EventListener @Async // Spring Event 默认在 Producer 发送的线程,通过 @Async 实现异步 public void onMessage(MailSendMessage message) { log.info("[onMessage][消息内容({})]", message); mailSendService.doSendMail(message); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/mq/consumer/sms/SmsSendConsumer.java ================================================ package co.yixiang.yshop.module.system.mq.consumer.sms; import co.yixiang.yshop.module.system.mq.message.sms.SmsSendMessage; import co.yixiang.yshop.module.system.service.sms.SmsSendService; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; /** * 针对 {@link SmsSendMessage} 的消费者 * * @author zzf */ @Component @Slf4j public class SmsSendConsumer { @Resource private SmsSendService smsSendService; @EventListener @Async // Spring Event 默认在 Producer 发送的线程,通过 @Async 实现异步 public void onMessage(SmsSendMessage message) { log.info("[onMessage][消息内容({})]", message); smsSendService.doSendSms(message); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/mq/message/mail/MailSendMessage.java ================================================ package co.yixiang.yshop.module.system.mq.message.mail; import lombok.Data; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; /** * 邮箱发送消息 * * @author yshop */ @Data public class MailSendMessage { /** * 邮件日志编号 */ @NotNull(message = "邮件日志编号不能为空") private Long logId; /** * 接收邮件地址 */ @NotNull(message = "接收邮件地址不能为空") private String mail; /** * 邮件账号编号 */ @NotNull(message = "邮件账号编号不能为空") private Long accountId; /** * 邮件发件人 */ private String nickname; /** * 邮件标题 */ @NotEmpty(message = "邮件标题不能为空") private String title; /** * 邮件内容 */ @NotEmpty(message = "邮件内容不能为空") private String content; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/mq/message/sms/SmsSendMessage.java ================================================ package co.yixiang.yshop.module.system.mq.message.sms; import co.yixiang.yshop.framework.common.core.KeyValue; import lombok.Data; import jakarta.validation.constraints.NotNull; import java.util.List; /** * 短信发送消息 * * @author yshop */ @Data public class SmsSendMessage { /** * 短信日志编号 */ @NotNull(message = "短信日志编号不能为空") private Long logId; /** * 手机号 */ @NotNull(message = "手机号不能为空") private String mobile; /** * 短信渠道编号 */ @NotNull(message = "短信渠道编号不能为空") private Long channelId; /** * 短信 API 的模板编号 */ @NotNull(message = "短信 API 的模板编号不能为空") private String apiTemplateId; /** * 短信模板参数 */ private List> templateParams; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/mq/producer/mail/MailProducer.java ================================================ package co.yixiang.yshop.module.system.mq.producer.mail; import co.yixiang.yshop.module.system.mq.message.mail.MailSendMessage; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; /** * Mail 邮件相关消息的 Producer * * @author wangjingyi * @since 2021/4/19 13:33 */ @Slf4j @Component public class MailProducer { @Resource private ApplicationContext applicationContext; /** * 发送 {@link MailSendMessage} 消息 * * @param sendLogId 发送日志编码 * @param mail 接收邮件地址 * @param accountId 邮件账号编号 * @param nickname 邮件发件人 * @param title 邮件标题 * @param content 邮件内容 */ public void sendMailSendMessage(Long sendLogId, String mail, Long accountId, String nickname, String title, String content) { MailSendMessage message = new MailSendMessage() .setLogId(sendLogId).setMail(mail).setAccountId(accountId) .setNickname(nickname).setTitle(title).setContent(content); applicationContext.publishEvent(message); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/mq/producer/sms/SmsProducer.java ================================================ package co.yixiang.yshop.module.system.mq.producer.sms; import co.yixiang.yshop.framework.common.core.KeyValue; import co.yixiang.yshop.module.system.mq.message.sms.SmsSendMessage; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; import java.util.List; /** * Sms 短信相关消息的 Producer * * @author zzf * @since 2021/3/9 16:35 */ @Slf4j @Component public class SmsProducer { @Resource private ApplicationContext applicationContext; /** * 发送 {@link SmsSendMessage} 消息 * * @param logId 短信日志编号 * @param mobile 手机号 * @param channelId 渠道编号 * @param apiTemplateId 短信模板编号 * @param templateParams 短信模板参数 */ public void sendSmsSendMessage(Long logId, String mobile, Long channelId, String apiTemplateId, List> templateParams) { SmsSendMessage message = new SmsSendMessage().setLogId(logId).setMobile(mobile); message.setChannelId(channelId).setApiTemplateId(apiTemplateId).setTemplateParams(templateParams); applicationContext.publishEvent(message); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/auth/AdminAuthService.java ================================================ package co.yixiang.yshop.module.system.service.auth; import co.yixiang.yshop.module.system.controller.admin.auth.vo.*; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import jakarta.validation.Valid; /** * 管理后台的认证 Service 接口 * * 提供用户的登录、登出的能力 * * @author yshop */ public interface AdminAuthService { /** * 验证账号 + 密码。如果通过,则返回用户 * * @param username 账号 * @param password 密码 * @return 用户 */ AdminUserDO authenticate(String username, String password); /** * 账号登录 * * @param reqVO 登录信息 * @return 登录结果 */ AuthLoginRespVO login(@Valid AuthLoginReqVO reqVO); /** * 基于 token 退出登录 * * @param token token * @param logType 登出类型 */ void logout(String token, Integer logType); /** * 短信验证码发送 * * @param reqVO 发送请求 */ void sendSmsCode(AuthSmsSendReqVO reqVO); /** * 短信登录 * * @param reqVO 登录信息 * @return 登录结果 */ AuthLoginRespVO smsLogin(AuthSmsLoginReqVO reqVO) ; /** * 社交快捷登录,使用 code 授权码 * * @param reqVO 登录信息 * @return 登录结果 */ AuthLoginRespVO socialLogin(@Valid AuthSocialLoginReqVO reqVO); /** * 刷新访问令牌 * * @param refreshToken 刷新令牌 * @return 登录结果 */ AuthLoginRespVO refreshToken(String refreshToken); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/auth/AdminAuthServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.auth; import cn.hutool.core.util.ObjectUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.util.monitor.TracerUtils; import co.yixiang.yshop.framework.common.util.servlet.ServletUtils; import co.yixiang.yshop.framework.common.util.validation.ValidationUtils; import co.yixiang.yshop.module.system.api.logger.dto.LoginLogCreateReqDTO; import co.yixiang.yshop.module.system.api.sms.SmsCodeApi; import co.yixiang.yshop.module.system.api.social.dto.SocialUserBindReqDTO; import co.yixiang.yshop.module.system.api.social.dto.SocialUserRespDTO; import co.yixiang.yshop.module.system.controller.admin.auth.vo.*; import co.yixiang.yshop.module.system.convert.auth.AuthConvert; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.module.system.enums.logger.LoginLogTypeEnum; import co.yixiang.yshop.module.system.enums.logger.LoginResultEnum; import co.yixiang.yshop.module.system.enums.oauth2.OAuth2ClientConstants; import co.yixiang.yshop.module.system.enums.sms.SmsSceneEnum; import co.yixiang.yshop.module.system.service.logger.LoginLogService; import co.yixiang.yshop.module.system.service.member.MemberService; import co.yixiang.yshop.module.system.service.oauth2.OAuth2TokenService; import co.yixiang.yshop.module.system.service.social.SocialUserService; import co.yixiang.yshop.module.system.service.user.AdminUserService; import com.google.common.annotations.VisibleForTesting; import com.xingyuv.captcha.model.common.ResponseModel; import com.xingyuv.captcha.model.vo.CaptchaVO; import com.xingyuv.captcha.service.CaptchaService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import jakarta.validation.Validator; import java.util.Objects; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.util.servlet.ServletUtils.getClientIP; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; /** * Auth Service 实现类 * * @author yshop */ @Service @Slf4j public class AdminAuthServiceImpl implements AdminAuthService { @Resource private AdminUserService userService; @Resource private LoginLogService loginLogService; @Resource private OAuth2TokenService oauth2TokenService; @Resource private SocialUserService socialUserService; @Resource private MemberService memberService; @Resource private Validator validator; @Resource private CaptchaService captchaService; @Resource private SmsCodeApi smsCodeApi; /** * 验证码的开关,默认为 true */ @Value("${yshop.captcha.enable:true}") private Boolean captchaEnable; @Override public AdminUserDO authenticate(String username, String password) { final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME; // 校验账号是否存在 AdminUserDO user = userService.getUserByUsername(username); if (user == null) { createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); throw exception(AUTH_LOGIN_BAD_CREDENTIALS); } if (!userService.isPasswordMatch(password, user.getPassword())) { createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); throw exception(AUTH_LOGIN_BAD_CREDENTIALS); } // 校验是否禁用 if (CommonStatusEnum.isDisable(user.getStatus())) { createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.USER_DISABLED); throw exception(AUTH_LOGIN_USER_DISABLED); } return user; } @Override public AuthLoginRespVO login(AuthLoginReqVO reqVO) { // 校验验证码 validateCaptcha(reqVO); // 使用账号密码,进行登录 AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword()); // 如果 socialType 非空,说明需要绑定社交用户 if (reqVO.getSocialType() != null) { socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())); } // 创建 Token 令牌,记录登录日志 return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME); } @Override public void sendSmsCode(AuthSmsSendReqVO reqVO) { // 登录场景,验证是否存在 if (userService.getUserByMobile(reqVO.getMobile()) == null) { throw exception(AUTH_MOBILE_NOT_EXISTS); } // 发送验证码 smsCodeApi.sendSmsCode(AuthConvert.INSTANCE.convert(reqVO).setCreateIp(getClientIP())); } @Override public AuthLoginRespVO smsLogin(AuthSmsLoginReqVO reqVO) { // 校验验证码 smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.ADMIN_MEMBER_LOGIN.getScene(), getClientIP())); // 获得用户信息 AdminUserDO user = userService.getUserByMobile(reqVO.getMobile()); if (user == null) { throw exception(USER_NOT_EXISTS); } // 创建 Token 令牌,记录登录日志 return createTokenAfterLoginSuccess(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE); } private void createLoginLog(Long userId, String username, LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) { // 插入登录日志 LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); reqDTO.setLogType(logTypeEnum.getType()); reqDTO.setTraceId(TracerUtils.getTraceId()); reqDTO.setUserId(userId); reqDTO.setUserType(getUserType().getValue()); reqDTO.setUsername(username); reqDTO.setUserAgent(ServletUtils.getUserAgent()); reqDTO.setUserIp(ServletUtils.getClientIP()); reqDTO.setResult(loginResult.getResult()); loginLogService.createLoginLog(reqDTO); // 更新最后登录时间 if (userId != null && Objects.equals(LoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) { userService.updateUserLogin(userId, ServletUtils.getClientIP()); } } @Override public AuthLoginRespVO socialLogin(AuthSocialLoginReqVO reqVO) { // 使用 code 授权码,进行登录。然后,获得到绑定的用户编号 SocialUserRespDTO socialUser = socialUserService.getSocialUserByCode(UserTypeEnum.ADMIN.getValue(), reqVO.getType(), reqVO.getCode(), reqVO.getState()); if (socialUser == null || socialUser.getUserId() == null) { throw exception(AUTH_THIRD_LOGIN_NOT_BIND); } // 获得用户 AdminUserDO user = userService.getUser(socialUser.getUserId()); if (user == null) { throw exception(USER_NOT_EXISTS); } // 创建 Token 令牌,记录登录日志 return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL); } @VisibleForTesting void validateCaptcha(AuthLoginReqVO reqVO) { // 如果验证码关闭,则不进行校验 if (!captchaEnable) { return; } // 校验验证码 ValidationUtils.validate(validator, reqVO, AuthLoginReqVO.CodeEnableGroup.class); CaptchaVO captchaVO = new CaptchaVO(); captchaVO.setCaptchaVerification(reqVO.getCaptchaVerification()); ResponseModel response = captchaService.verification(captchaVO); // 验证不通过 if (!response.isSuccess()) { // 创建登录失败日志(验证码不正确) createLoginLog(null, reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME, LoginResultEnum.CAPTCHA_CODE_ERROR); throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR, response.getRepMsg()); } } private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType) { // 插入登陆日志 createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS); // 创建访问令牌 OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(), OAuth2ClientConstants.CLIENT_ID_DEFAULT, null); // 构建返回结果 return AuthConvert.INSTANCE.convert(accessTokenDO); } @Override public AuthLoginRespVO refreshToken(String refreshToken) { OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT); return AuthConvert.INSTANCE.convert(accessTokenDO); } @Override public void logout(String token, Integer logType) { // 删除访问令牌 OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.removeAccessToken(token); if (accessTokenDO == null) { return; } // 删除成功,则记录登出日志 createLogoutLog(accessTokenDO.getUserId(), accessTokenDO.getUserType(), logType); } private void createLogoutLog(Long userId, Integer userType, Integer logType) { LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); reqDTO.setLogType(logType); reqDTO.setTraceId(TracerUtils.getTraceId()); reqDTO.setUserId(userId); reqDTO.setUserType(userType); if (ObjectUtil.equal(getUserType().getValue(), userType)) { reqDTO.setUsername(getUsername(userId)); } else { reqDTO.setUsername(memberService.getMemberUserMobile(userId)); } reqDTO.setUserAgent(ServletUtils.getUserAgent()); reqDTO.setUserIp(ServletUtils.getClientIP()); reqDTO.setResult(LoginResultEnum.SUCCESS.getResult()); loginLogService.createLoginLog(reqDTO); } private String getUsername(Long userId) { if (userId == null) { return null; } AdminUserDO user = userService.getUser(userId); return user != null ? user.getUsername() : null; } private UserTypeEnum getUserType() { return UserTypeEnum.ADMIN; } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/dept/DeptService.java ================================================ package co.yixiang.yshop.module.system.service.dept; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.module.system.controller.admin.dept.vo.dept.DeptListReqVO; import co.yixiang.yshop.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.dept.DeptDO; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; /** * 部门 Service 接口 * * @author yshop */ public interface DeptService { /** * 创建部门 * * @param createReqVO 部门信息 * @return 部门编号 */ Long createDept(DeptSaveReqVO createReqVO); /** * 更新部门 * * @param updateReqVO 部门信息 */ void updateDept(DeptSaveReqVO updateReqVO); /** * 删除部门 * * @param id 部门编号 */ void deleteDept(Long id); /** * 获得部门信息 * * @param id 部门编号 * @return 部门信息 */ DeptDO getDept(Long id); /** * 获得部门信息数组 * * @param ids 部门编号数组 * @return 部门信息数组 */ List getDeptList(Collection ids); /** * 筛选部门列表 * * @param reqVO 筛选条件请求 VO * @return 部门列表 */ List getDeptList(DeptListReqVO reqVO); /** * 获得指定编号的部门 Map * * @param ids 部门编号数组 * @return 部门 Map */ default Map getDeptMap(Collection ids) { List list = getDeptList(ids); return CollectionUtils.convertMap(list, DeptDO::getId); } /** * 获得指定部门的所有子部门 * * @param id 部门编号 * @return 子部门列表 */ List getChildDeptList(Long id); /** * 获得所有子部门,从缓存中 * * @param id 父部门编号 * @return 子部门列表 */ Set getChildDeptIdListFromCache(Long id); /** * 校验部门们是否有效。如下情况,视为无效: * 1. 部门编号不存在 * 2. 部门被禁用 * * @param ids 角色编号数组 */ void validateDeptList(Collection ids); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/dept/DeptServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.dept; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.datapermission.core.annotation.DataPermission; import co.yixiang.yshop.module.system.controller.admin.dept.vo.dept.DeptListReqVO; import co.yixiang.yshop.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.dept.DeptDO; import co.yixiang.yshop.module.system.dal.mysql.dept.DeptMapper; import co.yixiang.yshop.module.system.dal.redis.RedisKeyConstants; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.*; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertSet; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; /** * 部门 Service 实现类 * * @author yshop */ @Service @Validated @Slf4j public class DeptServiceImpl implements DeptService { @Resource private DeptMapper deptMapper; @Override @CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存 public Long createDept(DeptSaveReqVO createReqVO) { if (createReqVO.getParentId() == null) { createReqVO.setParentId(DeptDO.PARENT_ID_ROOT); } // 校验父部门的有效性 validateParentDept(null, createReqVO.getParentId()); // 校验部门名的唯一性 validateDeptNameUnique(null, createReqVO.getParentId(), createReqVO.getName()); // 插入部门 DeptDO dept = BeanUtils.toBean(createReqVO, DeptDO.class); deptMapper.insert(dept); return dept.getId(); } @Override @CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存 public void updateDept(DeptSaveReqVO updateReqVO) { if (updateReqVO.getParentId() == null) { updateReqVO.setParentId(DeptDO.PARENT_ID_ROOT); } // 校验自己存在 validateDeptExists(updateReqVO.getId()); // 校验父部门的有效性 validateParentDept(updateReqVO.getId(), updateReqVO.getParentId()); // 校验部门名的唯一性 validateDeptNameUnique(updateReqVO.getId(), updateReqVO.getParentId(), updateReqVO.getName()); // 更新部门 DeptDO updateObj = BeanUtils.toBean(updateReqVO, DeptDO.class); deptMapper.updateById(updateObj); } @Override @CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存 public void deleteDept(Long id) { // 校验是否存在 validateDeptExists(id); // 校验是否有子部门 if (deptMapper.selectCountByParentId(id) > 0) { throw exception(DEPT_EXITS_CHILDREN); } // 删除部门 deptMapper.deleteById(id); } @VisibleForTesting void validateDeptExists(Long id) { if (id == null) { return; } DeptDO dept = deptMapper.selectById(id); if (dept == null) { throw exception(DEPT_NOT_FOUND); } } @VisibleForTesting void validateParentDept(Long id, Long parentId) { if (parentId == null || DeptDO.PARENT_ID_ROOT.equals(parentId)) { return; } // 1. 不能设置自己为父部门 if (Objects.equals(id, parentId)) { throw exception(DEPT_PARENT_ERROR); } // 2. 父部门不存在 DeptDO parentDept = deptMapper.selectById(parentId); if (parentDept == null) { throw exception(DEPT_PARENT_NOT_EXITS); } // 3. 递归校验父部门,如果父部门是自己的子部门,则报错,避免形成环路 if (id == null) { // id 为空,说明新增,不需要考虑环路 return; } for (int i = 0; i < Short.MAX_VALUE; i++) { // 3.1 校验环路 parentId = parentDept.getParentId(); if (Objects.equals(id, parentId)) { throw exception(DEPT_PARENT_IS_CHILD); } // 3.2 继续递归下一级父部门 if (parentId == null || DeptDO.PARENT_ID_ROOT.equals(parentId)) { break; } parentDept = deptMapper.selectById(parentId); if (parentDept == null) { break; } } } @VisibleForTesting void validateDeptNameUnique(Long id, Long parentId, String name) { DeptDO dept = deptMapper.selectByParentIdAndName(parentId, name); if (dept == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的部门 if (id == null) { throw exception(DEPT_NAME_DUPLICATE); } if (ObjectUtil.notEqual(dept.getId(), id)) { throw exception(DEPT_NAME_DUPLICATE); } } @Override public DeptDO getDept(Long id) { return deptMapper.selectById(id); } @Override public List getDeptList(Collection ids) { if (CollUtil.isEmpty(ids)) { return Collections.emptyList(); } return deptMapper.selectBatchIds(ids); } @Override public List getDeptList(DeptListReqVO reqVO) { List list = deptMapper.selectList(reqVO); list.sort(Comparator.comparing(DeptDO::getSort)); return list; } @Override public List getChildDeptList(Long id) { List children = new LinkedList<>(); // 遍历每一层 Collection parentIds = Collections.singleton(id); for (int i = 0; i < Short.MAX_VALUE; i++) { // 使用 Short.MAX_VALUE 避免 bug 场景下,存在死循环 // 查询当前层,所有的子部门 List depts = deptMapper.selectListByParentId(parentIds); // 1. 如果没有子部门,则结束遍历 if (CollUtil.isEmpty(depts)) { break; } // 2. 如果有子部门,继续遍历 children.addAll(depts); parentIds = convertSet(depts, DeptDO::getId); } return children; } @Override @DataPermission(enable = false) // 禁用数据权限,避免建立不正确的缓存 @Cacheable(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, key = "#id") public Set getChildDeptIdListFromCache(Long id) { List children = getChildDeptList(id); return convertSet(children, DeptDO::getId); } @Override public void validateDeptList(Collection ids) { if (CollUtil.isEmpty(ids)) { return; } // 获得科室信息 Map deptMap = getDeptMap(ids); // 校验 ids.forEach(id -> { DeptDO dept = deptMap.get(id); if (dept == null) { throw exception(DEPT_NOT_FOUND); } if (!CommonStatusEnum.ENABLE.getStatus().equals(dept.getStatus())) { throw exception(DEPT_NOT_ENABLE, dept.getName()); } }); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/dept/PostService.java ================================================ package co.yixiang.yshop.module.system.service.dept; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.controller.admin.dept.vo.post.PostPageReqVO; import co.yixiang.yshop.module.system.controller.admin.dept.vo.post.PostSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.dept.PostDO; import org.springframework.lang.Nullable; import java.util.Collection; import java.util.List; /** * 岗位 Service 接口 * * @author yshop */ public interface PostService { /** * 创建岗位 * * @param createReqVO 岗位信息 * @return 岗位编号 */ Long createPost(PostSaveReqVO createReqVO); /** * 更新岗位 * * @param updateReqVO 岗位信息 */ void updatePost(PostSaveReqVO updateReqVO); /** * 删除岗位信息 * * @param id 岗位编号 */ void deletePost(Long id); /** * 获得岗位列表 * * @param ids 岗位编号数组 * @return 部门列表 */ List getPostList(@Nullable Collection ids); /** * 获得符合条件的岗位列表 * * @param ids 岗位编号数组。如果为空,不进行筛选 * @param statuses 状态数组。如果为空,不进行筛选 * @return 部门列表 */ List getPostList(@Nullable Collection ids, @Nullable Collection statuses); /** * 获得岗位分页列表 * * @param reqVO 分页条件 * @return 部门分页列表 */ PageResult getPostPage(PostPageReqVO reqVO); /** * 获得岗位信息 * * @param id 岗位编号 * @return 岗位信息 */ PostDO getPost(Long id); /** * 校验岗位们是否有效。如下情况,视为无效: * 1. 岗位编号不存在 * 2. 岗位被禁用 * * @param ids 岗位编号数组 */ void validatePostList(Collection ids); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/dept/PostServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.dept; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.dept.vo.post.PostPageReqVO; import co.yixiang.yshop.module.system.controller.admin.dept.vo.post.PostSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.dept.PostDO; import co.yixiang.yshop.module.system.dal.mysql.dept.PostMapper; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertMap; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; /** * 岗位 Service 实现类 * * @author yshop */ @Service @Validated public class PostServiceImpl implements PostService { @Resource private PostMapper postMapper; @Override public Long createPost(PostSaveReqVO createReqVO) { // 校验正确性 validatePostForCreateOrUpdate(null, createReqVO.getName(), createReqVO.getCode()); // 插入岗位 PostDO post = BeanUtils.toBean(createReqVO, PostDO.class); postMapper.insert(post); return post.getId(); } @Override public void updatePost(PostSaveReqVO updateReqVO) { // 校验正确性 validatePostForCreateOrUpdate(updateReqVO.getId(), updateReqVO.getName(), updateReqVO.getCode()); // 更新岗位 PostDO updateObj = BeanUtils.toBean(updateReqVO, PostDO.class); postMapper.updateById(updateObj); } @Override public void deletePost(Long id) { // 校验是否存在 validatePostExists(id); // 删除部门 postMapper.deleteById(id); } private void validatePostForCreateOrUpdate(Long id, String name, String code) { // 校验自己存在 validatePostExists(id); // 校验岗位名的唯一性 validatePostNameUnique(id, name); // 校验岗位编码的唯一性 validatePostCodeUnique(id, code); } private void validatePostNameUnique(Long id, String name) { PostDO post = postMapper.selectByName(name); if (post == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的岗位 if (id == null) { throw exception(POST_NAME_DUPLICATE); } if (!post.getId().equals(id)) { throw exception(POST_NAME_DUPLICATE); } } private void validatePostCodeUnique(Long id, String code) { PostDO post = postMapper.selectByCode(code); if (post == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的岗位 if (id == null) { throw exception(POST_CODE_DUPLICATE); } if (!post.getId().equals(id)) { throw exception(POST_CODE_DUPLICATE); } } private void validatePostExists(Long id) { if (id == null) { return; } if (postMapper.selectById(id) == null) { throw exception(POST_NOT_FOUND); } } @Override public List getPostList(Collection ids) { if (CollUtil.isEmpty(ids)) { return Collections.emptyList(); } return postMapper.selectBatchIds(ids); } @Override public List getPostList(Collection ids, Collection statuses) { return postMapper.selectList(ids, statuses); } @Override public PageResult getPostPage(PostPageReqVO reqVO) { return postMapper.selectPage(reqVO); } @Override public PostDO getPost(Long id) { return postMapper.selectById(id); } @Override public void validatePostList(Collection ids) { if (CollUtil.isEmpty(ids)) { return; } // 获得岗位信息 List posts = postMapper.selectBatchIds(ids); Map postMap = convertMap(posts, PostDO::getId); // 校验 ids.forEach(id -> { PostDO post = postMap.get(id); if (post == null) { throw exception(POST_NOT_FOUND); } if (!CommonStatusEnum.ENABLE.getStatus().equals(post.getStatus())) { throw exception(POST_NOT_ENABLE, post.getName()); } }); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/dict/DictDataService.java ================================================ package co.yixiang.yshop.module.system.service.dict; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.controller.admin.dict.vo.data.DictDataPageReqVO; import co.yixiang.yshop.module.system.controller.admin.dict.vo.data.DictDataSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.dict.DictDataDO; import org.springframework.lang.Nullable; import java.util.Collection; import java.util.List; /** * 字典数据 Service 接口 * * @author ruoyi */ public interface DictDataService { /** * 创建字典数据 * * @param createReqVO 字典数据信息 * @return 字典数据编号 */ Long createDictData(DictDataSaveReqVO createReqVO); /** * 更新字典数据 * * @param updateReqVO 字典数据信息 */ void updateDictData(DictDataSaveReqVO updateReqVO); /** * 删除字典数据 * * @param id 字典数据编号 */ void deleteDictData(Long id); /** * 获得字典数据列表 * * @param status 状态 * @param dictType 字典类型 * @return 字典数据全列表 */ List getDictDataList(@Nullable Integer status, @Nullable String dictType); /** * 获得字典数据分页列表 * * @param pageReqVO 分页请求 * @return 字典数据分页列表 */ PageResult getDictDataPage(DictDataPageReqVO pageReqVO); /** * 获得字典数据详情 * * @param id 字典数据编号 * @return 字典数据 */ DictDataDO getDictData(Long id); /** * 获得指定字典类型的数据数量 * * @param dictType 字典类型 * @return 数据数量 */ long getDictDataCountByDictType(String dictType); /** * 校验字典数据们是否有效。如下情况,视为无效: * 1. 字典数据不存在 * 2. 字典数据被禁用 * * @param dictType 字典类型 * @param values 字典数据值的数组 */ void validateDictDataList(String dictType, Collection values); /** * 获得指定的字典数据 * * @param dictType 字典类型 * @param value 字典数据值 * @return 字典数据 */ DictDataDO getDictData(String dictType, String value); /** * 解析获得指定的字典数据,从缓存中 * * @param dictType 字典类型 * @param label 字典数据标签 * @return 字典数据 */ DictDataDO parseDictData(String dictType, String label); /** * 获得指定数据类型的字典数据列表 * * @param dictType 字典类型 * @return 字典数据列表 */ List getDictDataListByDictType(String dictType); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/dict/DictDataServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.dict; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.dict.vo.data.DictDataPageReqVO; import co.yixiang.yshop.module.system.controller.admin.dict.vo.data.DictDataSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.dict.DictDataDO; import co.yixiang.yshop.module.system.dal.dataobject.dict.DictTypeDO; import co.yixiang.yshop.module.system.dal.mysql.dict.DictDataMapper; import com.google.common.annotations.VisibleForTesting; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Map; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; /** * 字典数据 Service 实现类 * * @author ruoyi */ @Service @Slf4j public class DictDataServiceImpl implements DictDataService { /** * 排序 dictType > sort */ private static final Comparator COMPARATOR_TYPE_AND_SORT = Comparator .comparing(DictDataDO::getDictType) .thenComparingInt(DictDataDO::getSort); @Resource private DictTypeService dictTypeService; @Resource private DictDataMapper dictDataMapper; @Override public List getDictDataList(Integer status, String dictType) { List list = dictDataMapper.selectListByStatusAndDictType(status, dictType); list.sort(COMPARATOR_TYPE_AND_SORT); return list; } @Override public PageResult getDictDataPage(DictDataPageReqVO pageReqVO) { return dictDataMapper.selectPage(pageReqVO); } @Override public DictDataDO getDictData(Long id) { return dictDataMapper.selectById(id); } @Override public Long createDictData(DictDataSaveReqVO createReqVO) { // 校验字典类型有效 validateDictTypeExists(createReqVO.getDictType()); // 校验字典数据的值的唯一性 validateDictDataValueUnique(null, createReqVO.getDictType(), createReqVO.getValue()); // 插入字典类型 DictDataDO dictData = BeanUtils.toBean(createReqVO, DictDataDO.class); dictDataMapper.insert(dictData); return dictData.getId(); } @Override public void updateDictData(DictDataSaveReqVO updateReqVO) { // 校验自己存在 validateDictDataExists(updateReqVO.getId()); // 校验字典类型有效 validateDictTypeExists(updateReqVO.getDictType()); // 校验字典数据的值的唯一性 validateDictDataValueUnique(updateReqVO.getId(), updateReqVO.getDictType(), updateReqVO.getValue()); // 更新字典类型 DictDataDO updateObj = BeanUtils.toBean(updateReqVO, DictDataDO.class); dictDataMapper.updateById(updateObj); } @Override public void deleteDictData(Long id) { // 校验是否存在 validateDictDataExists(id); // 删除字典数据 dictDataMapper.deleteById(id); } @Override public long getDictDataCountByDictType(String dictType) { return dictDataMapper.selectCountByDictType(dictType); } @VisibleForTesting public void validateDictDataValueUnique(Long id, String dictType, String value) { DictDataDO dictData = dictDataMapper.selectByDictTypeAndValue(dictType, value); if (dictData == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的字典数据 if (id == null) { throw exception(DICT_DATA_VALUE_DUPLICATE); } if (!dictData.getId().equals(id)) { throw exception(DICT_DATA_VALUE_DUPLICATE); } } @VisibleForTesting public void validateDictDataExists(Long id) { if (id == null) { return; } DictDataDO dictData = dictDataMapper.selectById(id); if (dictData == null) { throw exception(DICT_DATA_NOT_EXISTS); } } @VisibleForTesting public void validateDictTypeExists(String type) { DictTypeDO dictType = dictTypeService.getDictType(type); if (dictType == null) { throw exception(DICT_TYPE_NOT_EXISTS); } if (!CommonStatusEnum.ENABLE.getStatus().equals(dictType.getStatus())) { throw exception(DICT_TYPE_NOT_ENABLE); } } @Override public void validateDictDataList(String dictType, Collection values) { if (CollUtil.isEmpty(values)) { return; } Map dictDataMap = CollectionUtils.convertMap( dictDataMapper.selectByDictTypeAndValues(dictType, values), DictDataDO::getValue); // 校验 values.forEach(value -> { DictDataDO dictData = dictDataMap.get(value); if (dictData == null) { throw exception(DICT_DATA_NOT_EXISTS); } if (!CommonStatusEnum.ENABLE.getStatus().equals(dictData.getStatus())) { throw exception(DICT_DATA_NOT_ENABLE, dictData.getLabel()); } }); } @Override public DictDataDO getDictData(String dictType, String value) { return dictDataMapper.selectByDictTypeAndValue(dictType, value); } @Override public DictDataDO parseDictData(String dictType, String label) { return dictDataMapper.selectByDictTypeAndLabel(dictType, label); } @Override public List getDictDataListByDictType(String dictType) { List list = dictDataMapper.selectList(DictDataDO::getDictType, dictType); list.sort(Comparator.comparing(DictDataDO::getSort)); return list; } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/dict/DictTypeService.java ================================================ package co.yixiang.yshop.module.system.service.dict; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.controller.admin.dict.vo.type.DictTypePageReqVO; import co.yixiang.yshop.module.system.controller.admin.dict.vo.type.DictTypeSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.dict.DictTypeDO; import java.util.List; /** * 字典类型 Service 接口 * * @author yshop */ public interface DictTypeService { /** * 创建字典类型 * * @param createReqVO 字典类型信息 * @return 字典类型编号 */ Long createDictType(DictTypeSaveReqVO createReqVO); /** * 更新字典类型 * * @param updateReqVO 字典类型信息 */ void updateDictType(DictTypeSaveReqVO updateReqVO); /** * 删除字典类型 * * @param id 字典类型编号 */ void deleteDictType(Long id); /** * 获得字典类型分页列表 * * @param pageReqVO 分页请求 * @return 字典类型分页列表 */ PageResult getDictTypePage(DictTypePageReqVO pageReqVO); /** * 获得字典类型详情 * * @param id 字典类型编号 * @return 字典类型 */ DictTypeDO getDictType(Long id); /** * 获得字典类型详情 * * @param type 字典类型 * @return 字典类型详情 */ DictTypeDO getDictType(String type); /** * 获得全部字典类型列表 * * @return 字典类型列表 */ List getDictTypeList(); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/dict/DictTypeServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.dict; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.dict.vo.type.DictTypePageReqVO; import co.yixiang.yshop.module.system.controller.admin.dict.vo.type.DictTypeSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.dict.DictTypeDO; import co.yixiang.yshop.module.system.dal.mysql.dict.DictTypeMapper; import com.google.common.annotations.VisibleForTesting; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import java.time.LocalDateTime; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; /** * 字典类型 Service 实现类 * * @author yshop */ @Service public class DictTypeServiceImpl implements DictTypeService { @Resource private DictDataService dictDataService; @Resource private DictTypeMapper dictTypeMapper; @Override public PageResult getDictTypePage(DictTypePageReqVO pageReqVO) { return dictTypeMapper.selectPage(pageReqVO); } @Override public DictTypeDO getDictType(Long id) { return dictTypeMapper.selectById(id); } @Override public DictTypeDO getDictType(String type) { return dictTypeMapper.selectByType(type); } @Override public Long createDictType(DictTypeSaveReqVO createReqVO) { // 校验字典类型的名字的唯一性 validateDictTypeNameUnique(null, createReqVO.getName()); // 校验字典类型的类型的唯一性 validateDictTypeUnique(null, createReqVO.getType()); // 插入字典类型 DictTypeDO dictType = BeanUtils.toBean(createReqVO, DictTypeDO.class); dictType.setDeletedTime(LocalDateTimeUtils.EMPTY); // 唯一索引,避免 null 值 dictTypeMapper.insert(dictType); return dictType.getId(); } @Override public void updateDictType(DictTypeSaveReqVO updateReqVO) { // 校验自己存在 validateDictTypeExists(updateReqVO.getId()); // 校验字典类型的名字的唯一性 validateDictTypeNameUnique(updateReqVO.getId(), updateReqVO.getName()); // 校验字典类型的类型的唯一性 validateDictTypeUnique(updateReqVO.getId(), updateReqVO.getType()); // 更新字典类型 DictTypeDO updateObj = BeanUtils.toBean(updateReqVO, DictTypeDO.class); dictTypeMapper.updateById(updateObj); } @Override public void deleteDictType(Long id) { // 校验是否存在 DictTypeDO dictType = validateDictTypeExists(id); // 校验是否有字典数据 if (dictDataService.getDictDataCountByDictType(dictType.getType()) > 0) { throw exception(DICT_TYPE_HAS_CHILDREN); } // 删除字典类型 dictTypeMapper.updateToDelete(id, LocalDateTime.now()); } @Override public List getDictTypeList() { return dictTypeMapper.selectList(); } @VisibleForTesting void validateDictTypeNameUnique(Long id, String name) { DictTypeDO dictType = dictTypeMapper.selectByName(name); if (dictType == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的字典类型 if (id == null) { throw exception(DICT_TYPE_NAME_DUPLICATE); } if (!dictType.getId().equals(id)) { throw exception(DICT_TYPE_NAME_DUPLICATE); } } @VisibleForTesting void validateDictTypeUnique(Long id, String type) { if (StrUtil.isEmpty(type)) { return; } DictTypeDO dictType = dictTypeMapper.selectByType(type); if (dictType == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的字典类型 if (id == null) { throw exception(DICT_TYPE_TYPE_DUPLICATE); } if (!dictType.getId().equals(id)) { throw exception(DICT_TYPE_TYPE_DUPLICATE); } } @VisibleForTesting DictTypeDO validateDictTypeExists(Long id) { if (id == null) { return null; } DictTypeDO dictType = dictTypeMapper.selectById(id); if (dictType == null) { throw exception(DICT_TYPE_NOT_EXISTS); } return dictType; } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/logger/LoginLogService.java ================================================ package co.yixiang.yshop.module.system.service.logger; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.api.logger.dto.LoginLogCreateReqDTO; import co.yixiang.yshop.module.system.controller.admin.logger.vo.loginlog.LoginLogPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.logger.LoginLogDO; import jakarta.validation.Valid; /** * 登录日志 Service 接口 */ public interface LoginLogService { /** * 获得登录日志分页 * * @param pageReqVO 分页条件 * @return 登录日志分页 */ PageResult getLoginLogPage(LoginLogPageReqVO pageReqVO); /** * 创建登录日志 * * @param reqDTO 日志信息 */ void createLoginLog(@Valid LoginLogCreateReqDTO reqDTO); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/logger/LoginLogServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.logger; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.api.logger.dto.LoginLogCreateReqDTO; import co.yixiang.yshop.module.system.controller.admin.logger.vo.loginlog.LoginLogPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.logger.LoginLogDO; import co.yixiang.yshop.module.system.dal.mysql.logger.LoginLogMapper; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; /** * 登录日志 Service 实现 */ @Service @Validated public class LoginLogServiceImpl implements LoginLogService { @Resource private LoginLogMapper loginLogMapper; @Override public PageResult getLoginLogPage(LoginLogPageReqVO pageReqVO) { return loginLogMapper.selectPage(pageReqVO); } @Override public void createLoginLog(LoginLogCreateReqDTO reqDTO) { LoginLogDO loginLog = BeanUtils.toBean(reqDTO, LoginLogDO.class); loginLogMapper.insert(loginLog); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/logger/OperateLogService.java ================================================ package co.yixiang.yshop.module.system.service.logger; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.api.logger.dto.OperateLogCreateReqDTO; import co.yixiang.yshop.module.system.api.logger.dto.OperateLogPageReqDTO; import co.yixiang.yshop.module.system.controller.admin.logger.vo.operatelog.OperateLogPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.logger.OperateLogDO; /** * 操作日志 Service 接口 * * @author yshop */ public interface OperateLogService { /** * 记录操作日志 * * @param createReqDTO 创建请求 */ void createOperateLog(OperateLogCreateReqDTO createReqDTO); /** * 获得操作日志分页列表 * * @param pageReqVO 分页条件 * @return 操作日志分页列表 */ PageResult getOperateLogPage(OperateLogPageReqVO pageReqVO); /** * 获得操作日志分页列表 * * @param pageReqVO 分页条件 * @return 操作日志分页列表 */ PageResult getOperateLogPage(OperateLogPageReqDTO pageReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/logger/OperateLogServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.logger; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.api.logger.dto.OperateLogCreateReqDTO; import co.yixiang.yshop.module.system.api.logger.dto.OperateLogPageReqDTO; import co.yixiang.yshop.module.system.controller.admin.logger.vo.operatelog.OperateLogPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.logger.OperateLogDO; import co.yixiang.yshop.module.system.dal.mysql.logger.OperateLogMapper; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; /** * 操作日志 Service 实现类 * * @author yshop */ @Service @Validated @Slf4j public class OperateLogServiceImpl implements OperateLogService { @Resource private OperateLogMapper operateLogMapper; @Override public void createOperateLog(OperateLogCreateReqDTO createReqDTO) { OperateLogDO log = BeanUtils.toBean(createReqDTO, OperateLogDO.class); operateLogMapper.insert(log); } @Override public PageResult getOperateLogPage(OperateLogPageReqVO pageReqVO) { return operateLogMapper.selectPage(pageReqVO); } @Override public PageResult getOperateLogPage(OperateLogPageReqDTO pageReqDTO) { return operateLogMapper.selectPage(pageReqDTO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/mail/MailAccountService.java ================================================ package co.yixiang.yshop.module.system.service.mail; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.controller.admin.mail.vo.account.MailAccountPageReqVO; import co.yixiang.yshop.module.system.controller.admin.mail.vo.account.MailAccountSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailAccountDO; import jakarta.validation.Valid; import java.util.List; /** * 邮箱账号 Service 接口 * * @author wangjingyi * @since 2022-03-21 */ public interface MailAccountService { /** * 创建邮箱账号 * * @param createReqVO 邮箱账号信息 * @return 编号 */ Long createMailAccount(@Valid MailAccountSaveReqVO createReqVO); /** * 修改邮箱账号 * * @param updateReqVO 邮箱账号信息 */ void updateMailAccount(@Valid MailAccountSaveReqVO updateReqVO); /** * 删除邮箱账号 * * @param id 编号 */ void deleteMailAccount(Long id); /** * 获取邮箱账号信息 * * @param id 编号 * @return 邮箱账号信息 */ MailAccountDO getMailAccount(Long id); /** * 从缓存中获取邮箱账号 * * @param id 编号 * @return 邮箱账号 */ MailAccountDO getMailAccountFromCache(Long id); /** * 获取邮箱账号分页信息 * * @param pageReqVO 邮箱账号分页参数 * @return 邮箱账号分页信息 */ PageResult getMailAccountPage(MailAccountPageReqVO pageReqVO); /** * 获取邮箱数组信息 * * @return 邮箱账号信息数组 */ List getMailAccountList(); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/mail/MailAccountServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.mail; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.mail.vo.account.MailAccountPageReqVO; import co.yixiang.yshop.module.system.controller.admin.mail.vo.account.MailAccountSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailAccountDO; import co.yixiang.yshop.module.system.dal.mysql.mail.MailAccountMapper; import co.yixiang.yshop.module.system.dal.redis.RedisKeyConstants; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.MAIL_ACCOUNT_NOT_EXISTS; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.MAIL_ACCOUNT_RELATE_TEMPLATE_EXISTS; /** * 邮箱账号 Service 实现类 * * @author wangjingyi * @since 2022-03-21 */ @Service @Validated @Slf4j public class MailAccountServiceImpl implements MailAccountService { @Resource private MailAccountMapper mailAccountMapper; @Resource private MailTemplateService mailTemplateService; @Override public Long createMailAccount(MailAccountSaveReqVO createReqVO) { MailAccountDO account = BeanUtils.toBean(createReqVO, MailAccountDO.class); mailAccountMapper.insert(account); return account.getId(); } @Override @CacheEvict(value = RedisKeyConstants.MAIL_ACCOUNT, key = "#updateReqVO.id") public void updateMailAccount(MailAccountSaveReqVO updateReqVO) { // 校验是否存在 validateMailAccountExists(updateReqVO.getId()); // 更新 MailAccountDO updateObj = BeanUtils.toBean(updateReqVO, MailAccountDO.class); mailAccountMapper.updateById(updateObj); } @Override @CacheEvict(value = RedisKeyConstants.MAIL_ACCOUNT, key = "#id") public void deleteMailAccount(Long id) { // 校验是否存在账号 validateMailAccountExists(id); // 校验是否存在关联模版 if (mailTemplateService.getMailTemplateCountByAccountId(id) > 0) { throw exception(MAIL_ACCOUNT_RELATE_TEMPLATE_EXISTS); } // 删除 mailAccountMapper.deleteById(id); } private void validateMailAccountExists(Long id) { if (mailAccountMapper.selectById(id) == null) { throw exception(MAIL_ACCOUNT_NOT_EXISTS); } } @Override public MailAccountDO getMailAccount(Long id) { return mailAccountMapper.selectById(id); } @Override @Cacheable(value = RedisKeyConstants.MAIL_ACCOUNT, key = "#id", unless = "#result == null") public MailAccountDO getMailAccountFromCache(Long id) { return getMailAccount(id); } @Override public PageResult getMailAccountPage(MailAccountPageReqVO pageReqVO) { return mailAccountMapper.selectPage(pageReqVO); } @Override public List getMailAccountList() { return mailAccountMapper.selectList(); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/mail/MailLogService.java ================================================ package co.yixiang.yshop.module.system.service.mail; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.controller.admin.mail.vo.log.MailLogPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailAccountDO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailLogDO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailTemplateDO; import java.util.Map; /** * 邮件日志 Service 接口 * * @author wangjingyi * @since 2022-03-21 */ public interface MailLogService { /** * 邮件日志分页 * * @param pageVO 分页参数 * @return 分页结果 */ PageResult getMailLogPage(MailLogPageReqVO pageVO); /** * 获得指定编号的邮件日志 * * @param id 日志编号 * @return 邮件日志 */ MailLogDO getMailLog(Long id); /** * 创建邮件日志 * * @param userId 用户编码 * @param userType 用户类型 * @param toMail 收件人邮件 * @param account 邮件账号信息 * @param template 模版信息 * @param templateContent 模版内容 * @param templateParams 模版参数 * @param isSend 是否发送成功 * @return 日志编号 */ Long createMailLog(Long userId, Integer userType, String toMail, MailAccountDO account, MailTemplateDO template , String templateContent, Map templateParams, Boolean isSend); /** * 更新邮件发送结果 * * @param logId 日志编号 * @param messageId 发送后的消息编号 * @param exception 发送异常 */ void updateMailSendResult(Long logId, String messageId, Exception exception); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/mail/MailLogServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.mail; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.controller.admin.mail.vo.log.MailLogPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailAccountDO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailLogDO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailTemplateDO; import co.yixiang.yshop.module.system.dal.mysql.mail.MailLogMapper; import co.yixiang.yshop.module.system.enums.mail.MailSendStatusEnum; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.time.LocalDateTime; import java.util.Map; import java.util.Objects; import static cn.hutool.core.exceptions.ExceptionUtil.getRootCauseMessage; /** * 邮件日志 Service 实现类 * * @author wangjingyi * @since 2022-03-21 */ @Service @Validated public class MailLogServiceImpl implements MailLogService { @Resource private MailLogMapper mailLogMapper; @Override public PageResult getMailLogPage(MailLogPageReqVO pageVO) { return mailLogMapper.selectPage(pageVO); } @Override public MailLogDO getMailLog(Long id) { return mailLogMapper.selectById(id); } @Override public Long createMailLog(Long userId, Integer userType, String toMail, MailAccountDO account, MailTemplateDO template, String templateContent, Map templateParams, Boolean isSend) { MailLogDO.MailLogDOBuilder logDOBuilder = MailLogDO.builder(); // 根据是否要发送,设置状态 logDOBuilder.sendStatus(Objects.equals(isSend, true) ? MailSendStatusEnum.INIT.getStatus() : MailSendStatusEnum.IGNORE.getStatus()) // 用户信息 .userId(userId).userType(userType).toMail(toMail) .accountId(account.getId()).fromMail(account.getMail()) // 模板相关字段 .templateId(template.getId()).templateCode(template.getCode()).templateNickname(template.getNickname()) .templateTitle(template.getTitle()).templateContent(templateContent).templateParams(templateParams); // 插入数据库 MailLogDO logDO = logDOBuilder.build(); mailLogMapper.insert(logDO); return logDO.getId(); } @Override public void updateMailSendResult(Long logId, String messageId, Exception exception) { // 1. 成功 if (exception == null) { mailLogMapper.updateById(new MailLogDO().setId(logId).setSendTime(LocalDateTime.now()) .setSendStatus(MailSendStatusEnum.SUCCESS.getStatus()).setSendMessageId(messageId)); return; } // 2. 失败 mailLogMapper.updateById(new MailLogDO().setId(logId).setSendTime(LocalDateTime.now()) .setSendStatus(MailSendStatusEnum.FAILURE.getStatus()).setSendException(getRootCauseMessage(exception))); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/mail/MailSendService.java ================================================ package co.yixiang.yshop.module.system.service.mail; import co.yixiang.yshop.module.system.mq.message.mail.MailSendMessage; import java.util.Map; /** * 邮件发送 Service 接口 * * @author wangjingyi * @since 2022-03-21 */ public interface MailSendService { /** * 发送单条邮件给管理后台的用户 * * @param mail 邮箱 * @param userId 用户编码 * @param templateCode 邮件模版编码 * @param templateParams 邮件模版参数 * @return 发送日志编号 */ Long sendSingleMailToAdmin(String mail, Long userId, String templateCode, Map templateParams); /** * 发送单条邮件给用户 APP 的用户 * * @param mail 邮箱 * @param userId 用户编码 * @param templateCode 邮件模版编码 * @param templateParams 邮件模版参数 * @return 发送日志编号 */ Long sendSingleMailToMember(String mail, Long userId, String templateCode, Map templateParams); /** * 发送单条邮件给用户 * * @param mail 邮箱 * @param userId 用户编码 * @param userType 用户类型 * @param templateCode 邮件模版编码 * @param templateParams 邮件模版参数 * @return 发送日志编号 */ Long sendSingleMail(String mail, Long userId, Integer userType, String templateCode, Map templateParams); /** * 执行真正的邮件发送 * 注意,该方法仅仅提供给 MQ Consumer 使用 * * @param message 邮件 */ void doSendMail(MailSendMessage message); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/mail/MailSendServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.mail; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailAccountDO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailTemplateDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.module.system.mq.message.mail.MailSendMessage; import co.yixiang.yshop.module.system.mq.producer.mail.MailProducer; import co.yixiang.yshop.module.system.service.member.MemberService; import co.yixiang.yshop.module.system.service.user.AdminUserService; import com.google.common.annotations.VisibleForTesting; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.dromara.hutool.extra.mail.*; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.util.Map; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; /** * 邮箱发送 Service 实现类 * * @author wangjingyi * @since 2022-03-21 */ @Service @Validated @Slf4j public class MailSendServiceImpl implements MailSendService { @Resource private AdminUserService adminUserService; @Resource private MemberService memberService; @Resource private MailAccountService mailAccountService; @Resource private MailTemplateService mailTemplateService; @Resource private MailLogService mailLogService; @Resource private MailProducer mailProducer; @Override public Long sendSingleMailToAdmin(String mail, Long userId, String templateCode, Map templateParams) { // 如果 mail 为空,则加载用户编号对应的邮箱 if (StrUtil.isEmpty(mail)) { AdminUserDO user = adminUserService.getUser(userId); if (user != null) { mail = user.getEmail(); } } // 执行发送 return sendSingleMail(mail, userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams); } @Override public Long sendSingleMailToMember(String mail, Long userId, String templateCode, Map templateParams) { // 如果 mail 为空,则加载用户编号对应的邮箱 if (StrUtil.isEmpty(mail)) { mail = memberService.getMemberUserEmail(userId); } // 执行发送 return sendSingleMail(mail, userId, UserTypeEnum.MEMBER.getValue(), templateCode, templateParams); } @Override public Long sendSingleMail(String mail, Long userId, Integer userType, String templateCode, Map templateParams) { // 校验邮箱模版是否合法 MailTemplateDO template = validateMailTemplate(templateCode); // 校验邮箱账号是否合法 MailAccountDO account = validateMailAccount(template.getAccountId()); // 校验邮箱是否存在 mail = validateMail(mail); validateTemplateParams(template, templateParams); // 创建发送日志。如果模板被禁用,则不发送短信,只记录日志 Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus()); String title = mailTemplateService.formatMailTemplateContent(template.getTitle(), templateParams); String content = mailTemplateService.formatMailTemplateContent(template.getContent(), templateParams); Long sendLogId = mailLogService.createMailLog(userId, userType, mail, account, template, content, templateParams, isSend); // 发送 MQ 消息,异步执行发送短信 if (isSend) { mailProducer.sendMailSendMessage(sendLogId, mail, account.getId(), template.getNickname(), title, content); } return sendLogId; } @Override public void doSendMail(MailSendMessage message) { // 1. 创建发送账号 MailAccountDO account = validateMailAccount(message.getAccountId()); MailAccount mailAccount = buildMailAccount(account, message.getNickname()); // 2. 发送邮件 try { String messageId = MailUtil.send(mailAccount, message.getMail(), message.getTitle(), message.getContent(), true); // 3. 更新结果(成功) mailLogService.updateMailSendResult(message.getLogId(), messageId, null); } catch (Exception e) { // 3. 更新结果(异常) mailLogService.updateMailSendResult(message.getLogId(), null, e); } } private MailAccount buildMailAccount(MailAccountDO account, String nickname) { String from = StrUtil.isNotEmpty(nickname) ? nickname + " <" + account.getMail() + ">" : account.getMail(); return new MailAccount().setFrom(from).setAuth(true) .setUser(account.getUsername()).setPass(account.getPassword().toCharArray()) .setHost(account.getHost()).setPort(account.getPort()) .setSslEnable(account.getSslEnable()).setStarttlsEnable(account.getStarttlsEnable()); } @VisibleForTesting MailTemplateDO validateMailTemplate(String templateCode) { // 获得邮件模板。考虑到效率,从缓存中获取 MailTemplateDO template = mailTemplateService.getMailTemplateByCodeFromCache(templateCode); // 邮件模板不存在 if (template == null) { throw exception(MAIL_TEMPLATE_NOT_EXISTS); } return template; } @VisibleForTesting MailAccountDO validateMailAccount(Long accountId) { // 获得邮箱账号。考虑到效率,从缓存中获取 MailAccountDO account = mailAccountService.getMailAccountFromCache(accountId); // 邮箱账号不存在 if (account == null) { throw exception(MAIL_ACCOUNT_NOT_EXISTS); } return account; } @VisibleForTesting String validateMail(String mail) { if (StrUtil.isEmpty(mail)) { throw exception(MAIL_SEND_MAIL_NOT_EXISTS); } return mail; } /** * 校验邮件参数是否确实 * * @param template 邮箱模板 * @param templateParams 参数列表 */ @VisibleForTesting void validateTemplateParams(MailTemplateDO template, Map templateParams) { template.getParams().forEach(key -> { Object value = templateParams.get(key); if (value == null) { throw exception(MAIL_SEND_TEMPLATE_PARAM_MISS, key); } }); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/mail/MailTemplateService.java ================================================ package co.yixiang.yshop.module.system.service.mail; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.controller.admin.mail.vo.template.MailTemplatePageReqVO; import co.yixiang.yshop.module.system.controller.admin.mail.vo.template.MailTemplateSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailTemplateDO; import jakarta.validation.Valid; import java.util.List; import java.util.Map; /** * 邮件模版 Service 接口 * * @author wangjingyi * @since 2022-03-21 */ public interface MailTemplateService { /** * 邮件模版创建 * * @param createReqVO 邮件信息 * @return 编号 */ Long createMailTemplate(@Valid MailTemplateSaveReqVO createReqVO); /** * 邮件模版修改 * * @param updateReqVO 邮件信息 */ void updateMailTemplate(@Valid MailTemplateSaveReqVO updateReqVO); /** * 邮件模版删除 * * @param id 编号 */ void deleteMailTemplate(Long id); /** * 获取邮件模版 * * @param id 编号 * @return 邮件模版 */ MailTemplateDO getMailTemplate(Long id); /** * 获取邮件模版分页 * * @param pageReqVO 模版信息 * @return 邮件模版分页信息 */ PageResult getMailTemplatePage(MailTemplatePageReqVO pageReqVO); /** * 获取邮件模板数组 * * @return 模版数组 */ List getMailTemplateList(); /** * 从缓存中获取邮件模版 * * @param code 模板编码 * @return 邮件模板 */ MailTemplateDO getMailTemplateByCodeFromCache(String code); /** * 邮件模版内容合成 * * @param content 邮件模版 * @param params 合成参数 * @return 格式化后的内容 */ String formatMailTemplateContent(String content, Map params); /** * 获得指定邮件账号下的邮件模板数量 * * @param accountId 账号编号 * @return 数量 */ long getMailTemplateCountByAccountId(Long accountId); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/mail/MailTemplateServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.mail; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ReUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.mail.vo.template.MailTemplatePageReqVO; import co.yixiang.yshop.module.system.controller.admin.mail.vo.template.MailTemplateSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailTemplateDO; import co.yixiang.yshop.module.system.dal.mysql.mail.MailTemplateMapper; import co.yixiang.yshop.module.system.dal.redis.RedisKeyConstants; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import jakarta.validation.Valid; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.MAIL_TEMPLATE_CODE_EXISTS; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.MAIL_TEMPLATE_NOT_EXISTS; /** * 邮箱模版 Service 实现类 * * @author wangjingyi * @since 2022-03-21 */ @Service @Validated @Slf4j public class MailTemplateServiceImpl implements MailTemplateService { /** * 正则表达式,匹配 {} 中的变量 */ private static final Pattern PATTERN_PARAMS = Pattern.compile("\\{(.*?)}"); @Resource private MailTemplateMapper mailTemplateMapper; @Override public Long createMailTemplate(MailTemplateSaveReqVO createReqVO) { // 校验 code 是否唯一 validateCodeUnique(null, createReqVO.getCode()); // 插入 MailTemplateDO template = BeanUtils.toBean(createReqVO, MailTemplateDO.class) .setParams(parseTemplateContentParams(createReqVO.getContent())); mailTemplateMapper.insert(template); return template.getId(); } @Override @CacheEvict(cacheNames = RedisKeyConstants.MAIL_TEMPLATE, allEntries = true) // allEntries 清空所有缓存,因为可能修改到 code 字段,不好清理 public void updateMailTemplate(@Valid MailTemplateSaveReqVO updateReqVO) { // 校验是否存在 validateMailTemplateExists(updateReqVO.getId()); // 校验 code 是否唯一 validateCodeUnique(updateReqVO.getId(),updateReqVO.getCode()); // 更新 MailTemplateDO updateObj = BeanUtils.toBean(updateReqVO, MailTemplateDO.class) .setParams(parseTemplateContentParams(updateReqVO.getContent())); mailTemplateMapper.updateById(updateObj); } @VisibleForTesting void validateCodeUnique(Long id, String code) { MailTemplateDO template = mailTemplateMapper.selectByCode(code); if (template == null) { return; } // 存在 template 记录的情况下 if (id == null // 新增时,说明重复 || ObjUtil.notEqual(id, template.getId())) { // 更新时,如果 id 不一致,说明重复 throw exception(MAIL_TEMPLATE_CODE_EXISTS); } } @Override @CacheEvict(cacheNames = RedisKeyConstants.MAIL_TEMPLATE, allEntries = true) // allEntries 清空所有缓存,因为 id 不是直接的缓存 code,不好清理 public void deleteMailTemplate(Long id) { // 校验是否存在 validateMailTemplateExists(id); // 删除 mailTemplateMapper.deleteById(id); } private void validateMailTemplateExists(Long id) { if (mailTemplateMapper.selectById(id) == null) { throw exception(MAIL_TEMPLATE_NOT_EXISTS); } } @Override public MailTemplateDO getMailTemplate(Long id) {return mailTemplateMapper.selectById(id);} @Override @Cacheable(value = RedisKeyConstants.MAIL_TEMPLATE, key = "#code", unless = "#result == null") public MailTemplateDO getMailTemplateByCodeFromCache(String code) { return mailTemplateMapper.selectByCode(code); } @Override public PageResult getMailTemplatePage(MailTemplatePageReqVO pageReqVO) { return mailTemplateMapper.selectPage(pageReqVO); } @Override public List getMailTemplateList() {return mailTemplateMapper.selectList();} @Override public String formatMailTemplateContent(String content, Map params) { return StrUtil.format(content, params); } @VisibleForTesting public List parseTemplateContentParams(String content) { return ReUtil.findAllGroup1(PATTERN_PARAMS, content); } @Override public long getMailTemplateCountByAccountId(Long accountId) { return mailTemplateMapper.selectCountByAccountId(accountId); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/member/MemberService.java ================================================ package co.yixiang.yshop.module.system.service.member; /** * Member Service 接口 * * @author yshop */ public interface MemberService { /** * 获得会员用户的手机号码 * * @param id 会员用户编号 * @return 手机号码 */ String getMemberUserMobile(Long id); /** * 获得会员用户的邮箱 * * @param id 会员用户编号 * @return 邮箱 */ String getMemberUserEmail(Long id); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/member/MemberServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.member; import cn.hutool.core.util.ClassUtil; import cn.hutool.core.util.ReflectUtil; import cn.hutool.extra.spring.SpringUtil; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; /** * Member Service 实现类 * * @author yshop */ @Service public class MemberServiceImpl implements MemberService { @Value("${yshop.info.base-package}") private String basePackage; private volatile Object memberUserApi; @Override public String getMemberUserMobile(Long id) { Object user = getMemberUser(id); if (user == null) { return null; } return ReflectUtil.invoke(user, "getMobile"); } @Override public String getMemberUserEmail(Long id) { Object user = getMemberUser(id); if (user == null) { return null; } return ReflectUtil.invoke(user, "getEmail"); } private Object getMemberUser(Long id) { if (id == null) { return null; } return ReflectUtil.invoke(getMemberUserApi(), "getUser", id); } private Object getMemberUserApi() { if (memberUserApi == null) { memberUserApi = SpringUtil.getBean(ClassUtil.loadClass(String.format("%s.module.member.api.user.MemberUserApi", basePackage))); } return memberUserApi; } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/notice/NoticeService.java ================================================ package co.yixiang.yshop.module.system.service.notice; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.controller.admin.notice.vo.NoticePageReqVO; import co.yixiang.yshop.module.system.controller.admin.notice.vo.NoticeSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.notice.NoticeDO; /** * 通知公告 Service 接口 */ public interface NoticeService { /** * 创建通知公告 * * @param createReqVO 通知公告 * @return 编号 */ Long createNotice(NoticeSaveReqVO createReqVO); /** * 更新通知公告 * * @param reqVO 通知公告 */ void updateNotice(NoticeSaveReqVO reqVO); /** * 删除通知公告 * * @param id 编号 */ void deleteNotice(Long id); /** * 获得通知公告分页列表 * * @param reqVO 分页条件 * @return 部门分页列表 */ PageResult getNoticePage(NoticePageReqVO reqVO); /** * 获得通知公告 * * @param id 编号 * @return 通知公告 */ NoticeDO getNotice(Long id); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/notice/NoticeServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.notice; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.notice.vo.NoticePageReqVO; import co.yixiang.yshop.module.system.controller.admin.notice.vo.NoticeSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.notice.NoticeDO; import co.yixiang.yshop.module.system.dal.mysql.notice.NoticeMapper; import com.google.common.annotations.VisibleForTesting; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.NOTICE_NOT_FOUND; /** * 通知公告 Service 实现类 * * @author yshop */ @Service public class NoticeServiceImpl implements NoticeService { @Resource private NoticeMapper noticeMapper; @Override public Long createNotice(NoticeSaveReqVO createReqVO) { NoticeDO notice = BeanUtils.toBean(createReqVO, NoticeDO.class); noticeMapper.insert(notice); return notice.getId(); } @Override public void updateNotice(NoticeSaveReqVO updateReqVO) { // 校验是否存在 validateNoticeExists(updateReqVO.getId()); // 更新通知公告 NoticeDO updateObj = BeanUtils.toBean(updateReqVO, NoticeDO.class); noticeMapper.updateById(updateObj); } @Override public void deleteNotice(Long id) { // 校验是否存在 validateNoticeExists(id); // 删除通知公告 noticeMapper.deleteById(id); } @Override public PageResult getNoticePage(NoticePageReqVO reqVO) { return noticeMapper.selectPage(reqVO); } @Override public NoticeDO getNotice(Long id) { return noticeMapper.selectById(id); } @VisibleForTesting public void validateNoticeExists(Long id) { if (id == null) { return; } NoticeDO notice = noticeMapper.selectById(id); if (notice == null) { throw exception(NOTICE_NOT_FOUND); } } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/notify/NotifyMessageService.java ================================================ package co.yixiang.yshop.module.system.service.notify; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.controller.admin.notify.vo.message.NotifyMessageMyPageReqVO; import co.yixiang.yshop.module.system.controller.admin.notify.vo.message.NotifyMessagePageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.notify.NotifyMessageDO; import co.yixiang.yshop.module.system.dal.dataobject.notify.NotifyTemplateDO; import java.util.Collection; import java.util.List; import java.util.Map; /** * 站内信 Service 接口 * * @author xrcoder */ public interface NotifyMessageService { /** * 创建站内信 * * @param userId 用户编号 * @param userType 用户类型 * @param template 模版信息 * @param templateContent 模版内容 * @param templateParams 模版参数 * @return 站内信编号 */ Long createNotifyMessage(Long userId, Integer userType, NotifyTemplateDO template, String templateContent, Map templateParams); /** * 获得站内信分页 * * @param pageReqVO 分页查询 * @return 站内信分页 */ PageResult getNotifyMessagePage(NotifyMessagePageReqVO pageReqVO); /** * 获得【我的】站内信分页 * * @param pageReqVO 分页查询 * @param userId 用户编号 * @param userType 用户类型 * @return 站内信分页 */ PageResult getMyMyNotifyMessagePage(NotifyMessageMyPageReqVO pageReqVO, Long userId, Integer userType); /** * 获得站内信 * * @param id 编号 * @return 站内信 */ NotifyMessageDO getNotifyMessage(Long id); /** * 获得【我的】未读站内信列表 * * @param userId 用户编号 * @param userType 用户类型 * @param size 数量 * @return 站内信列表 */ List getUnreadNotifyMessageList(Long userId, Integer userType, Integer size); /** * 统计用户未读站内信条数 * * @param userId 用户编号 * @param userType 用户类型 * @return 返回未读站内信条数 */ Long getUnreadNotifyMessageCount(Long userId, Integer userType); /** * 标记站内信为已读 * * @param ids 站内信编号集合 * @param userId 用户编号 * @param userType 用户类型 * @return 更新到的条数 */ int updateNotifyMessageRead(Collection ids, Long userId, Integer userType); /** * 标记所有站内信为已读 * * @param userId 用户编号 * @param userType 用户类型 * @return 更新到的条数 */ int updateAllNotifyMessageRead(Long userId, Integer userType); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/notify/NotifyMessageServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.notify; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.controller.admin.notify.vo.message.NotifyMessageMyPageReqVO; import co.yixiang.yshop.module.system.controller.admin.notify.vo.message.NotifyMessagePageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.notify.NotifyMessageDO; import co.yixiang.yshop.module.system.dal.dataobject.notify.NotifyTemplateDO; import co.yixiang.yshop.module.system.dal.mysql.notify.NotifyMessageMapper; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.Collection; import java.util.List; import java.util.Map; /** * 站内信 Service 实现类 * * @author xrcoder */ @Service @Validated public class NotifyMessageServiceImpl implements NotifyMessageService { @Resource private NotifyMessageMapper notifyMessageMapper; @Override public Long createNotifyMessage(Long userId, Integer userType, NotifyTemplateDO template, String templateContent, Map templateParams) { NotifyMessageDO message = new NotifyMessageDO().setUserId(userId).setUserType(userType) .setTemplateId(template.getId()).setTemplateCode(template.getCode()) .setTemplateType(template.getType()).setTemplateNickname(template.getNickname()) .setTemplateContent(templateContent).setTemplateParams(templateParams).setReadStatus(false); notifyMessageMapper.insert(message); return message.getId(); } @Override public PageResult getNotifyMessagePage(NotifyMessagePageReqVO pageReqVO) { return notifyMessageMapper.selectPage(pageReqVO); } @Override public PageResult getMyMyNotifyMessagePage(NotifyMessageMyPageReqVO pageReqVO, Long userId, Integer userType) { return notifyMessageMapper.selectPage(pageReqVO, userId, userType); } @Override public NotifyMessageDO getNotifyMessage(Long id) { return notifyMessageMapper.selectById(id); } @Override public List getUnreadNotifyMessageList(Long userId, Integer userType, Integer size) { return notifyMessageMapper.selectUnreadListByUserIdAndUserType(userId, userType, size); } @Override public Long getUnreadNotifyMessageCount(Long userId, Integer userType) { return notifyMessageMapper.selectUnreadCountByUserIdAndUserType(userId, userType); } @Override public int updateNotifyMessageRead(Collection ids, Long userId, Integer userType) { return notifyMessageMapper.updateListRead(ids, userId, userType); } @Override public int updateAllNotifyMessageRead(Long userId, Integer userType) { return notifyMessageMapper.updateListRead(userId, userType); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/notify/NotifySendService.java ================================================ package co.yixiang.yshop.module.system.service.notify; import java.util.List; import java.util.Map; /** * 站内信发送 Service 接口 * * @author xrcoder */ public interface NotifySendService { /** * 发送单条站内信给管理后台的用户 * * 在 mobile 为空时,使用 userId 加载对应管理员的手机号 * * @param userId 用户编号 * @param templateCode 短信模板编号 * @param templateParams 短信模板参数 * @return 发送日志编号 */ Long sendSingleNotifyToAdmin(Long userId, String templateCode, Map templateParams); /** * 发送单条站内信给用户 APP 的用户 * * 在 mobile 为空时,使用 userId 加载对应会员的手机号 * * @param userId 用户编号 * @param templateCode 站内信模板编号 * @param templateParams 站内信模板参数 * @return 发送日志编号 */ Long sendSingleNotifyToMember(Long userId, String templateCode, Map templateParams); /** * 发送单条站内信给用户 * * @param userId 用户编号 * @param userType 用户类型 * @param templateCode 站内信模板编号 * @param templateParams 站内信模板参数 * @return 发送日志编号 */ Long sendSingleNotify( Long userId, Integer userType, String templateCode, Map templateParams); default void sendBatchNotify(List mobiles, List userIds, Integer userType, String templateCode, Map templateParams) { throw new UnsupportedOperationException("暂时不支持该操作,感兴趣可以实现该功能哟!"); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/notify/NotifySendServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.notify; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.module.system.dal.dataobject.notify.NotifyTemplateDO; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.Map; import java.util.Objects; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; /** * 站内信发送 Service 实现类 * * @author xrcoder */ @Service @Validated @Slf4j public class NotifySendServiceImpl implements NotifySendService { @Resource private NotifyTemplateService notifyTemplateService; @Resource private NotifyMessageService notifyMessageService; @Override public Long sendSingleNotifyToAdmin(Long userId, String templateCode, Map templateParams) { return sendSingleNotify(userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams); } @Override public Long sendSingleNotifyToMember(Long userId, String templateCode, Map templateParams) { return sendSingleNotify(userId, UserTypeEnum.MEMBER.getValue(), templateCode, templateParams); } @Override public Long sendSingleNotify(Long userId, Integer userType, String templateCode, Map templateParams) { // 校验模版 NotifyTemplateDO template = validateNotifyTemplate(templateCode); if (Objects.equals(template.getStatus(), CommonStatusEnum.DISABLE.getStatus())) { log.info("[sendSingleNotify][模版({})已经关闭,无法给用户({}/{})发送]", templateCode, userId, userType); return null; } // 校验参数 validateTemplateParams(template, templateParams); // 发送站内信 String content = notifyTemplateService.formatNotifyTemplateContent(template.getContent(), templateParams); return notifyMessageService.createNotifyMessage(userId, userType, template, content, templateParams); } @VisibleForTesting public NotifyTemplateDO validateNotifyTemplate(String templateCode) { // 获得站内信模板。考虑到效率,从缓存中获取 NotifyTemplateDO template = notifyTemplateService.getNotifyTemplateByCodeFromCache(templateCode); // 站内信模板不存在 if (template == null) { throw exception(NOTICE_NOT_FOUND); } return template; } /** * 校验站内信模版参数是否确实 * * @param template 邮箱模板 * @param templateParams 参数列表 */ @VisibleForTesting public void validateTemplateParams(NotifyTemplateDO template, Map templateParams) { template.getParams().forEach(key -> { Object value = templateParams.get(key); if (value == null) { throw exception(NOTIFY_SEND_TEMPLATE_PARAM_MISS, key); } }); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/notify/NotifyTemplateService.java ================================================ package co.yixiang.yshop.module.system.service.notify; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.controller.admin.notify.vo.template.NotifyTemplatePageReqVO; import co.yixiang.yshop.module.system.controller.admin.notify.vo.template.NotifyTemplateSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.notify.NotifyTemplateDO; import jakarta.validation.Valid; import java.util.Map; /** * 站内信模版 Service 接口 * * @author xrcoder */ public interface NotifyTemplateService { /** * 创建站内信模版 * * @param createReqVO 创建信息 * @return 编号 */ Long createNotifyTemplate(@Valid NotifyTemplateSaveReqVO createReqVO); /** * 更新站内信模版 * * @param updateReqVO 更新信息 */ void updateNotifyTemplate(@Valid NotifyTemplateSaveReqVO updateReqVO); /** * 删除站内信模版 * * @param id 编号 */ void deleteNotifyTemplate(Long id); /** * 获得站内信模版 * * @param id 编号 * @return 站内信模版 */ NotifyTemplateDO getNotifyTemplate(Long id); /** * 获得站内信模板,从缓存中 * * @param code 模板编码 * @return 站内信模板 */ NotifyTemplateDO getNotifyTemplateByCodeFromCache(String code); /** * 获得站内信模版分页 * * @param pageReqVO 分页查询 * @return 站内信模版分页 */ PageResult getNotifyTemplatePage(NotifyTemplatePageReqVO pageReqVO); /** * 格式化站内信内容 * * @param content 站内信模板的内容 * @param params 站内信内容的参数 * @return 格式化后的内容 */ String formatNotifyTemplateContent(String content, Map params); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/notify/NotifyTemplateServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.notify; import cn.hutool.core.util.ReUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.notify.vo.template.NotifyTemplatePageReqVO; import co.yixiang.yshop.module.system.controller.admin.notify.vo.template.NotifyTemplateSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.notify.NotifyTemplateDO; import co.yixiang.yshop.module.system.dal.mysql.notify.NotifyTemplateMapper; import co.yixiang.yshop.module.system.dal.redis.RedisKeyConstants; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.NOTIFY_TEMPLATE_CODE_DUPLICATE; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.NOTIFY_TEMPLATE_NOT_EXISTS; /** * 站内信模版 Service 实现类 * * @author xrcoder */ @Service @Validated @Slf4j public class NotifyTemplateServiceImpl implements NotifyTemplateService { /** * 正则表达式,匹配 {} 中的变量 */ private static final Pattern PATTERN_PARAMS = Pattern.compile("\\{(.*?)}"); @Resource private NotifyTemplateMapper notifyTemplateMapper; @Override public Long createNotifyTemplate(NotifyTemplateSaveReqVO createReqVO) { // 校验站内信编码是否重复 validateNotifyTemplateCodeDuplicate(null, createReqVO.getCode()); // 插入 NotifyTemplateDO notifyTemplate = BeanUtils.toBean(createReqVO, NotifyTemplateDO.class); notifyTemplate.setParams(parseTemplateContentParams(notifyTemplate.getContent())); notifyTemplateMapper.insert(notifyTemplate); return notifyTemplate.getId(); } @Override @CacheEvict(cacheNames = RedisKeyConstants.NOTIFY_TEMPLATE, allEntries = true) // allEntries 清空所有缓存,因为可能修改到 code 字段,不好清理 public void updateNotifyTemplate(NotifyTemplateSaveReqVO updateReqVO) { // 校验存在 validateNotifyTemplateExists(updateReqVO.getId()); // 校验站内信编码是否重复 validateNotifyTemplateCodeDuplicate(updateReqVO.getId(), updateReqVO.getCode()); // 更新 NotifyTemplateDO updateObj = BeanUtils.toBean(updateReqVO, NotifyTemplateDO.class); updateObj.setParams(parseTemplateContentParams(updateObj.getContent())); notifyTemplateMapper.updateById(updateObj); } @VisibleForTesting public List parseTemplateContentParams(String content) { return ReUtil.findAllGroup1(PATTERN_PARAMS, content); } @Override @CacheEvict(cacheNames = RedisKeyConstants.NOTIFY_TEMPLATE, allEntries = true) // allEntries 清空所有缓存,因为 id 不是直接的缓存 code,不好清理 public void deleteNotifyTemplate(Long id) { // 校验存在 validateNotifyTemplateExists(id); // 删除 notifyTemplateMapper.deleteById(id); } private void validateNotifyTemplateExists(Long id) { if (notifyTemplateMapper.selectById(id) == null) { throw exception(NOTIFY_TEMPLATE_NOT_EXISTS); } } @Override public NotifyTemplateDO getNotifyTemplate(Long id) { return notifyTemplateMapper.selectById(id); } @Override @Cacheable(cacheNames = RedisKeyConstants.NOTIFY_TEMPLATE, key = "#code", unless = "#result == null") public NotifyTemplateDO getNotifyTemplateByCodeFromCache(String code) { return notifyTemplateMapper.selectByCode(code); } @Override public PageResult getNotifyTemplatePage(NotifyTemplatePageReqVO pageReqVO) { return notifyTemplateMapper.selectPage(pageReqVO); } @VisibleForTesting void validateNotifyTemplateCodeDuplicate(Long id, String code) { NotifyTemplateDO template = notifyTemplateMapper.selectByCode(code); if (template == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的字典类型 if (id == null) { throw exception(NOTIFY_TEMPLATE_CODE_DUPLICATE, code); } if (!template.getId().equals(id)) { throw exception(NOTIFY_TEMPLATE_CODE_DUPLICATE, code); } } /** * 格式化站内信内容 * * @param content 站内信模板的内容 * @param params 站内信内容的参数 * @return 格式化后的内容 */ @Override public String formatNotifyTemplateContent(String content, Map params) { return StrUtil.format(content, params); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/oauth2/OAuth2ApproveService.java ================================================ package co.yixiang.yshop.module.system.service.oauth2; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; import java.util.Collection; import java.util.List; import java.util.Map; /** * OAuth2 批准 Service 接口 * * 从功能上,和 Spring Security OAuth 的 ApprovalStoreUserApprovalHandler 的功能,记录用户针对指定客户端的授权,减少手动确定。 * * @author yshop */ public interface OAuth2ApproveService { /** * 获得指定用户,针对指定客户端的指定授权,是否通过 * * 参考 ApprovalStoreUserApprovalHandler 的 checkForPreApproval 方法 * * @param userId 用户编号 * @param userType 用户类型 * @param clientId 客户端编号 * @param requestedScopes 授权范围 * @return 是否授权通过 */ boolean checkForPreApproval(Long userId, Integer userType, String clientId, Collection requestedScopes); /** * 在用户发起批准时,基于 scopes 的选项,计算最终是否通过 * * @param userId 用户编号 * @param userType 用户类型 * @param clientId 客户端编号 * @param requestedScopes 授权范围 * @return 是否授权通过 */ boolean updateAfterApproval(Long userId, Integer userType, String clientId, Map requestedScopes); /** * 获得用户的批准列表,排除已过期的 * * @param userId 用户编号 * @param userType 用户类型 * @param clientId 客户端编号 * @return 是否授权通过 */ List getApproveList(Long userId, Integer userType, String clientId); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/oauth2/OAuth2ApproveServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.oauth2; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import co.yixiang.yshop.framework.common.util.date.DateUtils; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2ClientDO; import co.yixiang.yshop.module.system.dal.mysql.oauth2.OAuth2ApproveMapper; import com.google.common.annotations.VisibleForTesting; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.time.LocalDateTime; import java.util.*; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertSet; /** * OAuth2 批准 Service 实现类 * * @author yshop */ @Service @Validated public class OAuth2ApproveServiceImpl implements OAuth2ApproveService { /** * 批准的过期时间,默认 30 天 */ private static final Integer TIMEOUT = 30 * 24 * 60 * 60; // 单位:秒 @Resource private OAuth2ClientService oauth2ClientService; @Resource private OAuth2ApproveMapper oauth2ApproveMapper; @Override @Transactional public boolean checkForPreApproval(Long userId, Integer userType, String clientId, Collection requestedScopes) { // 第一步,基于 Client 的自动授权计算,如果 scopes 都在自动授权中,则返回 true 通过 OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); Assert.notNull(clientDO, "客户端不能为空"); // 防御性编程 if (CollUtil.containsAll(clientDO.getAutoApproveScopes(), requestedScopes)) { // gh-877 - if all scopes are auto approved, approvals still need to be added to the approval store. LocalDateTime expireTime = LocalDateTime.now().plusSeconds(TIMEOUT); for (String scope : requestedScopes) { saveApprove(userId, userType, clientId, scope, true, expireTime); } return true; } // 第二步,算上用户已经批准的授权。如果 scopes 都包含,则返回 true List approveDOs = getApproveList(userId, userType, clientId); Set scopes = convertSet(approveDOs, OAuth2ApproveDO::getScope, OAuth2ApproveDO::getApproved); // 只保留未过期的 + 同意的 return CollUtil.containsAll(scopes, requestedScopes); } @Override @Transactional public boolean updateAfterApproval(Long userId, Integer userType, String clientId, Map requestedScopes) { // 如果 requestedScopes 为空,说明没有要求,则返回 true 通过 if (CollUtil.isEmpty(requestedScopes)) { return true; } // 更新批准的信息 boolean success = false; // 需要至少有一个同意 LocalDateTime expireTime = LocalDateTime.now().plusSeconds(TIMEOUT); for (Map.Entry entry : requestedScopes.entrySet()) { if (entry.getValue()) { success = true; } saveApprove(userId, userType, clientId, entry.getKey(), entry.getValue(), expireTime); } return success; } @Override public List getApproveList(Long userId, Integer userType, String clientId) { List approveDOs = oauth2ApproveMapper.selectListByUserIdAndUserTypeAndClientId( userId, userType, clientId); approveDOs.removeIf(o -> DateUtils.isExpired(o.getExpiresTime())); return approveDOs; } @VisibleForTesting void saveApprove(Long userId, Integer userType, String clientId, String scope, Boolean approved, LocalDateTime expireTime) { // 先更新 OAuth2ApproveDO approveDO = new OAuth2ApproveDO().setUserId(userId).setUserType(userType) .setClientId(clientId).setScope(scope).setApproved(approved).setExpiresTime(expireTime); if (oauth2ApproveMapper.update(approveDO) == 1) { return; } // 失败,则说明不存在,进行更新 oauth2ApproveMapper.insert(approveDO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/oauth2/OAuth2ClientService.java ================================================ package co.yixiang.yshop.module.system.service.oauth2; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.client.OAuth2ClientSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2ClientDO; import jakarta.validation.Valid; import java.util.Collection; /** * OAuth2.0 Client Service 接口 * * 从功能上,和 JdbcClientDetailsService 的功能,提供客户端的操作 * * @author yshop */ public interface OAuth2ClientService { /** * 创建 OAuth2 客户端 * * @param createReqVO 创建信息 * @return 编号 */ Long createOAuth2Client(@Valid OAuth2ClientSaveReqVO createReqVO); /** * 更新 OAuth2 客户端 * * @param updateReqVO 更新信息 */ void updateOAuth2Client(@Valid OAuth2ClientSaveReqVO updateReqVO); /** * 删除 OAuth2 客户端 * * @param id 编号 */ void deleteOAuth2Client(Long id); /** * 获得 OAuth2 客户端 * * @param id 编号 * @return OAuth2 客户端 */ OAuth2ClientDO getOAuth2Client(Long id); /** * 获得 OAuth2 客户端,从缓存中 * * @param clientId 客户端编号 * @return OAuth2 客户端 */ OAuth2ClientDO getOAuth2ClientFromCache(String clientId); /** * 获得 OAuth2 客户端分页 * * @param pageReqVO 分页查询 * @return OAuth2 客户端分页 */ PageResult getOAuth2ClientPage(OAuth2ClientPageReqVO pageReqVO); /** * 从缓存中,校验客户端是否合法 * * @return 客户端 */ default OAuth2ClientDO validOAuthClientFromCache(String clientId) { return validOAuthClientFromCache(clientId, null, null, null, null); } /** * 从缓存中,校验客户端是否合法 * * 非空时,进行校验 * * @param clientId 客户端编号 * @param clientSecret 客户端密钥 * @param authorizedGrantType 授权方式 * @param scopes 授权范围 * @param redirectUri 重定向地址 * @return 客户端 */ OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret, String authorizedGrantType, Collection scopes, String redirectUri); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/oauth2/OAuth2ClientServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.oauth2; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.common.util.string.StrUtils; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.client.OAuth2ClientSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2ClientDO; import co.yixiang.yshop.module.system.dal.mysql.oauth2.OAuth2ClientMapper; import co.yixiang.yshop.module.system.dal.redis.RedisKeyConstants; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.Collection; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; /** * OAuth2.0 Client Service 实现类 * * @author yshop */ @Service @Validated @Slf4j public class OAuth2ClientServiceImpl implements OAuth2ClientService { @Resource private OAuth2ClientMapper oauth2ClientMapper; @Override public Long createOAuth2Client(OAuth2ClientSaveReqVO createReqVO) { validateClientIdExists(null, createReqVO.getClientId()); // 插入 OAuth2ClientDO client = BeanUtils.toBean(createReqVO, OAuth2ClientDO.class); oauth2ClientMapper.insert(client); return client.getId(); } @Override @CacheEvict(cacheNames = RedisKeyConstants.OAUTH_CLIENT, allEntries = true) // allEntries 清空所有缓存,因为可能修改到 clientId 字段,不好清理 public void updateOAuth2Client(OAuth2ClientSaveReqVO updateReqVO) { // 校验存在 validateOAuth2ClientExists(updateReqVO.getId()); // 校验 Client 未被占用 validateClientIdExists(updateReqVO.getId(), updateReqVO.getClientId()); // 更新 OAuth2ClientDO updateObj = BeanUtils.toBean(updateReqVO, OAuth2ClientDO.class); oauth2ClientMapper.updateById(updateObj); } @Override @CacheEvict(cacheNames = RedisKeyConstants.OAUTH_CLIENT, allEntries = true) // allEntries 清空所有缓存,因为 id 不是直接的缓存 key,不好清理 public void deleteOAuth2Client(Long id) { // 校验存在 validateOAuth2ClientExists(id); // 删除 oauth2ClientMapper.deleteById(id); } private void validateOAuth2ClientExists(Long id) { if (oauth2ClientMapper.selectById(id) == null) { throw exception(OAUTH2_CLIENT_NOT_EXISTS); } } @VisibleForTesting void validateClientIdExists(Long id, String clientId) { OAuth2ClientDO client = oauth2ClientMapper.selectByClientId(clientId); if (client == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的客户端 if (id == null) { throw exception(OAUTH2_CLIENT_EXISTS); } if (!client.getId().equals(id)) { throw exception(OAUTH2_CLIENT_EXISTS); } } @Override public OAuth2ClientDO getOAuth2Client(Long id) { return oauth2ClientMapper.selectById(id); } @Override @Cacheable(cacheNames = RedisKeyConstants.OAUTH_CLIENT, key = "#clientId", unless = "#result == null") public OAuth2ClientDO getOAuth2ClientFromCache(String clientId) { return oauth2ClientMapper.selectByClientId(clientId); } @Override public PageResult getOAuth2ClientPage(OAuth2ClientPageReqVO pageReqVO) { return oauth2ClientMapper.selectPage(pageReqVO); } @Override public OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret, String authorizedGrantType, Collection scopes, String redirectUri) { // 校验客户端存在、且开启 OAuth2ClientDO client = getSelf().getOAuth2ClientFromCache(clientId); if (client == null) { throw exception(OAUTH2_CLIENT_NOT_EXISTS); } if (CommonStatusEnum.isDisable(client.getStatus())) { throw exception(OAUTH2_CLIENT_DISABLE); } // 校验客户端密钥 if (StrUtil.isNotEmpty(clientSecret) && ObjectUtil.notEqual(client.getSecret(), clientSecret)) { throw exception(OAUTH2_CLIENT_CLIENT_SECRET_ERROR); } // 校验授权方式 if (StrUtil.isNotEmpty(authorizedGrantType) && !CollUtil.contains(client.getAuthorizedGrantTypes(), authorizedGrantType)) { throw exception(OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS); } // 校验授权范围 if (CollUtil.isNotEmpty(scopes) && !CollUtil.containsAll(client.getScopes(), scopes)) { throw exception(OAUTH2_CLIENT_SCOPE_OVER); } // 校验回调地址 if (StrUtil.isNotEmpty(redirectUri) && !StrUtils.startWithAny(redirectUri, client.getRedirectUris())) { throw exception(OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH, redirectUri); } return client; } /** * 获得自身的代理对象,解决 AOP 生效问题 * * @return 自己 */ private OAuth2ClientServiceImpl getSelf() { return SpringUtil.getBean(getClass()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/oauth2/OAuth2CodeService.java ================================================ package co.yixiang.yshop.module.system.service.oauth2; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2CodeDO; import java.util.List; /** * OAuth2.0 授权码 Service 接口 * * 从功能上,和 Spring Security OAuth 的 JdbcAuthorizationCodeServices 的功能,提供授权码的操作 * * @author yshop */ public interface OAuth2CodeService { /** * 创建授权码 * * 参考 JdbcAuthorizationCodeServices 的 createAuthorizationCode 方法 * * @param userId 用户编号 * @param userType 用户类型 * @param clientId 客户端编号 * @param scopes 授权范围 * @param redirectUri 重定向 URI * @param state 状态 * @return 授权码的信息 */ OAuth2CodeDO createAuthorizationCode(Long userId, Integer userType, String clientId, List scopes, String redirectUri, String state); /** * 使用授权码 * * @param code 授权码 */ OAuth2CodeDO consumeAuthorizationCode(String code); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/oauth2/OAuth2CodeServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.oauth2; import cn.hutool.core.util.IdUtil; import co.yixiang.yshop.framework.common.util.date.DateUtils; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2CodeDO; import co.yixiang.yshop.module.system.dal.mysql.oauth2.OAuth2CodeMapper; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.time.LocalDateTime; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.OAUTH2_CODE_EXPIRE; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.OAUTH2_CODE_NOT_EXISTS; /** * OAuth2.0 授权码 Service 实现类 * * @author yshop */ @Service @Validated public class OAuth2CodeServiceImpl implements OAuth2CodeService { /** * 授权码的过期时间,默认 5 分钟 */ private static final Integer TIMEOUT = 5 * 60; @Resource private OAuth2CodeMapper oauth2CodeMapper; @Override public OAuth2CodeDO createAuthorizationCode(Long userId, Integer userType, String clientId, List scopes, String redirectUri, String state) { OAuth2CodeDO codeDO = new OAuth2CodeDO().setCode(generateCode()) .setUserId(userId).setUserType(userType) .setClientId(clientId).setScopes(scopes) .setExpiresTime(LocalDateTime.now().plusSeconds(TIMEOUT)) .setRedirectUri(redirectUri).setState(state); oauth2CodeMapper.insert(codeDO); return codeDO; } @Override public OAuth2CodeDO consumeAuthorizationCode(String code) { OAuth2CodeDO codeDO = oauth2CodeMapper.selectByCode(code); if (codeDO == null) { throw exception(OAUTH2_CODE_NOT_EXISTS); } if (DateUtils.isExpired(codeDO.getExpiresTime())) { throw exception(OAUTH2_CODE_EXPIRE); } oauth2CodeMapper.deleteById(codeDO.getId()); return codeDO; } private static String generateCode() { return IdUtil.fastSimpleUUID(); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/oauth2/OAuth2GrantService.java ================================================ package co.yixiang.yshop.module.system.service.oauth2; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import java.util.List; /** * OAuth2 授予 Service 接口 * * 从功能上,和 Spring Security OAuth 的 TokenGranter 的功能,提供访问令牌、刷新令牌的操作 * * 将自身的 AdminUser 用户,授权给第三方应用,采用 OAuth2.0 的协议。 * * 问题:为什么自身也作为一个第三方应用,也走这套流程呢? * 回复:当然可以这么做,采用 password 模式。考虑到大多数开发者使用不到这个特性,OAuth2.0 毕竟有一定学习成本,所以暂时没有采取这种方式。 * * @author yshop */ public interface OAuth2GrantService { /** * 简化模式 * * 对应 Spring Security OAuth2 的 ImplicitTokenGranter 功能 * * @param userId 用户编号 * @param userType 用户类型 * @param clientId 客户端编号 * @param scopes 授权范围 * @return 访问令牌 */ OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, String clientId, List scopes); /** * 授权码模式,第一阶段,获得 code 授权码 * * 对应 Spring Security OAuth2 的 AuthorizationEndpoint 的 generateCode 方法 * * @param userId 用户编号 * @param userType 用户类型 * @param clientId 客户端编号 * @param scopes 授权范围 * @param redirectUri 重定向 URI * @param state 状态 * @return 授权码 */ String grantAuthorizationCodeForCode(Long userId, Integer userType, String clientId, List scopes, String redirectUri, String state); /** * 授权码模式,第二阶段,获得 accessToken 访问令牌 * * 对应 Spring Security OAuth2 的 AuthorizationCodeTokenGranter 功能 * * @param clientId 客户端编号 * @param code 授权码 * @param redirectUri 重定向 URI * @param state 状态 * @return 访问令牌 */ OAuth2AccessTokenDO grantAuthorizationCodeForAccessToken(String clientId, String code, String redirectUri, String state); /** * 密码模式 * * 对应 Spring Security OAuth2 的 ResourceOwnerPasswordTokenGranter 功能 * * @param username 账号 * @param password 密码 * @param clientId 客户端编号 * @param scopes 授权范围 * @return 访问令牌 */ OAuth2AccessTokenDO grantPassword(String username, String password, String clientId, List scopes); /** * 刷新模式 * * 对应 Spring Security OAuth2 的 ResourceOwnerPasswordTokenGranter 功能 * * @param refreshToken 刷新令牌 * @param clientId 客户端编号 * @return 访问令牌 */ OAuth2AccessTokenDO grantRefreshToken(String refreshToken, String clientId); /** * 客户端模式 * * 对应 Spring Security OAuth2 的 ClientCredentialsTokenGranter 功能 * * @param clientId 客户端编号 * @param scopes 授权范围 * @return 访问令牌 */ OAuth2AccessTokenDO grantClientCredentials(String clientId, List scopes); /** * 移除访问令牌 * * 对应 Spring Security OAuth2 的 ConsumerTokenServices 的 revokeToken 方法 * * @param accessToken 访问令牌 * @param clientId 客户端编号 * @return 是否移除到 */ boolean revokeToken(String clientId, String accessToken); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/oauth2/OAuth2GrantServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.oauth2; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2CodeDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.module.system.enums.ErrorCodeConstants; import co.yixiang.yshop.module.system.service.auth.AdminAuthService; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; /** * OAuth2 授予 Service 实现类 * * @author yshop */ @Service public class OAuth2GrantServiceImpl implements OAuth2GrantService { @Resource private OAuth2TokenService oauth2TokenService; @Resource private OAuth2CodeService oauth2CodeService; @Resource private AdminAuthService adminAuthService; @Override public OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, String clientId, List scopes) { return oauth2TokenService.createAccessToken(userId, userType, clientId, scopes); } @Override public String grantAuthorizationCodeForCode(Long userId, Integer userType, String clientId, List scopes, String redirectUri, String state) { return oauth2CodeService.createAuthorizationCode(userId, userType, clientId, scopes, redirectUri, state).getCode(); } @Override public OAuth2AccessTokenDO grantAuthorizationCodeForAccessToken(String clientId, String code, String redirectUri, String state) { OAuth2CodeDO codeDO = oauth2CodeService.consumeAuthorizationCode(code); Assert.notNull(codeDO, "授权码不能为空"); // 防御性编程 // 校验 clientId 是否匹配 if (!StrUtil.equals(clientId, codeDO.getClientId())) { throw exception(ErrorCodeConstants.OAUTH2_GRANT_CLIENT_ID_MISMATCH); } // 校验 redirectUri 是否匹配 if (!StrUtil.equals(redirectUri, codeDO.getRedirectUri())) { throw exception(ErrorCodeConstants.OAUTH2_GRANT_REDIRECT_URI_MISMATCH); } // 校验 state 是否匹配 state = StrUtil.nullToDefault(state, ""); // 数据库 state 为 null 时,会设置为 "" 空串 if (!StrUtil.equals(state, codeDO.getState())) { throw exception(ErrorCodeConstants.OAUTH2_GRANT_STATE_MISMATCH); } // 创建访问令牌 return oauth2TokenService.createAccessToken(codeDO.getUserId(), codeDO.getUserType(), codeDO.getClientId(), codeDO.getScopes()); } @Override public OAuth2AccessTokenDO grantPassword(String username, String password, String clientId, List scopes) { // 使用账号 + 密码进行登录 AdminUserDO user = adminAuthService.authenticate(username, password); Assert.notNull(user, "用户不能为空!"); // 防御性编程 // 创建访问令牌 return oauth2TokenService.createAccessToken(user.getId(), UserTypeEnum.ADMIN.getValue(), clientId, scopes); } @Override public OAuth2AccessTokenDO grantRefreshToken(String refreshToken, String clientId) { return oauth2TokenService.refreshAccessToken(refreshToken, clientId); } @Override public OAuth2AccessTokenDO grantClientCredentials(String clientId, List scopes) { // TODO yshop:项目中使用 OAuth2 解决的是三方应用的授权,内部的 SSO 等问题,所以暂时不考虑 client_credentials 这个场景 throw new UnsupportedOperationException("暂时不支持 client_credentials 授权模式"); } @Override public boolean revokeToken(String clientId, String accessToken) { // 先查询,保证 clientId 时匹配的 OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.getAccessToken(accessToken); if (accessTokenDO == null || ObjectUtil.notEqual(clientId, accessTokenDO.getClientId())) { return false; } // 再删除 return oauth2TokenService.removeAccessToken(accessToken) != null; } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/oauth2/OAuth2TokenService.java ================================================ package co.yixiang.yshop.module.system.service.oauth2; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import java.util.List; /** * OAuth2.0 Token Service 接口 * * 从功能上,和 Spring Security OAuth 的 DefaultTokenServices + JdbcTokenStore 的功能,提供访问令牌、刷新令牌的操作 * * @author yshop */ public interface OAuth2TokenService { /** * 创建访问令牌 * 注意:该流程中,会包含创建刷新令牌的创建 * * 参考 DefaultTokenServices 的 createAccessToken 方法 * * @param userId 用户编号 * @param userType 用户类型 * @param clientId 客户端编号 * @param scopes 授权范围 * @return 访问令牌的信息 */ OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List scopes); /** * 刷新访问令牌 * * 参考 DefaultTokenServices 的 refreshAccessToken 方法 * * @param refreshToken 刷新令牌 * @param clientId 客户端编号 * @return 访问令牌的信息 */ OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId); /** * 获得访问令牌 * * 参考 DefaultTokenServices 的 getAccessToken 方法 * * @param accessToken 访问令牌 * @return 访问令牌的信息 */ OAuth2AccessTokenDO getAccessToken(String accessToken); /** * 校验访问令牌 * * @param accessToken 访问令牌 * @return 访问令牌的信息 */ OAuth2AccessTokenDO checkAccessToken(String accessToken); /** * 移除访问令牌 * 注意:该流程中,会移除相关的刷新令牌 * * 参考 DefaultTokenServices 的 revokeToken 方法 * * @param accessToken 刷新令牌 * @return 访问令牌的信息 */ OAuth2AccessTokenDO removeAccessToken(String accessToken); /** * 获得访问令牌分页 * * @param reqVO 请求 * @return 访问令牌分页 */ PageResult getAccessTokenPage(OAuth2AccessTokenPageReqVO reqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/oauth2/OAuth2TokenServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.oauth2; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.exception.enums.GlobalErrorCodeConstants; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.date.DateUtils; import co.yixiang.yshop.framework.security.core.LoginUser; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; import co.yixiang.yshop.module.store.dal.mysql.storeshop.StoreShopMapper; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2ClientDO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2RefreshTokenDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.module.system.dal.mysql.oauth2.OAuth2AccessTokenMapper; import co.yixiang.yshop.module.system.dal.mysql.oauth2.OAuth2RefreshTokenMapper; import co.yixiang.yshop.module.system.dal.redis.oauth2.OAuth2AccessTokenRedisDAO; import co.yixiang.yshop.module.system.service.user.AdminUserService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import jakarta.annotation.Resource; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.Collections; import java.util.List; import java.util.Map; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception0; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertSet; /** * OAuth2.0 Token Service 实现类 * * @author yshop */ @Service public class OAuth2TokenServiceImpl implements OAuth2TokenService { @Resource private OAuth2AccessTokenMapper oauth2AccessTokenMapper; @Resource private OAuth2RefreshTokenMapper oauth2RefreshTokenMapper; @Resource private OAuth2AccessTokenRedisDAO oauth2AccessTokenRedisDAO; @Resource private OAuth2ClientService oauth2ClientService; @Resource @Lazy // 懒加载,避免循环依赖 private AdminUserService adminUserService; @Resource private StoreShopMapper storeShopMapper; @Override @Transactional public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List scopes) { OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); // 创建刷新令牌 OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO, scopes); // 创建访问令牌 return createOAuth2AccessToken(refreshTokenDO, clientDO); } @Override public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) { // 查询访问令牌 OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken); if (refreshTokenDO == null) { throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "无效的刷新令牌"); } // 校验 Client 匹配 OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); if (ObjectUtil.notEqual(clientId, refreshTokenDO.getClientId())) { throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "刷新令牌的客户端编号不正确"); } // 移除相关的访问令牌 List accessTokenDOs = oauth2AccessTokenMapper.selectListByRefreshToken(refreshToken); if (CollUtil.isNotEmpty(accessTokenDOs)) { oauth2AccessTokenMapper.deleteBatchIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId)); oauth2AccessTokenRedisDAO.deleteList(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getAccessToken)); } // 已过期的情况下,删除刷新令牌 if (DateUtils.isExpired(refreshTokenDO.getExpiresTime())) { oauth2RefreshTokenMapper.deleteById(refreshTokenDO.getId()); throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "刷新令牌已过期"); } // 创建访问令牌 return createOAuth2AccessToken(refreshTokenDO, clientDO); } @Override public OAuth2AccessTokenDO getAccessToken(String accessToken) { // 优先从 Redis 中获取 OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenRedisDAO.get(accessToken); if (accessTokenDO != null) { return accessTokenDO; } // 获取不到,从 MySQL 中获取 accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken); // 如果在 MySQL 存在,则往 Redis 中写入 if (accessTokenDO != null && !DateUtils.isExpired(accessTokenDO.getExpiresTime())) { oauth2AccessTokenRedisDAO.set(accessTokenDO); } return accessTokenDO; } @Override public OAuth2AccessTokenDO checkAccessToken(String accessToken) { OAuth2AccessTokenDO accessTokenDO = getAccessToken(accessToken); if (accessTokenDO == null) { throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "访问令牌不存在"); } if (DateUtils.isExpired(accessTokenDO.getExpiresTime())) { throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "访问令牌已过期"); } return accessTokenDO; } @Override public OAuth2AccessTokenDO removeAccessToken(String accessToken) { // 删除访问令牌 OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken); if (accessTokenDO == null) { return null; } oauth2AccessTokenMapper.deleteById(accessTokenDO.getId()); oauth2AccessTokenRedisDAO.delete(accessToken); // 删除刷新令牌 oauth2RefreshTokenMapper.deleteByRefreshToken(accessTokenDO.getRefreshToken()); return accessTokenDO; } @Override public PageResult getAccessTokenPage(OAuth2AccessTokenPageReqVO reqVO) { return oauth2AccessTokenMapper.selectPage(reqVO); } private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) { OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken()) .setShopId(refreshTokenDO.getShopId()) .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType()) .setUserInfo(buildUserInfo(refreshTokenDO.getUserId(), refreshTokenDO.getUserType())) .setClientId(clientDO.getClientId()).setScopes(refreshTokenDO.getScopes()) .setRefreshToken(refreshTokenDO.getRefreshToken()) .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getAccessTokenValiditySeconds())); accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号,避免缓存到 Redis 的时候,无对应的租户编号 oauth2AccessTokenMapper.insert(accessTokenDO); // 记录到 Redis 中 oauth2AccessTokenRedisDAO.set(accessTokenDO); return accessTokenDO; } private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO, List scopes) { Long shopId = getShopId(userId); OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken()) .setShopId(shopId) .setUserId(userId).setUserType(userType) .setClientId(clientDO.getClientId()).setScopes(scopes) .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getRefreshTokenValiditySeconds())); oauth2RefreshTokenMapper.insert(refreshToken); return refreshToken; } private Long getShopId(Long userId) { StoreShopDO storeShopDO = storeShopMapper.selectOne(new LambdaQueryWrapper() .apply(userId > 0, "FIND_IN_SET ('" + userId + "',admin_id)")); if (storeShopDO == null) { return 0L; } return storeShopDO.getId(); } /** * 加载用户信息,方便 {@link co.yixiang.yshop.framework.security.core.LoginUser} 获取到昵称、部门等信息 * * @param userId 用户编号 * @param userType 用户类型 * @return 用户信息 */ private Map buildUserInfo(Long userId, Integer userType) { if (userType.equals(UserTypeEnum.ADMIN.getValue())) { AdminUserDO user = adminUserService.getUser(userId); return MapUtil.builder(LoginUser.INFO_KEY_NICKNAME, user.getNickname()) .put(LoginUser.INFO_KEY_DEPT_ID, StrUtil.toStringOrNull(user.getDeptId())).build(); } else if (userType.equals(UserTypeEnum.MEMBER.getValue())) { // 注意:目前 Member 暂时不读取,可以按需实现 return Collections.emptyMap(); } return null; } private static String generateAccessToken() { return IdUtil.fastSimpleUUID(); } private static String generateRefreshToken() { return IdUtil.fastSimpleUUID(); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/permission/MenuService.java ================================================ package co.yixiang.yshop.module.system.service.permission; import co.yixiang.yshop.module.system.controller.admin.permission.vo.menu.MenuSaveVO; import co.yixiang.yshop.module.system.controller.admin.permission.vo.menu.MenuListReqVO; import co.yixiang.yshop.module.system.dal.dataobject.permission.MenuDO; import java.util.Collection; import java.util.List; /** * 菜单 Service 接口 * * @author yshop */ public interface MenuService { /** * 创建菜单 * * @param createReqVO 菜单信息 * @return 创建出来的菜单编号 */ Long createMenu(MenuSaveVO createReqVO); /** * 更新菜单 * * @param updateReqVO 菜单信息 */ void updateMenu(MenuSaveVO updateReqVO); /** * 删除菜单 * * @param id 菜单编号 */ void deleteMenu(Long id); /** * 获得所有菜单列表 * * @return 菜单列表 */ List getMenuList(); /** * 基于租户,筛选菜单列表 * 注意,如果是系统租户,返回的还是全菜单 * * @param reqVO 筛选条件请求 VO * @return 菜单列表 */ List getMenuListByTenant(MenuListReqVO reqVO); /** * 筛选菜单列表 * * @param reqVO 筛选条件请求 VO * @return 菜单列表 */ List getMenuList(MenuListReqVO reqVO); /** * 获得权限对应的菜单编号数组 * * @param permission 权限标识 * @return 数组 */ List getMenuIdListByPermissionFromCache(String permission); /** * 获得菜单 * * @param id 菜单编号 * @return 菜单 */ MenuDO getMenu(Long id); /** * 获得菜单数组 * * @param ids 菜单编号数组 * @return 菜单数组 */ List getMenuList(Collection ids); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/permission/MenuServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.permission; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.permission.vo.menu.MenuListReqVO; import co.yixiang.yshop.module.system.controller.admin.permission.vo.menu.MenuSaveVO; import co.yixiang.yshop.module.system.dal.dataobject.permission.MenuDO; import co.yixiang.yshop.module.system.dal.mysql.permission.MenuMapper; import co.yixiang.yshop.module.system.dal.redis.RedisKeyConstants; import co.yixiang.yshop.module.system.enums.permission.MenuTypeEnum; import co.yixiang.yshop.module.system.service.tenant.TenantService; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Collection; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertList; import static co.yixiang.yshop.module.system.dal.dataobject.permission.MenuDO.ID_ROOT; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; /** * 菜单 Service 实现 * * @author yshop */ @Service @Slf4j public class MenuServiceImpl implements MenuService { @Resource private MenuMapper menuMapper; @Resource private PermissionService permissionService; @Resource @Lazy // 延迟,避免循环依赖报错 private TenantService tenantService; @Override @CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, key = "#createReqVO.permission", condition = "#createReqVO.permission != null") public Long createMenu(MenuSaveVO createReqVO) { // 校验父菜单存在 validateParentMenu(createReqVO.getParentId(), null); // 校验菜单(自己) validateMenu(createReqVO.getParentId(), createReqVO.getName(), null); // 插入数据库 MenuDO menu = BeanUtils.toBean(createReqVO, MenuDO.class); initMenuProperty(menu); menuMapper.insert(menu); // 返回 return menu.getId(); } @Override @CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, allEntries = true) // allEntries 清空所有缓存,因为 permission 如果变更,涉及到新老两个 permission。直接清理,简单有效 public void updateMenu(MenuSaveVO updateReqVO) { // 校验更新的菜单是否存在 if (menuMapper.selectById(updateReqVO.getId()) == null) { throw exception(MENU_NOT_EXISTS); } // 校验父菜单存在 validateParentMenu(updateReqVO.getParentId(), updateReqVO.getId()); // 校验菜单(自己) validateMenu(updateReqVO.getParentId(), updateReqVO.getName(), updateReqVO.getId()); // 更新到数据库 MenuDO updateObj = BeanUtils.toBean(updateReqVO, MenuDO.class); initMenuProperty(updateObj); menuMapper.updateById(updateObj); } @Override @Transactional(rollbackFor = Exception.class) @CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, allEntries = true) // allEntries 清空所有缓存,因为此时不知道 id 对应的 permission 是多少。直接清理,简单有效 public void deleteMenu(Long id) { // 校验是否还有子菜单 if (menuMapper.selectCountByParentId(id) > 0) { throw exception(MENU_EXISTS_CHILDREN); } // 校验删除的菜单是否存在 if (menuMapper.selectById(id) == null) { throw exception(MENU_NOT_EXISTS); } // 标记删除 menuMapper.deleteById(id); // 删除授予给角色的权限 permissionService.processMenuDeleted(id); } @Override public List getMenuList() { return menuMapper.selectList(); } @Override public List getMenuListByTenant(MenuListReqVO reqVO) { List menus = getMenuList(reqVO); // 开启多租户的情况下,需要过滤掉未开通的菜单 tenantService.handleTenantMenu(menuIds -> menus.removeIf(menu -> !CollUtil.contains(menuIds, menu.getId()))); return menus; } @Override public List getMenuList(MenuListReqVO reqVO) { return menuMapper.selectList(reqVO); } @Override @Cacheable(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, key = "#permission") public List getMenuIdListByPermissionFromCache(String permission) { List menus = menuMapper.selectListByPermission(permission); return convertList(menus, MenuDO::getId); } @Override public MenuDO getMenu(Long id) { return menuMapper.selectById(id); } @Override public List getMenuList(Collection ids) { // 当 ids 为空时,返回一个空的实例对象 if (CollUtil.isEmpty(ids)) { return Lists.newArrayList(); } return menuMapper.selectBatchIds(ids); } /** * 校验父菜单是否合法 *

* 1. 不能设置自己为父菜单 * 2. 父菜单不存在 * 3. 父菜单必须是 {@link MenuTypeEnum#MENU} 菜单类型 * * @param parentId 父菜单编号 * @param childId 当前菜单编号 */ @VisibleForTesting void validateParentMenu(Long parentId, Long childId) { if (parentId == null || ID_ROOT.equals(parentId)) { return; } // 不能设置自己为父菜单 if (parentId.equals(childId)) { throw exception(MENU_PARENT_ERROR); } MenuDO menu = menuMapper.selectById(parentId); // 父菜单不存在 if (menu == null) { throw exception(MENU_PARENT_NOT_EXISTS); } // 父菜单必须是目录或者菜单类型 if (!MenuTypeEnum.DIR.getType().equals(menu.getType()) && !MenuTypeEnum.MENU.getType().equals(menu.getType())) { throw exception(MENU_PARENT_NOT_DIR_OR_MENU); } } /** * 校验菜单是否合法 *

* 1. 校验相同父菜单编号下,是否存在相同的菜单名 * * @param name 菜单名字 * @param parentId 父菜单编号 * @param id 菜单编号 */ @VisibleForTesting void validateMenu(Long parentId, String name, Long id) { MenuDO menu = menuMapper.selectByParentIdAndName(parentId, name); if (menu == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的菜单 if (id == null) { throw exception(MENU_NAME_DUPLICATE); } if (!menu.getId().equals(id)) { throw exception(MENU_NAME_DUPLICATE); } } /** * 初始化菜单的通用属性。 *

* 例如说,只有目录或者菜单类型的菜单,才设置 icon * * @param menu 菜单 */ private void initMenuProperty(MenuDO menu) { // 菜单为按钮类型时,无需 component、icon、path 属性,进行置空 if (MenuTypeEnum.BUTTON.getType().equals(menu.getType())) { menu.setComponent(""); menu.setComponentName(""); menu.setIcon(""); menu.setPath(""); } } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/permission/PermissionService.java ================================================ package co.yixiang.yshop.module.system.service.permission; import co.yixiang.yshop.module.system.api.permission.dto.DeptDataPermissionRespDTO; import java.util.Collection; import java.util.Set; import static java.util.Collections.singleton; /** * 权限 Service 接口 *

* 提供用户-角色、角色-菜单、角色-部门的关联权限处理 * * @author yshop */ public interface PermissionService { /** * 判断是否有权限,任一一个即可 * * @param userId 用户编号 * @param permissions 权限 * @return 是否 */ boolean hasAnyPermissions(Long userId, String... permissions); /** * 判断是否有角色,任一一个即可 * * @param roles 角色数组 * @return 是否 */ boolean hasAnyRoles(Long userId, String... roles); // ========== 角色-菜单的相关方法 ========== /** * 设置角色菜单 * * @param roleId 角色编号 * @param menuIds 菜单编号集合 */ void assignRoleMenu(Long roleId, Set menuIds); /** * 处理角色删除时,删除关联授权数据 * * @param roleId 角色编号 */ void processRoleDeleted(Long roleId); /** * 处理菜单删除时,删除关联授权数据 * * @param menuId 菜单编号 */ void processMenuDeleted(Long menuId); /** * 获得角色拥有的菜单编号集合 * * @param roleId 角色编号 * @return 菜单编号集合 */ default Set getRoleMenuListByRoleId(Long roleId) { return getRoleMenuListByRoleId(singleton(roleId)); } /** * 获得角色们拥有的菜单编号集合 * * @param roleIds 角色编号数组 * @return 菜单编号集合 */ Set getRoleMenuListByRoleId(Collection roleIds); /** * 获得拥有指定菜单的角色编号数组,从缓存中获取 * * @param menuId 菜单编号 * @return 角色编号数组 */ Set getMenuRoleIdListByMenuIdFromCache(Long menuId); // ========== 用户-角色的相关方法 ========== /** * 设置用户角色 * * @param userId 角色编号 * @param roleIds 角色编号集合 */ void assignUserRole(Long userId, Set roleIds); /** * 处理用户删除时,删除关联授权数据 * * @param userId 用户编号 */ void processUserDeleted(Long userId); /** * 获得拥有多个角色的用户编号集合 * * @param roleIds 角色编号集合 * @return 用户编号集合 */ Set getUserRoleIdListByRoleId(Collection roleIds); /** * 获得用户拥有的角色编号集合 * * @param userId 用户编号 * @return 角色编号集合 */ Set getUserRoleIdListByUserId(Long userId); /** * 获得用户拥有的角色编号集合,从缓存中获取 * * @param userId 用户编号 * @return 角色编号集合 */ Set getUserRoleIdListByUserIdFromCache(Long userId); // ========== 用户-部门的相关方法 ========== /** * 设置角色的数据权限 * * @param roleId 角色编号 * @param dataScope 数据范围 * @param dataScopeDeptIds 部门编号数组 */ void assignRoleDataScope(Long roleId, Integer dataScope, Set dataScopeDeptIds); /** * 获得登陆用户的部门数据权限 * * @param userId 用户编号 * @return 部门数据权限 */ DeptDataPermissionRespDTO getDeptDataPermission(Long userId); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/permission/PermissionServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.permission; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.extra.spring.SpringUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.framework.datapermission.core.annotation.DataPermission; import co.yixiang.yshop.module.system.api.permission.dto.DeptDataPermissionRespDTO; import co.yixiang.yshop.module.system.dal.dataobject.permission.MenuDO; import co.yixiang.yshop.module.system.dal.dataobject.permission.RoleDO; import co.yixiang.yshop.module.system.dal.dataobject.permission.RoleMenuDO; import co.yixiang.yshop.module.system.dal.dataobject.permission.UserRoleDO; import co.yixiang.yshop.module.system.dal.mysql.permission.RoleMenuMapper; import co.yixiang.yshop.module.system.dal.mysql.permission.UserRoleMapper; import co.yixiang.yshop.module.system.dal.redis.RedisKeyConstants; import co.yixiang.yshop.module.system.enums.permission.DataScopeEnum; import co.yixiang.yshop.module.system.service.dept.DeptService; import co.yixiang.yshop.module.system.service.user.AdminUserService; import com.baomidou.dynamic.datasource.annotation.DSTransactional; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Suppliers; import com.google.common.collect.Sets; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import jakarta.annotation.Resource; import java.util.*; import java.util.function.Supplier; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertSet; import static co.yixiang.yshop.framework.common.util.json.JsonUtils.toJsonString; /** * 权限 Service 实现类 * * @author yshop */ @Service @Slf4j public class PermissionServiceImpl implements PermissionService { @Resource private RoleMenuMapper roleMenuMapper; @Resource private UserRoleMapper userRoleMapper; @Resource private RoleService roleService; @Resource private MenuService menuService; @Resource private DeptService deptService; @Resource private AdminUserService userService; @Override public boolean hasAnyPermissions(Long userId, String... permissions) { // 如果为空,说明已经有权限 if (ArrayUtil.isEmpty(permissions)) { return true; } // 获得当前登录的角色。如果为空,说明没有权限 List roles = getEnableUserRoleListByUserIdFromCache(userId); if (CollUtil.isEmpty(roles)) { return false; } // 情况一:遍历判断每个权限,如果有一满足,说明有权限 for (String permission : permissions) { if (hasAnyPermission(roles, permission)) { return true; } } // 情况二:如果是超管,也说明有权限 return roleService.hasAnySuperAdmin(convertSet(roles, RoleDO::getId)); } /** * 判断指定角色,是否拥有该 permission 权限 * * @param roles 指定角色数组 * @param permission 权限标识 * @return 是否拥有 */ private boolean hasAnyPermission(List roles, String permission) { List menuIds = menuService.getMenuIdListByPermissionFromCache(permission); // 采用严格模式,如果权限找不到对应的 Menu 的话,也认为没有权限 if (CollUtil.isEmpty(menuIds)) { return false; } // 判断是否有权限 Set roleIds = convertSet(roles, RoleDO::getId); for (Long menuId : menuIds) { // 获得拥有该菜单的角色编号集合 Set menuRoleIds = getSelf().getMenuRoleIdListByMenuIdFromCache(menuId); // 如果有交集,说明有权限 if (CollUtil.containsAny(menuRoleIds, roleIds)) { return true; } } return false; } @Override public boolean hasAnyRoles(Long userId, String... roles) { // 如果为空,说明已经有权限 if (ArrayUtil.isEmpty(roles)) { return true; } // 获得当前登录的角色。如果为空,说明没有权限 List roleList = getEnableUserRoleListByUserIdFromCache(userId); if (CollUtil.isEmpty(roleList)) { return false; } // 判断是否有角色 Set userRoles = convertSet(roleList, RoleDO::getCode); return CollUtil.containsAny(userRoles, Sets.newHashSet(roles)); } // ========== 角色-菜单的相关方法 ========== @Override @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换 @CacheEvict(value = RedisKeyConstants.MENU_ROLE_ID_LIST, allEntries = true) // allEntries 清空所有缓存,主要一次更新涉及到的 menuIds 较多,反倒批量会更快 public void assignRoleMenu(Long roleId, Set menuIds) { // 获得角色拥有菜单编号 Set dbMenuIds = convertSet(roleMenuMapper.selectListByRoleId(roleId), RoleMenuDO::getMenuId); // 计算新增和删除的菜单编号 Set menuIdList = CollUtil.emptyIfNull(menuIds); Collection createMenuIds = CollUtil.subtract(menuIdList, dbMenuIds); Collection deleteMenuIds = CollUtil.subtract(dbMenuIds, menuIdList); // 执行新增和删除。对于已经授权的菜单,不用做任何处理 if (CollUtil.isNotEmpty(createMenuIds)) { roleMenuMapper.insertBatch(CollectionUtils.convertList(createMenuIds, menuId -> { RoleMenuDO entity = new RoleMenuDO(); entity.setRoleId(roleId); entity.setMenuId(menuId); return entity; })); } if (CollUtil.isNotEmpty(deleteMenuIds)) { roleMenuMapper.deleteListByRoleIdAndMenuIds(roleId, deleteMenuIds); } } @Override @Transactional(rollbackFor = Exception.class) @Caching(evict = { @CacheEvict(value = RedisKeyConstants.MENU_ROLE_ID_LIST, allEntries = true), // allEntries 清空所有缓存,此处无法方便获得 roleId 对应的 menu 缓存们 @CacheEvict(value = RedisKeyConstants.USER_ROLE_ID_LIST, allEntries = true) // allEntries 清空所有缓存,此处无法方便获得 roleId 对应的 user 缓存们 }) public void processRoleDeleted(Long roleId) { // 标记删除 UserRole userRoleMapper.deleteListByRoleId(roleId); // 标记删除 RoleMenu roleMenuMapper.deleteListByRoleId(roleId); } @Override @CacheEvict(value = RedisKeyConstants.MENU_ROLE_ID_LIST, key = "#menuId") public void processMenuDeleted(Long menuId) { roleMenuMapper.deleteListByMenuId(menuId); } @Override public Set getRoleMenuListByRoleId(Collection roleIds) { if (CollUtil.isEmpty(roleIds)) { return Collections.emptySet(); } // 如果是管理员的情况下,获取全部菜单编号 if (roleService.hasAnySuperAdmin(roleIds)) { return convertSet(menuService.getMenuList(), MenuDO::getId); } // 如果是非管理员的情况下,获得拥有的菜单编号 return convertSet(roleMenuMapper.selectListByRoleId(roleIds), RoleMenuDO::getMenuId); } @Override @Cacheable(value = RedisKeyConstants.MENU_ROLE_ID_LIST, key = "#menuId") public Set getMenuRoleIdListByMenuIdFromCache(Long menuId) { return convertSet(roleMenuMapper.selectListByMenuId(menuId), RoleMenuDO::getRoleId); } // ========== 用户-角色的相关方法 ========== @Override @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换 @CacheEvict(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId") public void assignUserRole(Long userId, Set roleIds) { // 获得角色拥有角色编号 Set dbRoleIds = convertSet(userRoleMapper.selectListByUserId(userId), UserRoleDO::getRoleId); // 计算新增和删除的角色编号 Set roleIdList = CollUtil.emptyIfNull(roleIds); Collection createRoleIds = CollUtil.subtract(roleIdList, dbRoleIds); Collection deleteMenuIds = CollUtil.subtract(dbRoleIds, roleIdList); // 执行新增和删除。对于已经授权的角色,不用做任何处理 if (!CollectionUtil.isEmpty(createRoleIds)) { userRoleMapper.insertBatch(CollectionUtils.convertList(createRoleIds, roleId -> { UserRoleDO entity = new UserRoleDO(); entity.setUserId(userId); entity.setRoleId(roleId); return entity; })); } if (!CollectionUtil.isEmpty(deleteMenuIds)) { userRoleMapper.deleteListByUserIdAndRoleIdIds(userId, deleteMenuIds); } } @Override @CacheEvict(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId") public void processUserDeleted(Long userId) { userRoleMapper.deleteListByUserId(userId); } @Override public Set getUserRoleIdListByUserId(Long userId) { return convertSet(userRoleMapper.selectListByUserId(userId), UserRoleDO::getRoleId); } @Override @Cacheable(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId") public Set getUserRoleIdListByUserIdFromCache(Long userId) { return getUserRoleIdListByUserId(userId); } @Override public Set getUserRoleIdListByRoleId(Collection roleIds) { return convertSet(userRoleMapper.selectListByRoleIds(roleIds), UserRoleDO::getUserId); } /** * 获得用户拥有的角色,并且这些角色是开启状态的 * * @param userId 用户编号 * @return 用户拥有的角色 */ @VisibleForTesting List getEnableUserRoleListByUserIdFromCache(Long userId) { // 获得用户拥有的角色编号 Set roleIds = getSelf().getUserRoleIdListByUserIdFromCache(userId); // 获得角色数组,并移除被禁用的 List roles = roleService.getRoleListFromCache(roleIds); roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); return roles; } // ========== 用户-部门的相关方法 ========== @Override public void assignRoleDataScope(Long roleId, Integer dataScope, Set dataScopeDeptIds) { roleService.updateRoleDataScope(roleId, dataScope, dataScopeDeptIds); } @Override @DataPermission(enable = false) // 关闭数据权限,不然就会出现递归获取数据权限的问题 public DeptDataPermissionRespDTO getDeptDataPermission(Long userId) { // 获得用户的角色 List roles = getEnableUserRoleListByUserIdFromCache(userId); // 如果角色为空,则只能查看自己 DeptDataPermissionRespDTO result = new DeptDataPermissionRespDTO(); if (CollUtil.isEmpty(roles)) { result.setSelf(true); return result; } // 获得用户的部门编号的缓存,通过 Guava 的 Suppliers 惰性求值,即有且仅有第一次发起 DB 的查询 Supplier userDeptId = Suppliers.memoize(() -> userService.getUser(userId).getDeptId()); // 遍历每个角色,计算 for (RoleDO role : roles) { // 为空时,跳过 if (role.getDataScope() == null) { continue; } // 情况一,ALL if (Objects.equals(role.getDataScope(), DataScopeEnum.ALL.getScope())) { result.setAll(true); continue; } // 情况二,DEPT_CUSTOM if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_CUSTOM.getScope())) { CollUtil.addAll(result.getDeptIds(), role.getDataScopeDeptIds()); // 自定义可见部门时,保证可以看到自己所在的部门。否则,一些场景下可能会有问题。 // 例如说,登录时,基于 t_user 的 username 查询会可能被 dept_id 过滤掉 CollUtil.addAll(result.getDeptIds(), userDeptId.get()); continue; } // 情况三,DEPT_ONLY if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_ONLY.getScope())) { CollectionUtils.addIfNotNull(result.getDeptIds(), userDeptId.get()); continue; } // 情况四,DEPT_DEPT_AND_CHILD if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_AND_CHILD.getScope())) { CollUtil.addAll(result.getDeptIds(), deptService.getChildDeptIdListFromCache(userDeptId.get())); // 添加本身部门编号 CollUtil.addAll(result.getDeptIds(), userDeptId.get()); continue; } // 情况五,SELF if (Objects.equals(role.getDataScope(), DataScopeEnum.SELF.getScope())) { result.setSelf(true); continue; } // 未知情况,error log 即可 log.error("[getDeptDataPermission][LoginUser({}) role({}) 无法处理]", userId, toJsonString(result)); } return result; } /** * 获得自身的代理对象,解决 AOP 生效问题 * * @return 自己 */ private PermissionServiceImpl getSelf() { return SpringUtil.getBean(getClass()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/permission/RoleService.java ================================================ package co.yixiang.yshop.module.system.service.permission; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.controller.admin.permission.vo.role.RolePageReqVO; import co.yixiang.yshop.module.system.controller.admin.permission.vo.role.RoleSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.permission.RoleDO; import jakarta.validation.Valid; import java.util.Collection; import java.util.List; import java.util.Set; /** * 角色 Service 接口 * * @author yshop */ public interface RoleService { /** * 创建角色 * * @param createReqVO 创建角色信息 * @param type 角色类型 * @return 角色编号 */ Long createRole(@Valid RoleSaveReqVO createReqVO, Integer type); /** * 更新角色 * * @param updateReqVO 更新角色信息 */ void updateRole(@Valid RoleSaveReqVO updateReqVO); /** * 删除角色 * * @param id 角色编号 */ void deleteRole(Long id); /** * 设置角色的数据权限 * * @param id 角色编号 * @param dataScope 数据范围 * @param dataScopeDeptIds 部门编号数组 */ void updateRoleDataScope(Long id, Integer dataScope, Set dataScopeDeptIds); /** * 获得角色 * * @param id 角色编号 * @return 角色 */ RoleDO getRole(Long id); /** * 获得角色,从缓存中 * * @param id 角色编号 * @return 角色 */ RoleDO getRoleFromCache(Long id); /** * 获得角色列表 * * @param ids 角色编号数组 * @return 角色列表 */ List getRoleList(Collection ids); /** * 获得角色数组,从缓存中 * * @param ids 角色编号数组 * @return 角色数组 */ List getRoleListFromCache(Collection ids); /** * 获得角色列表 * * @param statuses 筛选的状态 * @return 角色列表 */ List getRoleListByStatus(Collection statuses); /** * 获得所有角色列表 * * @return 角色列表 */ List getRoleList(); /** * 获得角色分页 * * @param reqVO 角色分页查询 * @return 角色分页结果 */ PageResult getRolePage(RolePageReqVO reqVO); /** * 判断角色编号数组中,是否有管理员 * * @param ids 角色编号数组 * @return 是否有管理员 */ boolean hasAnySuperAdmin(Collection ids); /** * 校验角色们是否有效。如下情况,视为无效: * 1. 角色编号不存在 * 2. 角色被禁用 * * @param ids 角色编号数组 */ void validateRoleList(Collection ids); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/permission/RoleServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.permission; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.extra.spring.SpringUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.permission.vo.role.RolePageReqVO; import co.yixiang.yshop.module.system.controller.admin.permission.vo.role.RoleSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.permission.RoleDO; import co.yixiang.yshop.module.system.dal.mysql.permission.RoleMapper; import co.yixiang.yshop.module.system.dal.redis.RedisKeyConstants; import co.yixiang.yshop.module.system.enums.permission.DataScopeEnum; import co.yixiang.yshop.module.system.enums.permission.RoleCodeEnum; import co.yixiang.yshop.module.system.enums.permission.RoleTypeEnum; import com.google.common.annotations.VisibleForTesting; import com.mzt.logapi.context.LogRecordContext; import com.mzt.logapi.service.impl.DiffParseFunction; import com.mzt.logapi.starter.annotation.LogRecord; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.util.*; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertMap; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; import static co.yixiang.yshop.module.system.enums.LogRecordConstants.*; /** * 角色 Service 实现类 * * @author yshop */ @Service @Slf4j public class RoleServiceImpl implements RoleService { @Resource private PermissionService permissionService; @Resource private RoleMapper roleMapper; @Override @Transactional(rollbackFor = Exception.class) @LogRecord(type = SYSTEM_ROLE_TYPE, subType = SYSTEM_ROLE_CREATE_SUB_TYPE, bizNo = "{{#role.id}}", success = SYSTEM_ROLE_CREATE_SUCCESS) public Long createRole(RoleSaveReqVO createReqVO, Integer type) { // 1. 校验角色 validateRoleDuplicate(createReqVO.getName(), createReqVO.getCode(), null); // 2. 插入到数据库 RoleDO role = BeanUtils.toBean(createReqVO, RoleDO.class) .setType(ObjectUtil.defaultIfNull(type, RoleTypeEnum.CUSTOM.getType())) .setStatus(CommonStatusEnum.ENABLE.getStatus()) .setDataScope(DataScopeEnum.ALL.getScope()); // 默认可查看所有数据。原因是,可能一些项目不需要项目权限 roleMapper.insert(role); // 3. 记录操作日志上下文 LogRecordContext.putVariable("role", role); return role.getId(); } @Override @CacheEvict(value = RedisKeyConstants.ROLE, key = "#updateReqVO.id") @LogRecord(type = SYSTEM_ROLE_TYPE, subType = SYSTEM_ROLE_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}", success = SYSTEM_ROLE_UPDATE_SUCCESS) public void updateRole(RoleSaveReqVO updateReqVO) { // 1.1 校验是否可以更新 RoleDO role = validateRoleForUpdate(updateReqVO.getId()); // 1.2 校验角色的唯一字段是否重复 validateRoleDuplicate(updateReqVO.getName(), updateReqVO.getCode(), updateReqVO.getId()); // 2. 更新到数据库 RoleDO updateObj = BeanUtils.toBean(updateReqVO, RoleDO.class); roleMapper.updateById(updateObj); // 3. 记录操作日志上下文 LogRecordContext.putVariable("role", role); } @Override @CacheEvict(value = RedisKeyConstants.ROLE, key = "#id") public void updateRoleDataScope(Long id, Integer dataScope, Set dataScopeDeptIds) { // 校验是否可以更新 validateRoleForUpdate(id); // 更新数据范围 RoleDO updateObject = new RoleDO(); updateObject.setId(id); updateObject.setDataScope(dataScope); updateObject.setDataScopeDeptIds(dataScopeDeptIds); roleMapper.updateById(updateObject); } @Override @Transactional(rollbackFor = Exception.class) @CacheEvict(value = RedisKeyConstants.ROLE, key = "#id") @LogRecord(type = SYSTEM_ROLE_TYPE, subType = SYSTEM_ROLE_DELETE_SUB_TYPE, bizNo = "{{#id}}", success = SYSTEM_ROLE_DELETE_SUCCESS) public void deleteRole(Long id) { // 1. 校验是否可以更新 RoleDO role = validateRoleForUpdate(id); // 2.1 标记删除 roleMapper.deleteById(id); // 2.2 删除相关数据 permissionService.processRoleDeleted(id); // 3. 记录操作日志上下文 LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(role, RoleSaveReqVO.class)); LogRecordContext.putVariable("role", role); } /** * 校验角色的唯一字段是否重复 * * 1. 是否存在相同名字的角色 * 2. 是否存在相同编码的角色 * * @param name 角色名字 * @param code 角色额编码 * @param id 角色编号 */ @VisibleForTesting void validateRoleDuplicate(String name, String code, Long id) { // 0. 超级管理员,不允许创建 if (RoleCodeEnum.isSuperAdmin(code)) { throw exception(ROLE_ADMIN_CODE_ERROR, code); } // 1. 该 name 名字被其它角色所使用 RoleDO role = roleMapper.selectByName(name); if (role != null && !role.getId().equals(id)) { throw exception(ROLE_NAME_DUPLICATE, name); } // 2. 是否存在相同编码的角色 if (!StringUtils.hasText(code)) { return; } // 该 code 编码被其它角色所使用 role = roleMapper.selectByCode(code); if (role != null && !role.getId().equals(id)) { throw exception(ROLE_CODE_DUPLICATE, code); } } /** * 校验角色是否可以被更新 * * @param id 角色编号 */ @VisibleForTesting RoleDO validateRoleForUpdate(Long id) { RoleDO role = roleMapper.selectById(id); if (role == null) { throw exception(ROLE_NOT_EXISTS); } // 内置角色,不允许删除 if (RoleTypeEnum.SYSTEM.getType().equals(role.getType())) { throw exception(ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE); } return role; } @Override public RoleDO getRole(Long id) { return roleMapper.selectById(id); } @Override @Cacheable(value = RedisKeyConstants.ROLE, key = "#id", unless = "#result == null") public RoleDO getRoleFromCache(Long id) { return roleMapper.selectById(id); } @Override public List getRoleListByStatus(Collection statuses) { return roleMapper.selectListByStatus(statuses); } @Override public List getRoleList() { return roleMapper.selectList(); } @Override public List getRoleList(Collection ids) { if (CollectionUtil.isEmpty(ids)) { return Collections.emptyList(); } return roleMapper.selectBatchIds(ids); } @Override public List getRoleListFromCache(Collection ids) { if (CollectionUtil.isEmpty(ids)) { return Collections.emptyList(); } // 这里采用 for 循环从缓存中获取,主要考虑 Spring CacheManager 无法批量操作的问题 RoleServiceImpl self = getSelf(); return CollectionUtils.convertList(ids, self::getRoleFromCache); } @Override public PageResult getRolePage(RolePageReqVO reqVO) { return roleMapper.selectPage(reqVO); } @Override public boolean hasAnySuperAdmin(Collection ids) { if (CollectionUtil.isEmpty(ids)) { return false; } RoleServiceImpl self = getSelf(); return ids.stream().anyMatch(id -> { RoleDO role = self.getRoleFromCache(id); return role != null && RoleCodeEnum.isSuperAdmin(role.getCode()); }); } @Override public void validateRoleList(Collection ids) { if (CollUtil.isEmpty(ids)) { return; } // 获得角色信息 List roles = roleMapper.selectBatchIds(ids); Map roleMap = convertMap(roles, RoleDO::getId); // 校验 ids.forEach(id -> { RoleDO role = roleMap.get(id); if (role == null) { throw exception(ROLE_NOT_EXISTS); } if (!CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())) { throw exception(ROLE_IS_DISABLE, role.getName()); } }); } /** * 获得自身的代理对象,解决 AOP 生效问题 * * @return 自己 */ private RoleServiceImpl getSelf() { return SpringUtil.getBean(getClass()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/sms/SmsChannelService.java ================================================ package co.yixiang.yshop.module.system.service.sms; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.framework.sms.core.client.SmsClient; import co.yixiang.yshop.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; import co.yixiang.yshop.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsChannelDO; import jakarta.validation.Valid; import java.util.List; /** * 短信渠道 Service 接口 * * @author zzf * @since 2021/1/25 9:24 */ public interface SmsChannelService { /** * 创建短信渠道 * * @param createReqVO 创建信息 * @return 编号 */ Long createSmsChannel(@Valid SmsChannelSaveReqVO createReqVO); /** * 更新短信渠道 * * @param updateReqVO 更新信息 */ void updateSmsChannel(@Valid SmsChannelSaveReqVO updateReqVO); /** * 删除短信渠道 * * @param id 编号 */ void deleteSmsChannel(Long id); /** * 获得短信渠道 * * @param id 编号 * @return 短信渠道 */ SmsChannelDO getSmsChannel(Long id); /** * 获得所有短信渠道列表 * * @return 短信渠道列表 */ List getSmsChannelList(); /** * 获得短信渠道分页 * * @param pageReqVO 分页查询 * @return 短信渠道分页 */ PageResult getSmsChannelPage(SmsChannelPageReqVO pageReqVO); /** * 获得短信客户端 * * @param id 编号 * @return 短信客户端 */ SmsClient getSmsClient(Long id); /** * 获得短信客户端 * * @param code 编码 * @return 短信客户端 */ SmsClient getSmsClient(String code); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/sms/SmsChannelServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.sms; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.framework.sms.core.client.SmsClient; import co.yixiang.yshop.module.system.framework.sms.core.client.SmsClientFactory; import co.yixiang.yshop.module.system.framework.sms.core.property.SmsChannelProperties; import co.yixiang.yshop.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; import co.yixiang.yshop.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsChannelDO; import co.yixiang.yshop.module.system.dal.mysql.sms.SmsChannelMapper; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import java.time.Duration; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS; /** * 短信渠道 Service 实现类 * * @author zzf */ @Service @Slf4j public class SmsChannelServiceImpl implements SmsChannelService { /** * {@link SmsClient} 缓存,通过它异步刷新 smsClientFactory */ @Getter private final LoadingCache idClientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L), new CacheLoader() { @Override public SmsClient load(Long id) { // 查询,然后尝试刷新 SmsChannelDO channel = smsChannelMapper.selectById(id); if (channel != null) { SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class); smsClientFactory.createOrUpdateSmsClient(properties); } return smsClientFactory.getSmsClient(id); } }); /** * {@link SmsClient} 缓存,通过它异步刷新 smsClientFactory */ @Getter private final LoadingCache codeClientCache = buildAsyncReloadingCache(Duration.ofSeconds(60L), new CacheLoader() { @Override public SmsClient load(String code) { // 查询,然后尝试刷新 SmsChannelDO channel = smsChannelMapper.selectByCode(code); if (channel != null) { SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class); smsClientFactory.createOrUpdateSmsClient(properties); } return smsClientFactory.getSmsClient(code); } }); @Resource private SmsClientFactory smsClientFactory; @Resource private SmsChannelMapper smsChannelMapper; @Resource private SmsTemplateService smsTemplateService; @Override public Long createSmsChannel(SmsChannelSaveReqVO createReqVO) { SmsChannelDO channel = BeanUtils.toBean(createReqVO, SmsChannelDO.class); smsChannelMapper.insert(channel); return channel.getId(); } @Override public void updateSmsChannel(SmsChannelSaveReqVO updateReqVO) { // 校验存在 SmsChannelDO channel = validateSmsChannelExists(updateReqVO.getId()); // 更新 SmsChannelDO updateObj = BeanUtils.toBean(updateReqVO, SmsChannelDO.class); smsChannelMapper.updateById(updateObj); // 清空缓存 clearCache(updateReqVO.getId(), channel.getCode()); } @Override public void deleteSmsChannel(Long id) { // 校验存在 SmsChannelDO channel = validateSmsChannelExists(id); // 校验是否有在使用该账号的模版 if (smsTemplateService.getSmsTemplateCountByChannelId(id) > 0) { throw exception(SMS_CHANNEL_HAS_CHILDREN); } // 删除 smsChannelMapper.deleteById(id); // 清空缓存 clearCache(id, channel.getCode()); } /** * 清空指定渠道编号的缓存 * * @param id 渠道编号 * @param code 渠道编码 */ private void clearCache(Long id, String code) { idClientCache.invalidate(id); if (StrUtil.isNotEmpty(code)) { codeClientCache.invalidate(code); } } private SmsChannelDO validateSmsChannelExists(Long id) { SmsChannelDO channel = smsChannelMapper.selectById(id); if (channel == null) { throw exception(SMS_CHANNEL_NOT_EXISTS); } return channel; } @Override public SmsChannelDO getSmsChannel(Long id) { return smsChannelMapper.selectById(id); } @Override public List getSmsChannelList() { return smsChannelMapper.selectList(); } @Override public PageResult getSmsChannelPage(SmsChannelPageReqVO pageReqVO) { return smsChannelMapper.selectPage(pageReqVO); } @Override public SmsClient getSmsClient(Long id) { return idClientCache.getUnchecked(id); } @Override public SmsClient getSmsClient(String code) { return codeClientCache.getUnchecked(code); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/sms/SmsCodeService.java ================================================ package co.yixiang.yshop.module.system.service.sms; import co.yixiang.yshop.framework.common.exception.ServiceException; import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeValidateReqDTO; import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeSendReqDTO; import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeUseReqDTO; import jakarta.validation.Valid; /** * 短信验证码 Service 接口 * * @author yshop */ public interface SmsCodeService { /** * 创建短信验证码,并进行发送 * * @param reqDTO 发送请求 */ void sendSmsCode(@Valid SmsCodeSendReqDTO reqDTO); /** * 验证短信验证码,并进行使用 * 如果正确,则将验证码标记成已使用 * 如果错误,则抛出 {@link ServiceException} 异常 * * @param reqDTO 使用请求 */ void useSmsCode(@Valid SmsCodeUseReqDTO reqDTO); /** * 检查验证码是否有效 * * @param reqDTO 校验请求 */ void validateSmsCode(@Valid SmsCodeValidateReqDTO reqDTO); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/sms/SmsCodeServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.sms; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeSendReqDTO; import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeUseReqDTO; import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeValidateReqDTO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsCodeDO; import co.yixiang.yshop.module.system.dal.mysql.sms.SmsCodeMapper; import co.yixiang.yshop.module.system.enums.sms.SmsSceneEnum; import co.yixiang.yshop.module.system.framework.sms.config.SmsCodeProperties; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.time.LocalDateTime; import static cn.hutool.core.util.RandomUtil.randomInt; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.util.date.DateUtils.isToday; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; /** * 短信验证码 Service 实现类 * * @author yshop */ @Service @Validated public class SmsCodeServiceImpl implements SmsCodeService { @Resource private SmsCodeProperties smsCodeProperties; @Resource private SmsCodeMapper smsCodeMapper; @Resource private SmsSendService smsSendService; @Override public void sendSmsCode(SmsCodeSendReqDTO reqDTO) { SmsSceneEnum sceneEnum = SmsSceneEnum.getCodeByScene(reqDTO.getScene()); Assert.notNull(sceneEnum, "验证码场景({}) 查找不到配置", reqDTO.getScene()); // 创建验证码 String code = createSmsCode(reqDTO.getMobile(), reqDTO.getScene(), reqDTO.getCreateIp()); // 发送验证码 smsSendService.sendSingleSms(reqDTO.getMobile(), null, null, sceneEnum.getTemplateCode(), MapUtil.of("code", code)); } private String createSmsCode(String mobile, Integer scene, String ip) { // 校验是否可以发送验证码,不用筛选场景 SmsCodeDO lastSmsCode = smsCodeMapper.selectLastByMobile(mobile, null, null); if (lastSmsCode != null) { if (LocalDateTimeUtil.between(lastSmsCode.getCreateTime(), LocalDateTime.now()).toMillis() < smsCodeProperties.getSendFrequency().toMillis()) { // 发送过于频繁 throw exception(SMS_CODE_SEND_TOO_FAST); } if (isToday(lastSmsCode.getCreateTime()) && // 必须是今天,才能计算超过当天的上限 lastSmsCode.getTodayIndex() >= smsCodeProperties.getSendMaximumQuantityPerDay()) { // 超过当天发送的上限。 throw exception(SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY); } // TODO yshop:提升,每个 IP 每天可发送数量 // TODO yshop:提升,每个 IP 每小时可发送数量 } // 创建验证码记录 String code = String.format("%0" + smsCodeProperties.getEndCode().toString().length() + "d", randomInt(smsCodeProperties.getBeginCode(), smsCodeProperties.getEndCode() + 1)); SmsCodeDO newSmsCode = SmsCodeDO.builder().mobile(mobile).code(code).scene(scene) .todayIndex(lastSmsCode != null && isToday(lastSmsCode.getCreateTime()) ? lastSmsCode.getTodayIndex() + 1 : 1) .createIp(ip).used(false).build(); smsCodeMapper.insert(newSmsCode); return code; } @Override public void useSmsCode(SmsCodeUseReqDTO reqDTO) { // 检测验证码是否有效 SmsCodeDO lastSmsCode = validateSmsCode0(reqDTO.getMobile(), reqDTO.getCode(), reqDTO.getScene()); // 使用验证码 smsCodeMapper.updateById(SmsCodeDO.builder().id(lastSmsCode.getId()) .used(true).usedTime(LocalDateTime.now()).usedIp(reqDTO.getUsedIp()).build()); } @Override public void validateSmsCode(SmsCodeValidateReqDTO reqDTO) { validateSmsCode0(reqDTO.getMobile(), reqDTO.getCode(), reqDTO.getScene()); } private SmsCodeDO validateSmsCode0(String mobile, String code, Integer scene) { // 校验验证码 SmsCodeDO lastSmsCode = smsCodeMapper.selectLastByMobile(mobile, code, scene); // 若验证码不存在,抛出异常 if (lastSmsCode == null) { throw exception(SMS_CODE_NOT_FOUND); } // 超过时间 if (LocalDateTimeUtil.between(lastSmsCode.getCreateTime(), LocalDateTime.now()).toMillis() >= smsCodeProperties.getExpireTimes().toMillis()) { // 验证码已过期 throw exception(SMS_CODE_EXPIRED); } // 判断验证码是否已被使用 if (Boolean.TRUE.equals(lastSmsCode.getUsed())) { throw exception(SMS_CODE_USED); } return lastSmsCode; } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/sms/SmsLogService.java ================================================ package co.yixiang.yshop.module.system.service.sms; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsLogDO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsTemplateDO; import java.time.LocalDateTime; import java.util.Map; /** * 短信日志 Service 接口 * * @author zzf * @date 13:48 2021/3/2 */ public interface SmsLogService { /** * 创建短信日志 * * @param mobile 手机号 * @param userId 用户编号 * @param userType 用户类型 * @param isSend 是否发送 * @param template 短信模板 * @param templateContent 短信内容 * @param templateParams 短信参数 * @return 发送日志编号 */ Long createSmsLog(String mobile, Long userId, Integer userType, Boolean isSend, SmsTemplateDO template, String templateContent, Map templateParams); /** * 更新日志的发送结果 * * @param id 日志编号 * @param success 发送是否成功 * @param apiSendCode 短信 API 发送结果的编码 * @param apiSendMsg 短信 API 发送失败的提示 * @param apiRequestId 短信 API 发送返回的唯一请求 ID * @param apiSerialNo 短信 API 发送返回的序号 */ void updateSmsSendResult(Long id, Boolean success, String apiSendCode, String apiSendMsg, String apiRequestId, String apiSerialNo); /** * 更新日志的接收结果 * * @param id 日志编号 * @param success 是否接收成功 * @param receiveTime 用户接收时间 * @param apiReceiveCode API 接收结果的编码 * @param apiReceiveMsg API 接收结果的说明 */ void updateSmsReceiveResult(Long id, Boolean success, LocalDateTime receiveTime, String apiReceiveCode, String apiReceiveMsg); /** * 获得短信日志分页 * * @param pageReqVO 分页查询 * @return 短信日志分页 */ PageResult getSmsLogPage(SmsLogPageReqVO pageReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/sms/SmsLogServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.sms; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsLogDO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsTemplateDO; import co.yixiang.yshop.module.system.dal.mysql.sms.SmsLogMapper; import co.yixiang.yshop.module.system.enums.sms.SmsReceiveStatusEnum; import co.yixiang.yshop.module.system.enums.sms.SmsSendStatusEnum; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import java.time.LocalDateTime; import java.util.Map; import java.util.Objects; /** * 短信日志 Service 实现类 * * @author zzf */ @Slf4j @Service public class SmsLogServiceImpl implements SmsLogService { @Resource private SmsLogMapper smsLogMapper; @Override public Long createSmsLog(String mobile, Long userId, Integer userType, Boolean isSend, SmsTemplateDO template, String templateContent, Map templateParams) { SmsLogDO.SmsLogDOBuilder logBuilder = SmsLogDO.builder(); // 根据是否要发送,设置状态 logBuilder.sendStatus(Objects.equals(isSend, true) ? SmsSendStatusEnum.INIT.getStatus() : SmsSendStatusEnum.IGNORE.getStatus()); // 设置手机相关字段 logBuilder.mobile(mobile).userId(userId).userType(userType); // 设置模板相关字段 logBuilder.templateId(template.getId()).templateCode(template.getCode()).templateType(template.getType()); logBuilder.templateContent(templateContent).templateParams(templateParams) .apiTemplateId(template.getApiTemplateId()); // 设置渠道相关字段 logBuilder.channelId(template.getChannelId()).channelCode(template.getChannelCode()); // 设置接收相关字段 logBuilder.receiveStatus(SmsReceiveStatusEnum.INIT.getStatus()); // 插入数据库 SmsLogDO logDO = logBuilder.build(); smsLogMapper.insert(logDO); return logDO.getId(); } @Override public void updateSmsSendResult(Long id, Boolean success, String apiSendCode, String apiSendMsg, String apiRequestId, String apiSerialNo) { SmsSendStatusEnum sendStatus = success ? SmsSendStatusEnum.SUCCESS : SmsSendStatusEnum.FAILURE; smsLogMapper.updateById(SmsLogDO.builder().id(id) .sendStatus(sendStatus.getStatus()).sendTime(LocalDateTime.now()) .apiSendCode(apiSendCode).apiSendMsg(apiSendMsg) .apiRequestId(apiRequestId).apiSerialNo(apiSerialNo).build()); } @Override public void updateSmsReceiveResult(Long id, Boolean success, LocalDateTime receiveTime, String apiReceiveCode, String apiReceiveMsg) { SmsReceiveStatusEnum receiveStatus = Objects.equals(success, true) ? SmsReceiveStatusEnum.SUCCESS : SmsReceiveStatusEnum.FAILURE; smsLogMapper.updateById(SmsLogDO.builder().id(id).receiveStatus(receiveStatus.getStatus()) .receiveTime(receiveTime).apiReceiveCode(apiReceiveCode).apiReceiveMsg(apiReceiveMsg).build()); } @Override public PageResult getSmsLogPage(SmsLogPageReqVO pageReqVO) { return smsLogMapper.selectPage(pageReqVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/sms/SmsSendService.java ================================================ package co.yixiang.yshop.module.system.service.sms; import co.yixiang.yshop.module.system.mq.message.sms.SmsSendMessage; import java.util.List; import java.util.Map; /** * 短信发送 Service 接口 * * @author yshop */ public interface SmsSendService { /** * 发送单条短信给管理后台的用户 * * 在 mobile 为空时,使用 userId 加载对应管理员的手机号 * * @param mobile 手机号 * @param userId 用户编号 * @param templateCode 短信模板编号 * @param templateParams 短信模板参数 * @return 发送日志编号 */ Long sendSingleSmsToAdmin(String mobile, Long userId, String templateCode, Map templateParams); /** * 发送单条短信给用户 APP 的用户 * * 在 mobile 为空时,使用 userId 加载对应会员的手机号 * * @param mobile 手机号 * @param userId 用户编号 * @param templateCode 短信模板编号 * @param templateParams 短信模板参数 * @return 发送日志编号 */ Long sendSingleSmsToMember(String mobile, Long userId, String templateCode, Map templateParams); /** * 发送单条短信给用户 * * @param mobile 手机号 * @param userId 用户编号 * @param userType 用户类型 * @param templateCode 短信模板编号 * @param templateParams 短信模板参数 * @return 发送日志编号 */ Long sendSingleSms(String mobile, Long userId, Integer userType, String templateCode, Map templateParams); default void sendBatchSms(List mobiles, List userIds, Integer userType, String templateCode, Map templateParams) { throw new UnsupportedOperationException("暂时不支持该操作,感兴趣可以实现该功能哟!"); } /** * 执行真正的短信发送 * 注意,该方法仅仅提供给 MQ Consumer 使用 * * @param message 短信 */ void doSendSms(SmsSendMessage message); /** * 接收短信的接收结果 * * @param channelCode 渠道编码 * @param text 结果内容 * @throws Throwable 处理失败时,抛出异常 */ void receiveSmsStatus(String channelCode, String text) throws Throwable; } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/sms/SmsSendServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.sms; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.exceptions.ExceptionUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.core.KeyValue; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.datapermission.core.annotation.DataPermission; import co.yixiang.yshop.module.system.framework.sms.core.client.SmsClient; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsSendRespDTO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsChannelDO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsTemplateDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.module.system.mq.message.sms.SmsSendMessage; import co.yixiang.yshop.module.system.mq.producer.sms.SmsProducer; import co.yixiang.yshop.module.system.service.member.MemberService; import co.yixiang.yshop.module.system.service.user.AdminUserService; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; /** * 短信发送 Service 发送的实现 * * @author yshop */ @Service @Slf4j public class SmsSendServiceImpl implements SmsSendService { @Resource private AdminUserService adminUserService; @Resource private MemberService memberService; @Resource private SmsChannelService smsChannelService; @Resource private SmsTemplateService smsTemplateService; @Resource private SmsLogService smsLogService; @Resource private SmsProducer smsProducer; @Override @DataPermission(enable = false) // 发送短信时,无需考虑数据权限 public Long sendSingleSmsToAdmin(String mobile, Long userId, String templateCode, Map templateParams) { // 如果 mobile 为空,则加载用户编号对应的手机号 if (StrUtil.isEmpty(mobile)) { AdminUserDO user = adminUserService.getUser(userId); if (user != null) { mobile = user.getMobile(); } } // 执行发送 return sendSingleSms(mobile, userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams); } @Override public Long sendSingleSmsToMember(String mobile, Long userId, String templateCode, Map templateParams) { // 如果 mobile 为空,则加载用户编号对应的手机号 if (StrUtil.isEmpty(mobile)) { mobile = memberService.getMemberUserMobile(userId); } // 执行发送 return sendSingleSms(mobile, userId, UserTypeEnum.MEMBER.getValue(), templateCode, templateParams); } @Override public Long sendSingleSms(String mobile, Long userId, Integer userType, String templateCode, Map templateParams) { // 校验短信模板是否合法 SmsTemplateDO template = validateSmsTemplate(templateCode); // 校验短信渠道是否合法 SmsChannelDO smsChannel = validateSmsChannel(template.getChannelId()); // 校验手机号码是否存在 mobile = validateMobile(mobile); // 构建有序的模板参数。为什么放在这个位置,是提前保证模板参数的正确性,而不是到了插入发送日志 List> newTemplateParams = buildTemplateParams(template, templateParams); // 创建发送日志。如果模板被禁用,则不发送短信,只记录日志 Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus()) && CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus()); String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams); Long sendLogId = smsLogService.createSmsLog(mobile, userId, userType, isSend, template, content, templateParams); // 发送 MQ 消息,异步执行发送短信 if (isSend) { smsProducer.sendSmsSendMessage(sendLogId, mobile, template.getChannelId(), template.getApiTemplateId(), newTemplateParams); } return sendLogId; } @VisibleForTesting SmsChannelDO validateSmsChannel(Long channelId) { // 获得短信模板。考虑到效率,从缓存中获取 SmsChannelDO channelDO = smsChannelService.getSmsChannel(channelId); // 短信模板不存在 if (channelDO == null) { throw exception(SMS_CHANNEL_NOT_EXISTS); } return channelDO; } @VisibleForTesting SmsTemplateDO validateSmsTemplate(String templateCode) { // 获得短信模板。考虑到效率,从缓存中获取 SmsTemplateDO template = smsTemplateService.getSmsTemplateByCodeFromCache(templateCode); // 短信模板不存在 if (template == null) { throw exception(SMS_SEND_TEMPLATE_NOT_EXISTS); } return template; } /** * 将参数模板,处理成有序的 KeyValue 数组 *

* 原因是,部分短信平台并不是使用 key 作为参数,而是数组下标,例如说 腾讯云 * * @param template 短信模板 * @param templateParams 原始参数 * @return 处理后的参数 */ @VisibleForTesting List> buildTemplateParams(SmsTemplateDO template, Map templateParams) { return template.getParams().stream().map(key -> { Object value = templateParams.get(key); if (value == null) { throw exception(SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS, key); } return new KeyValue<>(key, value); }).collect(Collectors.toList()); } @VisibleForTesting public String validateMobile(String mobile) { if (StrUtil.isEmpty(mobile)) { throw exception(SMS_SEND_MOBILE_NOT_EXISTS); } return mobile; } @Override public void doSendSms(SmsSendMessage message) { // 获得渠道对应的 SmsClient 客户端 SmsClient smsClient = smsChannelService.getSmsClient(message.getChannelId()); Assert.notNull(smsClient, "短信客户端({}) 不存在", message.getChannelId()); // 发送短信 try { SmsSendRespDTO sendResponse = smsClient.sendSms(message.getLogId(), message.getMobile(), message.getApiTemplateId(), message.getTemplateParams()); smsLogService.updateSmsSendResult(message.getLogId(), sendResponse.getSuccess(), sendResponse.getApiCode(), sendResponse.getApiMsg(), sendResponse.getApiRequestId(), sendResponse.getSerialNo()); } catch (Throwable ex) { log.error("[doSendSms][发送短信异常,日志编号({})]", message.getLogId(), ex); smsLogService.updateSmsSendResult(message.getLogId(), false, "EXCEPTION", ExceptionUtil.getRootCauseMessage(ex), null, null); } } @Override public void receiveSmsStatus(String channelCode, String text) throws Throwable { // 获得渠道对应的 SmsClient 客户端 SmsClient smsClient = smsChannelService.getSmsClient(channelCode); Assert.notNull(smsClient, "短信客户端({}) 不存在", channelCode); // 解析内容 List receiveResults = smsClient.parseSmsReceiveStatus(text); if (CollUtil.isEmpty(receiveResults)) { return; } // 更新短信日志的接收结果. 因为量一般不大,所以先使用 for 循环更新 receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(result.getLogId(), result.getSuccess(), result.getReceiveTime(), result.getErrorCode(), result.getErrorMsg())); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/sms/SmsTemplateService.java ================================================ package co.yixiang.yshop.module.system.service.sms; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.controller.admin.sms.vo.template.SmsTemplatePageReqVO; import co.yixiang.yshop.module.system.controller.admin.sms.vo.template.SmsTemplateSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsTemplateDO; import jakarta.validation.Valid; import java.util.Map; /** * 短信模板 Service 接口 * * @author zzf * @since 2021/1/25 9:24 */ public interface SmsTemplateService { /** * 创建短信模板 * * @param createReqVO 创建信息 * @return 编号 */ Long createSmsTemplate(@Valid SmsTemplateSaveReqVO createReqVO); /** * 更新短信模板 * * @param updateReqVO 更新信息 */ void updateSmsTemplate(@Valid SmsTemplateSaveReqVO updateReqVO); /** * 删除短信模板 * * @param id 编号 */ void deleteSmsTemplate(Long id); /** * 获得短信模板 * * @param id 编号 * @return 短信模板 */ SmsTemplateDO getSmsTemplate(Long id); /** * 获得短信模板,从缓存中 * * @param code 模板编码 * @return 短信模板 */ SmsTemplateDO getSmsTemplateByCodeFromCache(String code); /** * 获得短信模板分页 * * @param pageReqVO 分页查询 * @return 短信模板分页 */ PageResult getSmsTemplatePage(SmsTemplatePageReqVO pageReqVO); /** * 获得指定短信渠道下的短信模板数量 * * @param channelId 短信渠道编号 * @return 数量 */ Long getSmsTemplateCountByChannelId(Long channelId); /** * 格式化短信内容 * * @param content 短信模板的内容 * @param params 内容的参数 * @return 格式化后的内容 */ String formatSmsTemplateContent(String content, Map params); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/sms/SmsTemplateServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.sms; import cn.hutool.core.exceptions.ExceptionUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ReUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.framework.sms.core.client.SmsClient; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; import co.yixiang.yshop.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; import co.yixiang.yshop.module.system.controller.admin.sms.vo.template.SmsTemplatePageReqVO; import co.yixiang.yshop.module.system.controller.admin.sms.vo.template.SmsTemplateSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsChannelDO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsTemplateDO; import co.yixiang.yshop.module.system.dal.mysql.sms.SmsTemplateMapper; import co.yixiang.yshop.module.system.dal.redis.RedisKeyConstants; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.regex.Pattern; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; /** * 短信模板 Service 实现类 * * @author zzf * @since 2021/1/25 9:25 */ @Service @Slf4j public class SmsTemplateServiceImpl implements SmsTemplateService { /** * 正则表达式,匹配 {} 中的变量 */ private static final Pattern PATTERN_PARAMS = Pattern.compile("\\{(.*?)}"); @Resource private SmsTemplateMapper smsTemplateMapper; @Resource private SmsChannelService smsChannelService; @Override public Long createSmsTemplate(SmsTemplateSaveReqVO createReqVO) { // 校验短信渠道 SmsChannelDO channelDO = validateSmsChannel(createReqVO.getChannelId()); // 校验短信编码是否重复 validateSmsTemplateCodeDuplicate(null, createReqVO.getCode()); // 校验短信模板 validateApiTemplate(createReqVO.getChannelId(), createReqVO.getApiTemplateId()); // 插入 SmsTemplateDO template = BeanUtils.toBean(createReqVO, SmsTemplateDO.class); template.setParams(parseTemplateContentParams(template.getContent())); template.setChannelCode(channelDO.getCode()); smsTemplateMapper.insert(template); // 返回 return template.getId(); } @Override @CacheEvict(cacheNames = RedisKeyConstants.SMS_TEMPLATE, allEntries = true) // allEntries 清空所有缓存,因为可能修改到 code 字段,不好清理 public void updateSmsTemplate(SmsTemplateSaveReqVO updateReqVO) { // 校验存在 validateSmsTemplateExists(updateReqVO.getId()); // 校验短信渠道 SmsChannelDO channelDO = validateSmsChannel(updateReqVO.getChannelId()); // 校验短信编码是否重复 validateSmsTemplateCodeDuplicate(updateReqVO.getId(), updateReqVO.getCode()); // 校验短信模板 validateApiTemplate(updateReqVO.getChannelId(), updateReqVO.getApiTemplateId()); // 更新 SmsTemplateDO updateObj = BeanUtils.toBean(updateReqVO, SmsTemplateDO.class); updateObj.setParams(parseTemplateContentParams(updateObj.getContent())); updateObj.setChannelCode(channelDO.getCode()); smsTemplateMapper.updateById(updateObj); } @Override @CacheEvict(cacheNames = RedisKeyConstants.SMS_TEMPLATE, allEntries = true) // allEntries 清空所有缓存,因为 id 不是直接的缓存 code,不好清理 public void deleteSmsTemplate(Long id) { // 校验存在 validateSmsTemplateExists(id); // 更新 smsTemplateMapper.deleteById(id); } private void validateSmsTemplateExists(Long id) { if (smsTemplateMapper.selectById(id) == null) { throw exception(SMS_TEMPLATE_NOT_EXISTS); } } @Override public SmsTemplateDO getSmsTemplate(Long id) { return smsTemplateMapper.selectById(id); } @Override @Cacheable(cacheNames = RedisKeyConstants.SMS_TEMPLATE, key = "#code", unless = "#result == null") public SmsTemplateDO getSmsTemplateByCodeFromCache(String code) { return smsTemplateMapper.selectByCode(code); } @Override public PageResult getSmsTemplatePage(SmsTemplatePageReqVO pageReqVO) { return smsTemplateMapper.selectPage(pageReqVO); } @Override public Long getSmsTemplateCountByChannelId(Long channelId) { return smsTemplateMapper.selectCountByChannelId(channelId); } @VisibleForTesting public SmsChannelDO validateSmsChannel(Long channelId) { SmsChannelDO channelDO = smsChannelService.getSmsChannel(channelId); if (channelDO == null) { throw exception(SMS_CHANNEL_NOT_EXISTS); } if (CommonStatusEnum.isDisable(channelDO.getStatus())) { throw exception(SMS_CHANNEL_DISABLE); } return channelDO; } @VisibleForTesting public void validateSmsTemplateCodeDuplicate(Long id, String code) { SmsTemplateDO template = smsTemplateMapper.selectByCode(code); if (template == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的字典类型 if (id == null) { throw exception(SMS_TEMPLATE_CODE_DUPLICATE, code); } if (!template.getId().equals(id)) { throw exception(SMS_TEMPLATE_CODE_DUPLICATE, code); } } /** * 校验 API 短信平台的模板是否有效 * * @param channelId 渠道编号 * @param apiTemplateId API 模板编号 */ @VisibleForTesting void validateApiTemplate(Long channelId, String apiTemplateId) { // 获得短信模板 SmsClient smsClient = smsChannelService.getSmsClient(channelId); Assert.notNull(smsClient, String.format("短信客户端(%d) 不存在", channelId)); SmsTemplateRespDTO template; try { template = smsClient.getSmsTemplate(apiTemplateId); } catch (Throwable ex) { throw exception(SMS_TEMPLATE_API_ERROR, ExceptionUtil.getRootCauseMessage(ex)); } // 校验短信模版 if (template == null) { throw exception(SMS_TEMPLATE_API_NOT_FOUND); } if (Objects.equals(template.getAuditStatus(), SmsTemplateAuditStatusEnum.CHECKING.getStatus())) { throw exception(SMS_TEMPLATE_API_AUDIT_CHECKING); } if (Objects.equals(template.getAuditStatus(), SmsTemplateAuditStatusEnum.FAIL.getStatus())) { throw exception(SMS_TEMPLATE_API_AUDIT_FAIL, template.getAuditReason()); } Assert.equals(template.getAuditStatus(), SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), String.format("短信模板(%s) 审核状态(%d) 不正确", apiTemplateId, template.getAuditStatus())); } @Override public String formatSmsTemplateContent(String content, Map params) { return StrUtil.format(content, params); } @VisibleForTesting List parseTemplateContentParams(String content) { return ReUtil.findAllGroup1(PATTERN_PARAMS, content); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/social/SocialClientService.java ================================================ package co.yixiang.yshop.module.system.service.social; import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO; import co.yixiang.yshop.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.social.SocialClientDO; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import com.xingyuv.jushauth.model.AuthUser; import me.chanjar.weixin.common.bean.WxJsapiSignature; import jakarta.validation.Valid; /** * 社交应用 Service 接口 * * @author yshop */ public interface SocialClientService { /** * 获得社交平台的授权 URL * * @param socialType 社交平台的类型 {@link SocialTypeEnum} * @param userType 用户类型 * @param redirectUri 重定向 URL * @return 社交平台的授权 URL */ String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri); /** * 请求社交平台,获得授权的用户 * * @param socialType 社交平台的类型 * @param userType 用户类型 * @param code 授权码 * @param state 授权 state * @return 授权的用户 */ AuthUser getAuthUser(Integer socialType, Integer userType, String code, String state); // =================== 微信公众号独有 =================== /** * 创建微信公众号的 JS SDK 初始化所需的签名 * * @param userType 用户类型 * @param url 访问的 URL 地址 * @return 签名 */ WxJsapiSignature createWxMpJsapiSignature(Integer userType, String url); // =================== 微信小程序独有 =================== /** * 获得微信小程序的手机信息 * * @param userType 用户类型 * @param phoneCode 手机授权码 * @return 手机信息 */ WxMaPhoneNumberInfo getWxMaPhoneNumberInfo(Integer userType, String phoneCode); // =================== 客户端管理 =================== /** * 创建社交客户端 * * @param createReqVO 创建信息 * @return 编号 */ Long createSocialClient(@Valid SocialClientSaveReqVO createReqVO); /** * 更新社交客户端 * * @param updateReqVO 更新信息 */ void updateSocialClient(@Valid SocialClientSaveReqVO updateReqVO); /** * 删除社交客户端 * * @param id 编号 */ void deleteSocialClient(Long id); /** * 获得社交客户端 * * @param id 编号 * @return 社交客户端 */ SocialClientDO getSocialClient(Long id); /** * 获得社交客户端分页 * * @param pageReqVO 分页查询 * @return 社交客户端分页 */ PageResult getSocialClientPage(SocialClientPageReqVO pageReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/social/SocialClientServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.social; import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; import cn.binarywang.wx.miniapp.config.impl.WxMaRedisBetterConfigImpl; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ReflectUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.cache.CacheUtils; import co.yixiang.yshop.framework.common.util.http.HttpUtils; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO; import co.yixiang.yshop.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.social.SocialClientDO; import co.yixiang.yshop.module.system.dal.mysql.social.SocialClientMapper; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties; import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; import com.google.common.annotations.VisibleForTesting; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.xingyuv.jushauth.config.AuthConfig; import com.xingyuv.jushauth.model.AuthCallback; import com.xingyuv.jushauth.model.AuthResponse; import com.xingyuv.jushauth.model.AuthUser; import com.xingyuv.jushauth.request.AuthRequest; import com.xingyuv.jushauth.utils.AuthStateUtils; import com.xingyuv.justauth.AuthRequestFactory; import jakarta.annotation.Resource; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.bean.WxJsapiSignature; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl; import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import java.time.Duration; import java.util.Objects; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.util.json.JsonUtils.toJsonString; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; /** * 社交应用 Service 实现类 * * @author yshop */ @Service @Slf4j public class SocialClientServiceImpl implements SocialClientService { @Resource private AuthRequestFactory authRequestFactory; @Resource private WxMpService wxMpService; @Resource private WxMpProperties wxMpProperties; @Resource private StringRedisTemplate stringRedisTemplate; // WxMpService 需要使用到,所以在 Service 注入了它 /** * 缓存 WxMpService 对象 * * key:使用微信公众号的 appId + secret 拼接,即 {@link SocialClientDO} 的 clientId 和 clientSecret 属性。 * 为什么 key 使用这种格式?因为 {@link SocialClientDO} 在管理后台可以变更,通过这个 key 存储它的单例。 * * 为什么要做 WxMpService 缓存?因为 WxMpService 构建成本比较大,所以尽量保证它是单例。 */ private final LoadingCache wxMpServiceCache = CacheUtils.buildAsyncReloadingCache( Duration.ofSeconds(10L), new CacheLoader() { @Override public WxMpService load(String key) { String[] keys = key.split(":"); return buildWxMpService(keys[0], keys[1]); } }); @Resource private WxMaService wxMaService; @Resource private WxMaProperties wxMaProperties; /** * 缓存 WxMaService 对象 * * 说明同 {@link #wxMpServiceCache} 变量 */ private final LoadingCache wxMaServiceCache = CacheUtils.buildAsyncReloadingCache( Duration.ofSeconds(10L), new CacheLoader() { @Override public WxMaService load(String key) { String[] keys = key.split(":"); return buildWxMaService(keys[0], keys[1]); } }); @Resource private SocialClientMapper socialClientMapper; @Override public String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri) { // 获得对应的 AuthRequest 实现 AuthRequest authRequest = buildAuthRequest(socialType, userType); // 生成跳转地址 String authorizeUri = authRequest.authorize(AuthStateUtils.createState()); return HttpUtils.replaceUrlQuery(authorizeUri, "redirect_uri", redirectUri); } @Override public AuthUser getAuthUser(Integer socialType, Integer userType, String code, String state) { // 构建请求 AuthRequest authRequest = buildAuthRequest(socialType, userType); AuthCallback authCallback = AuthCallback.builder().code(code).state(state).build(); // 执行请求 AuthResponse authResponse = authRequest.login(authCallback); log.info("[getAuthUser][请求社交平台 type({}) request({}) response({})]", socialType, toJsonString(authCallback), toJsonString(authResponse)); if (!authResponse.ok()) { throw exception(SOCIAL_USER_AUTH_FAILURE, authResponse.getMsg()); } return (AuthUser) authResponse.getData(); } /** * 构建 AuthRequest 对象,支持多租户配置 * * @param socialType 社交类型 * @param userType 用户类型 * @return AuthRequest 对象 */ @VisibleForTesting AuthRequest buildAuthRequest(Integer socialType, Integer userType) { // 1. 先查找默认的配置项,从 application-*.yaml 中读取 AuthRequest request = authRequestFactory.get(SocialTypeEnum.valueOfType(socialType).getSource()); Assert.notNull(request, String.format("社交平台(%d) 不存在", socialType)); // 2. 查询 DB 的配置项,如果存在则进行覆盖 SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType(socialType, userType); if (client != null && Objects.equals(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { // 2.1 构造新的 AuthConfig 对象 AuthConfig authConfig = (AuthConfig) ReflectUtil.getFieldValue(request, "config"); AuthConfig newAuthConfig = ReflectUtil.newInstance(authConfig.getClass()); BeanUtil.copyProperties(authConfig, newAuthConfig); // 2.2 修改对应的 clientId + clientSecret 密钥 newAuthConfig.setClientId(client.getClientId()); newAuthConfig.setClientSecret(client.getClientSecret()); if (client.getAgentId() != null) { // 如果有 agentId 则修改 agentId newAuthConfig.setAgentId(client.getAgentId()); } // 2.3 设置会 request 里,进行后续使用 ReflectUtil.setFieldValue(request, "config", newAuthConfig); } return request; } // =================== 微信公众号独有 =================== @Override @SneakyThrows public WxJsapiSignature createWxMpJsapiSignature(Integer userType, String url) { WxMpService service = getWxMpService(userType); return service.createJsapiSignature(url); } /** * 获得 clientId + clientSecret 对应的 WxMpService 对象 * * @param userType 用户类型 * @return WxMpService 对象 */ @VisibleForTesting WxMpService getWxMpService(Integer userType) { // 第一步,查询 DB 的配置项,获得对应的 WxMpService 对象 SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType( SocialTypeEnum.WECHAT_MP.getType(), userType); if (client != null && Objects.equals(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { return wxMpServiceCache.getUnchecked(client.getClientId() + ":" + client.getClientSecret()); } // 第二步,不存在 DB 配置项,则使用 application-*.yaml 对应的 WxMpService 对象 return wxMpService; } /** * 创建 clientId + clientSecret 对应的 WxMpService 对象 * * @param clientId 微信公众号 appId * @param clientSecret 微信公众号 secret * @return WxMpService 对象 */ public WxMpService buildWxMpService(String clientId, String clientSecret) { // 第一步,创建 WxMpRedisConfigImpl 对象 WxMpRedisConfigImpl configStorage = new WxMpRedisConfigImpl( new RedisTemplateWxRedisOps(stringRedisTemplate), wxMpProperties.getConfigStorage().getKeyPrefix()); configStorage.setAppId(clientId); configStorage.setSecret(clientSecret); // 第二步,创建 WxMpService 对象 WxMpService service = new WxMpServiceImpl(); service.setWxMpConfigStorage(configStorage); return service; } // =================== 微信小程序独有 =================== @Override public WxMaPhoneNumberInfo getWxMaPhoneNumberInfo(Integer userType, String phoneCode) { WxMaService service = getWxMaService(userType); try { return service.getUserService().getPhoneNoInfo(phoneCode); } catch (WxErrorException e) { log.error("[getPhoneNoInfo][userType({}) phoneCode({}) 获得手机号失败]", userType, phoneCode, e); throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_PHONE_CODE_ERROR); } } /** * 获得 clientId + clientSecret 对应的 WxMpService 对象 * * @param userType 用户类型 * @return WxMpService 对象 */ @VisibleForTesting WxMaService getWxMaService(Integer userType) { // 第一步,查询 DB 的配置项,获得对应的 WxMaService 对象 SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType( SocialTypeEnum.WECHAT_MINI_APP.getType(), userType); if (client != null && Objects.equals(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { return wxMaServiceCache.getUnchecked(client.getClientId() + ":" + client.getClientSecret()); } // 第二步,不存在 DB 配置项,则使用 application-*.yaml 对应的 WxMaService 对象 return wxMaService; } /** * 创建 clientId + clientSecret 对应的 WxMaService 对象 * * @param clientId 微信小程序 appId * @param clientSecret 微信小程序 secret * @return WxMaService 对象 */ private WxMaService buildWxMaService(String clientId, String clientSecret) { // 第一步,创建 WxMaRedisBetterConfigImpl 对象 WxMaRedisBetterConfigImpl configStorage = new WxMaRedisBetterConfigImpl( new RedisTemplateWxRedisOps(stringRedisTemplate), wxMaProperties.getConfigStorage().getKeyPrefix()); configStorage.setAppid(clientId); configStorage.setSecret(clientSecret); // 第二步,创建 WxMpService 对象 WxMaService service = new WxMaServiceImpl(); service.setWxMaConfig(configStorage); return service; } // =================== 客户端管理 =================== @Override public Long createSocialClient(SocialClientSaveReqVO createReqVO) { // 校验重复 validateSocialClientUnique(null, createReqVO.getUserType(), createReqVO.getSocialType()); // 插入 SocialClientDO client = BeanUtils.toBean(createReqVO, SocialClientDO.class); socialClientMapper.insert(client); return client.getId(); } @Override public void updateSocialClient(SocialClientSaveReqVO updateReqVO) { // 校验存在 validateSocialClientExists(updateReqVO.getId()); // 校验重复 validateSocialClientUnique(updateReqVO.getId(), updateReqVO.getUserType(), updateReqVO.getSocialType()); // 更新 SocialClientDO updateObj = BeanUtils.toBean(updateReqVO, SocialClientDO.class); socialClientMapper.updateById(updateObj); } @Override public void deleteSocialClient(Long id) { // 校验存在 validateSocialClientExists(id); // 删除 socialClientMapper.deleteById(id); } private void validateSocialClientExists(Long id) { if (socialClientMapper.selectById(id) == null) { throw exception(SOCIAL_CLIENT_NOT_EXISTS); } } /** * 校验社交应用是否重复,需要保证 userType + socialType 唯一 * * 原因是,不同端(userType)选择某个社交登录(socialType)时,需要通过 {@link #buildAuthRequest(Integer, Integer)} 构建对应的请求 * * @param id 编号 * @param userType 用户类型 * @param socialType 社交类型 */ private void validateSocialClientUnique(Long id, Integer userType, Integer socialType) { SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType( socialType, userType); if (client == null) { return; } if (id == null // 新增时,说明重复 || ObjUtil.notEqual(id, client.getId())) { // 更新时,如果 id 不一致,说明重复 throw exception(SOCIAL_CLIENT_UNIQUE); } } @Override public SocialClientDO getSocialClient(Long id) { return socialClientMapper.selectById(id); } @Override public PageResult getSocialClientPage(SocialClientPageReqVO pageReqVO) { return socialClientMapper.selectPage(pageReqVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/social/SocialUserService.java ================================================ package co.yixiang.yshop.module.system.service.social; import co.yixiang.yshop.framework.common.exception.ServiceException; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.api.social.dto.SocialUserBindReqDTO; import co.yixiang.yshop.module.system.api.social.dto.SocialUserRespDTO; import co.yixiang.yshop.module.system.controller.admin.socail.vo.user.SocialUserPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.social.SocialUserDO; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import jakarta.validation.Valid; import java.util.List; /** * 社交用户 Service 接口,例如说社交平台的授权登录 * * @author yshop */ public interface SocialUserService { /** * 获得指定用户的社交用户列表 * * @param userId 用户编号 * @param userType 用户类型 * @return 社交用户列表 */ List getSocialUserList(Long userId, Integer userType); /** * 绑定社交用户 * * @param reqDTO 绑定信息 * @return 社交用户 openid */ String bindSocialUser(@Valid SocialUserBindReqDTO reqDTO); /** * 取消绑定社交用户 * * @param userId 用户编号 * @param userType 全局用户类型 * @param socialType 社交平台的类型 {@link SocialTypeEnum} * @param openid 社交平台的 openid */ void unbindSocialUser(Long userId, Integer userType, Integer socialType, String openid); /** * 获得社交用户,基于 userId * * @param userType 用户类型 * @param userId 用户编号 * @param socialType 社交平台的类型 * @return 社交用户 */ SocialUserRespDTO getSocialUserByUserId(Integer userType, Long userId, Integer socialType); /** * 获得社交用户 * * 在认证信息不正确的情况下,也会抛出 {@link ServiceException} 业务异常 * * @param userType 用户类型 * @param socialType 社交平台的类型 * @param code 授权码 * @param state state * @return 社交用户 */ SocialUserRespDTO getSocialUserByCode(Integer userType, Integer socialType, String code, String state); // ==================== 社交用户 CRUD ==================== /** * 获得社交用户 * * @param id 编号 * @return 社交用户 */ SocialUserDO getSocialUser(Long id); /** * 获得社交用户分页 * * @param pageReqVO 分页查询 * @return 社交用户分页 */ PageResult getSocialUserPage(SocialUserPageReqVO pageReqVO); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/social/SocialUserServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.social; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import co.yixiang.yshop.framework.common.exception.ServiceException; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.api.social.dto.SocialUserBindReqDTO; import co.yixiang.yshop.module.system.api.social.dto.SocialUserRespDTO; import co.yixiang.yshop.module.system.controller.admin.socail.vo.user.SocialUserPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.social.SocialUserBindDO; import co.yixiang.yshop.module.system.dal.dataobject.social.SocialUserDO; import co.yixiang.yshop.module.system.dal.mysql.social.SocialUserBindMapper; import co.yixiang.yshop.module.system.dal.mysql.social.SocialUserMapper; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import com.xingyuv.jushauth.model.AuthUser; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import jakarta.validation.constraints.NotNull; import java.util.Collections; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertSet; import static co.yixiang.yshop.framework.common.util.json.JsonUtils.toJsonString; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.SOCIAL_USER_NOT_FOUND; /** * 社交用户 Service 实现类 * * @author yshop */ @Service @Validated @Slf4j public class SocialUserServiceImpl implements SocialUserService { @Resource private SocialUserBindMapper socialUserBindMapper; @Resource private SocialUserMapper socialUserMapper; @Resource private SocialClientService socialClientService; @Override public List getSocialUserList(Long userId, Integer userType) { // 获得绑定 List socialUserBinds = socialUserBindMapper.selectListByUserIdAndUserType(userId, userType); if (CollUtil.isEmpty(socialUserBinds)) { return Collections.emptyList(); } // 获得社交用户 return socialUserMapper.selectBatchIds(convertSet(socialUserBinds, SocialUserBindDO::getSocialUserId)); } @Override @Transactional(rollbackFor = Exception.class) public String bindSocialUser(SocialUserBindReqDTO reqDTO) { // 获得社交用户 SocialUserDO socialUser = authSocialUser(reqDTO.getSocialType(), reqDTO.getUserType(), reqDTO.getCode(), reqDTO.getState()); Assert.notNull(socialUser, "社交用户不能为空"); // 社交用户可能之前绑定过别的用户,需要进行解绑 socialUserBindMapper.deleteByUserTypeAndSocialUserId(reqDTO.getUserType(), socialUser.getId()); // 用户可能之前已经绑定过该社交类型,需要进行解绑 socialUserBindMapper.deleteByUserTypeAndUserIdAndSocialType(reqDTO.getUserType(), reqDTO.getUserId(), socialUser.getType()); // 绑定当前登录的社交用户 SocialUserBindDO socialUserBind = SocialUserBindDO.builder() .userId(reqDTO.getUserId()).userType(reqDTO.getUserType()) .socialUserId(socialUser.getId()).socialType(socialUser.getType()).build(); socialUserBindMapper.insert(socialUserBind); return socialUser.getOpenid(); } @Override public void unbindSocialUser(Long userId, Integer userType, Integer socialType, String openid) { // 获得 openid 对应的 SocialUserDO 社交用户 SocialUserDO socialUser = socialUserMapper.selectByTypeAndOpenid(socialType, openid); if (socialUser == null) { throw exception(SOCIAL_USER_NOT_FOUND); } // 获得对应的社交绑定关系 socialUserBindMapper.deleteByUserTypeAndUserIdAndSocialType(userType, userId, socialUser.getType()); } @Override public SocialUserRespDTO getSocialUserByUserId(Integer userType, Long userId, Integer socialType) { // 获得绑定用户 SocialUserBindDO socialUserBind = socialUserBindMapper.selectByUserIdAndUserTypeAndSocialType(userId, userType, socialType); if (socialUserBind == null) { return null; } // 获得社交用户 SocialUserDO socialUser = socialUserMapper.selectById(socialUserBind.getSocialUserId()); Assert.notNull(socialUser, "社交用户不能为空"); return new SocialUserRespDTO(socialUser.getOpenid(), socialUser.getNickname(), socialUser.getAvatar(), socialUserBind.getUserId()); } @Override public SocialUserRespDTO getSocialUserByCode(Integer userType, Integer socialType, String code, String state) { // 获得社交用户 SocialUserDO socialUser = authSocialUser(socialType, userType, code, state); Assert.notNull(socialUser, "社交用户不能为空"); // 获得绑定用户 SocialUserBindDO socialUserBind = socialUserBindMapper.selectByUserTypeAndSocialUserId(userType, socialUser.getId()); return new SocialUserRespDTO(socialUser.getOpenid(), socialUser.getNickname(), socialUser.getAvatar(), socialUserBind != null ? socialUserBind.getUserId() : null); } /** * 授权获得对应的社交用户 * 如果授权失败,则会抛出 {@link ServiceException} 异常 * * @param socialType 社交平台的类型 {@link SocialTypeEnum} * @param userType 用户类型 * @param code 授权码 * @param state state * @return 授权用户 */ @NotNull public SocialUserDO authSocialUser(Integer socialType, Integer userType, String code, String state) { // 优先从 DB 中获取,因为 code 有且可以使用一次。 // 在社交登录时,当未绑定 User 时,需要绑定登录,此时需要 code 使用两次 SocialUserDO socialUser = socialUserMapper.selectByTypeAndCodeAnState(socialType, code, state); if (socialUser != null) { return socialUser; } // 请求获取 AuthUser authUser = socialClientService.getAuthUser(socialType, userType, code, state); Assert.notNull(authUser, "三方用户不能为空"); // 保存到 DB 中 socialUser = socialUserMapper.selectByTypeAndOpenid(socialType, authUser.getUuid()); if (socialUser == null) { socialUser = new SocialUserDO(); } socialUser.setType(socialType).setCode(code).setState(state) // 需要保存 code + state 字段,保证后续可查询 .setOpenid(authUser.getUuid()).setToken(authUser.getToken().getAccessToken()).setRawTokenInfo((toJsonString(authUser.getToken()))) .setNickname(authUser.getNickname()).setAvatar(authUser.getAvatar()).setRawUserInfo(toJsonString(authUser.getRawUserInfo())); if (socialUser.getId() == null) { socialUserMapper.insert(socialUser); } else { socialUserMapper.updateById(socialUser); } return socialUser; } // ==================== 社交用户 CRUD ==================== @Override public SocialUserDO getSocialUser(Long id) { return socialUserMapper.selectById(id); } @Override public PageResult getSocialUserPage(SocialUserPageReqVO pageReqVO) { return socialUserMapper.selectPage(pageReqVO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/tenant/TenantPackageService.java ================================================ package co.yixiang.yshop.module.system.service.tenant; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.controller.admin.tenant.vo.packages.TenantPackagePageReqVO; import co.yixiang.yshop.module.system.controller.admin.tenant.vo.packages.TenantPackageSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.tenant.TenantPackageDO; import jakarta.validation.Valid; import java.util.List; /** * 租户套餐 Service 接口 * * @author yshop */ public interface TenantPackageService { /** * 创建租户套餐 * * @param createReqVO 创建信息 * @return 编号 */ Long createTenantPackage(@Valid TenantPackageSaveReqVO createReqVO); /** * 更新租户套餐 * * @param updateReqVO 更新信息 */ void updateTenantPackage(@Valid TenantPackageSaveReqVO updateReqVO); /** * 删除租户套餐 * * @param id 编号 */ void deleteTenantPackage(Long id); /** * 获得租户套餐 * * @param id 编号 * @return 租户套餐 */ TenantPackageDO getTenantPackage(Long id); /** * 获得租户套餐分页 * * @param pageReqVO 分页查询 * @return 租户套餐分页 */ PageResult getTenantPackagePage(TenantPackagePageReqVO pageReqVO); /** * 校验租户套餐 * * @param id 编号 * @return 租户套餐 */ TenantPackageDO validTenantPackage(Long id); /** * 获得指定状态的租户套餐列表 * * @param status 状态 * @return 租户套餐 */ List getTenantPackageListByStatus(Integer status); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/tenant/TenantPackageServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.tenant; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.controller.admin.tenant.vo.packages.TenantPackagePageReqVO; import co.yixiang.yshop.module.system.controller.admin.tenant.vo.packages.TenantPackageSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.tenant.TenantDO; import co.yixiang.yshop.module.system.dal.dataobject.tenant.TenantPackageDO; import co.yixiang.yshop.module.system.dal.mysql.tenant.TenantPackageMapper; import com.baomidou.dynamic.datasource.annotation.DSTransactional; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.List; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; /** * 租户套餐 Service 实现类 * * @author yshop */ @Service @Validated public class TenantPackageServiceImpl implements TenantPackageService { @Resource private TenantPackageMapper tenantPackageMapper; @Resource @Lazy // 避免循环依赖的报错 private TenantService tenantService; @Override public Long createTenantPackage(TenantPackageSaveReqVO createReqVO) { // 插入 TenantPackageDO tenantPackage = BeanUtils.toBean(createReqVO, TenantPackageDO.class); tenantPackageMapper.insert(tenantPackage); // 返回 return tenantPackage.getId(); } @Override @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换 public void updateTenantPackage(TenantPackageSaveReqVO updateReqVO) { // 校验存在 TenantPackageDO tenantPackage = validateTenantPackageExists(updateReqVO.getId()); // 更新 TenantPackageDO updateObj = BeanUtils.toBean(updateReqVO, TenantPackageDO.class); tenantPackageMapper.updateById(updateObj); // 如果菜单发生变化,则修改每个租户的菜单 if (!CollUtil.isEqualList(tenantPackage.getMenuIds(), updateReqVO.getMenuIds())) { List tenants = tenantService.getTenantListByPackageId(tenantPackage.getId()); tenants.forEach(tenant -> tenantService.updateTenantRoleMenu(tenant.getId(), updateReqVO.getMenuIds())); } } @Override public void deleteTenantPackage(Long id) { // 校验存在 validateTenantPackageExists(id); // 校验正在使用 validateTenantUsed(id); // 删除 tenantPackageMapper.deleteById(id); } private TenantPackageDO validateTenantPackageExists(Long id) { TenantPackageDO tenantPackage = tenantPackageMapper.selectById(id); if (tenantPackage == null) { throw exception(TENANT_PACKAGE_NOT_EXISTS); } return tenantPackage; } private void validateTenantUsed(Long id) { if (tenantService.getTenantCountByPackageId(id) > 0) { throw exception(TENANT_PACKAGE_USED); } } @Override public TenantPackageDO getTenantPackage(Long id) { return tenantPackageMapper.selectById(id); } @Override public PageResult getTenantPackagePage(TenantPackagePageReqVO pageReqVO) { return tenantPackageMapper.selectPage(pageReqVO); } @Override public TenantPackageDO validTenantPackage(Long id) { TenantPackageDO tenantPackage = tenantPackageMapper.selectById(id); if (tenantPackage == null) { throw exception(TENANT_PACKAGE_NOT_EXISTS); } if (tenantPackage.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { throw exception(TENANT_PACKAGE_DISABLE, tenantPackage.getName()); } return tenantPackage; } @Override public List getTenantPackageListByStatus(Integer status) { return tenantPackageMapper.selectListByStatus(status); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/tenant/TenantService.java ================================================ package co.yixiang.yshop.module.system.service.tenant; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import co.yixiang.yshop.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; import co.yixiang.yshop.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.tenant.TenantDO; import co.yixiang.yshop.module.system.service.tenant.handler.TenantInfoHandler; import co.yixiang.yshop.module.system.service.tenant.handler.TenantMenuHandler; import jakarta.validation.Valid; import java.util.List; import java.util.Set; /** * 租户 Service 接口 * * @author yshop */ public interface TenantService { /** * 创建租户 * * @param createReqVO 创建信息 * @return 编号 */ Long createTenant(@Valid TenantSaveReqVO createReqVO); /** * 更新租户 * * @param updateReqVO 更新信息 */ void updateTenant(@Valid TenantSaveReqVO updateReqVO); /** * 更新租户的角色菜单 * * @param tenantId 租户编号 * @param menuIds 菜单编号数组 */ void updateTenantRoleMenu(Long tenantId, Set menuIds); /** * 删除租户 * * @param id 编号 */ void deleteTenant(Long id); /** * 获得租户 * * @param id 编号 * @return 租户 */ TenantDO getTenant(Long id); /** * 获得租户分页 * * @param pageReqVO 分页查询 * @return 租户分页 */ PageResult getTenantPage(TenantPageReqVO pageReqVO); /** * 获得名字对应的租户 * * @param name 租户名 * @return 租户 */ TenantDO getTenantByName(String name); /** * 获得域名对应的租户 * * @param website 域名 * @return 租户 */ TenantDO getTenantByWebsite(String website); /** * 获得使用指定套餐的租户数量 * * @param packageId 租户套餐编号 * @return 租户数量 */ Long getTenantCountByPackageId(Long packageId); /** * 获得使用指定套餐的租户数组 * * @param packageId 租户套餐编号 * @return 租户数组 */ List getTenantListByPackageId(Long packageId); /** * 进行租户的信息处理逻辑 * 其中,租户编号从 {@link TenantContextHolder} 上下文中获取 * * @param handler 处理器 */ void handleTenantInfo(TenantInfoHandler handler); /** * 进行租户的菜单处理逻辑 * 其中,租户编号从 {@link TenantContextHolder} 上下文中获取 * * @param handler 处理器 */ void handleTenantMenu(TenantMenuHandler handler); /** * 获得所有租户 * * @return 租户编号数组 */ List getTenantIdList(); /** * 校验租户是否合法 * * @param id 租户编号 */ void validTenant(Long id); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/tenant/TenantServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.tenant; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.framework.common.util.date.DateUtils; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.tenant.config.TenantProperties; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import co.yixiang.yshop.framework.tenant.core.util.TenantUtils; import co.yixiang.yshop.module.system.controller.admin.permission.vo.role.RoleSaveReqVO; import co.yixiang.yshop.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; import co.yixiang.yshop.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO; import co.yixiang.yshop.module.system.convert.tenant.TenantConvert; import co.yixiang.yshop.module.system.dal.dataobject.permission.MenuDO; import co.yixiang.yshop.module.system.dal.dataobject.permission.RoleDO; import co.yixiang.yshop.module.system.dal.dataobject.tenant.TenantDO; import co.yixiang.yshop.module.system.dal.dataobject.tenant.TenantPackageDO; import co.yixiang.yshop.module.system.dal.mysql.tenant.TenantMapper; import co.yixiang.yshop.module.system.enums.permission.RoleCodeEnum; import co.yixiang.yshop.module.system.enums.permission.RoleTypeEnum; import co.yixiang.yshop.module.system.service.permission.MenuService; import co.yixiang.yshop.module.system.service.permission.PermissionService; import co.yixiang.yshop.module.system.service.permission.RoleService; import co.yixiang.yshop.module.system.service.tenant.handler.TenantInfoHandler; import co.yixiang.yshop.module.system.service.tenant.handler.TenantMenuHandler; import co.yixiang.yshop.module.system.service.user.AdminUserService; import com.baomidou.dynamic.datasource.annotation.DSTransactional; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.List; import java.util.Objects; import java.util.Set; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; import static java.util.Collections.singleton; /** * 租户 Service 实现类 * * @author yshop */ @Service @Validated @Slf4j public class TenantServiceImpl implements TenantService { @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") @Autowired(required = false) // 由于 yshop.tenant.enable 配置项,可以关闭多租户的功能,所以这里只能不强制注入 private TenantProperties tenantProperties; @Resource private TenantMapper tenantMapper; @Resource private TenantPackageService tenantPackageService; @Resource @Lazy // 延迟,避免循环依赖报错 private AdminUserService userService; @Resource private RoleService roleService; @Resource private MenuService menuService; @Resource private PermissionService permissionService; @Override public List getTenantIdList() { List tenants = tenantMapper.selectList(); return CollectionUtils.convertList(tenants, TenantDO::getId); } @Override public void validTenant(Long id) { TenantDO tenant = getTenant(id); if (tenant == null) { throw exception(TENANT_NOT_EXISTS); } if (tenant.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { throw exception(TENANT_DISABLE, tenant.getName()); } if (DateUtils.isExpired(tenant.getExpireTime())) { throw exception(TENANT_EXPIRE, tenant.getName()); } } @Override @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换 public Long createTenant(TenantSaveReqVO createReqVO) { // 校验租户名称是否重复 validTenantNameDuplicate(createReqVO.getName(), null); // 校验租户域名是否重复 validTenantWebsiteDuplicate(createReqVO.getWebsite(), null); // 校验套餐被禁用 TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(createReqVO.getPackageId()); // 创建租户 TenantDO tenant = BeanUtils.toBean(createReqVO, TenantDO.class); tenantMapper.insert(tenant); // 创建租户的管理员 TenantUtils.execute(tenant.getId(), () -> { // 创建角色 Long roleId = createRole(tenantPackage); // 创建用户,并分配角色 Long userId = createUser(roleId, createReqVO); // 修改租户的管理员 tenantMapper.updateById(new TenantDO().setId(tenant.getId()).setContactUserId(userId)); }); return tenant.getId(); } private Long createUser(Long roleId, TenantSaveReqVO createReqVO) { // 创建用户 Long userId = userService.createUser(TenantConvert.INSTANCE.convert02(createReqVO)); // 分配角色 permissionService.assignUserRole(userId, singleton(roleId)); return userId; } private Long createRole(TenantPackageDO tenantPackage) { // 创建角色 RoleSaveReqVO reqVO = new RoleSaveReqVO(); reqVO.setName(RoleCodeEnum.TENANT_ADMIN.getName()).setCode(RoleCodeEnum.TENANT_ADMIN.getCode()) .setSort(0).setRemark("系统自动生成"); Long roleId = roleService.createRole(reqVO, RoleTypeEnum.SYSTEM.getType()); // 分配权限 permissionService.assignRoleMenu(roleId, tenantPackage.getMenuIds()); return roleId; } @Override @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换 public void updateTenant(TenantSaveReqVO updateReqVO) { // 校验存在 TenantDO tenant = validateUpdateTenant(updateReqVO.getId()); // 校验租户名称是否重复 validTenantNameDuplicate(updateReqVO.getName(), updateReqVO.getId()); // 校验租户域名是否重复 validTenantWebsiteDuplicate(updateReqVO.getWebsite(), updateReqVO.getId()); // 校验套餐被禁用 TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(updateReqVO.getPackageId()); // 更新租户 TenantDO updateObj = BeanUtils.toBean(updateReqVO, TenantDO.class); tenantMapper.updateById(updateObj); // 如果套餐发生变化,则修改其角色的权限 if (ObjectUtil.notEqual(tenant.getPackageId(), updateReqVO.getPackageId())) { updateTenantRoleMenu(tenant.getId(), tenantPackage.getMenuIds()); } } private void validTenantNameDuplicate(String name, Long id) { TenantDO tenant = tenantMapper.selectByName(name); if (tenant == null) { return; } // 如果 id 为空,说明不用比较是否为相同名字的租户 if (id == null) { throw exception(TENANT_NAME_DUPLICATE, name); } if (!tenant.getId().equals(id)) { throw exception(TENANT_NAME_DUPLICATE, name); } } private void validTenantWebsiteDuplicate(String website, Long id) { if (StrUtil.isEmpty(website)) { return; } TenantDO tenant = tenantMapper.selectByWebsite(website); if (tenant == null) { return; } // 如果 id 为空,说明不用比较是否为相同名字的租户 if (id == null) { throw exception(TENANT_WEBSITE_DUPLICATE, website); } if (!tenant.getId().equals(id)) { throw exception(TENANT_WEBSITE_DUPLICATE, website); } } @Override @DSTransactional public void updateTenantRoleMenu(Long tenantId, Set menuIds) { TenantUtils.execute(tenantId, () -> { // 获得所有角色 List roles = roleService.getRoleList(); roles.forEach(role -> Assert.isTrue(tenantId.equals(role.getTenantId()), "角色({}/{}) 租户不匹配", role.getId(), role.getTenantId(), tenantId)); // 兜底校验 // 重新分配每个角色的权限 roles.forEach(role -> { // 如果是租户管理员,重新分配其权限为租户套餐的权限 if (Objects.equals(role.getCode(), RoleCodeEnum.TENANT_ADMIN.getCode())) { permissionService.assignRoleMenu(role.getId(), menuIds); log.info("[updateTenantRoleMenu][租户管理员({}/{}) 的权限修改为({})]", role.getId(), role.getTenantId(), menuIds); return; } // 如果是其他角色,则去掉超过套餐的权限 Set roleMenuIds = permissionService.getRoleMenuListByRoleId(role.getId()); roleMenuIds = CollUtil.intersectionDistinct(roleMenuIds, menuIds); permissionService.assignRoleMenu(role.getId(), roleMenuIds); log.info("[updateTenantRoleMenu][角色({}/{}) 的权限修改为({})]", role.getId(), role.getTenantId(), roleMenuIds); }); }); } @Override public void deleteTenant(Long id) { // 校验存在 validateUpdateTenant(id); // 删除 tenantMapper.deleteById(id); } private TenantDO validateUpdateTenant(Long id) { TenantDO tenant = tenantMapper.selectById(id); if (tenant == null) { throw exception(TENANT_NOT_EXISTS); } // 内置租户,不允许删除 if (isSystemTenant(tenant)) { throw exception(TENANT_CAN_NOT_UPDATE_SYSTEM); } return tenant; } @Override public TenantDO getTenant(Long id) { return tenantMapper.selectById(id); } @Override public PageResult getTenantPage(TenantPageReqVO pageReqVO) { return tenantMapper.selectPage(pageReqVO); } @Override public TenantDO getTenantByName(String name) { return tenantMapper.selectByName(name); } @Override public TenantDO getTenantByWebsite(String website) { return tenantMapper.selectByWebsite(website); } @Override public Long getTenantCountByPackageId(Long packageId) { return tenantMapper.selectCountByPackageId(packageId); } @Override public List getTenantListByPackageId(Long packageId) { return tenantMapper.selectListByPackageId(packageId); } @Override public void handleTenantInfo(TenantInfoHandler handler) { // 如果禁用,则不执行逻辑 if (isTenantDisable()) { return; } // 获得租户 TenantDO tenant = getTenant(TenantContextHolder.getRequiredTenantId()); // 执行处理器 handler.handle(tenant); } @Override public void handleTenantMenu(TenantMenuHandler handler) { // 如果禁用,则不执行逻辑 if (isTenantDisable()) { return; } // 获得租户,然后获得菜单 TenantDO tenant = getTenant(TenantContextHolder.getRequiredTenantId()); Set menuIds; if (isSystemTenant(tenant)) { // 系统租户,菜单是全量的 menuIds = CollectionUtils.convertSet(menuService.getMenuList(), MenuDO::getId); } else { menuIds = tenantPackageService.getTenantPackage(tenant.getPackageId()).getMenuIds(); } // 执行处理器 handler.handle(menuIds); } private static boolean isSystemTenant(TenantDO tenant) { return Objects.equals(tenant.getPackageId(), TenantDO.PACKAGE_ID_SYSTEM); } private boolean isTenantDisable() { return tenantProperties == null || Boolean.FALSE.equals(tenantProperties.getEnable()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/tenant/handler/TenantInfoHandler.java ================================================ package co.yixiang.yshop.module.system.service.tenant.handler; import co.yixiang.yshop.module.system.dal.dataobject.tenant.TenantDO; /** * 租户信息处理 * 目的:尽量减少租户逻辑耦合到系统中 * * @author yshop */ public interface TenantInfoHandler { /** * 基于传入的租户信息,进行相关逻辑的执行 * 例如说,创建用户时,超过最大账户配额 * * @param tenant 租户信息 */ void handle(TenantDO tenant); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/tenant/handler/TenantMenuHandler.java ================================================ package co.yixiang.yshop.module.system.service.tenant.handler; import java.util.Set; /** * 租户菜单处理 * 目的:尽量减少租户逻辑耦合到系统中 * * @author yshop */ public interface TenantMenuHandler { /** * 基于传入的租户菜单【全】列表,进行相关逻辑的执行 * 例如说,返回可分配菜单的时候,可以移除多余的 * * @param menuIds 菜单列表 */ void handle(Set menuIds); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/user/AdminUserService.java ================================================ package co.yixiang.yshop.module.system.service.user; import cn.hutool.core.collection.CollUtil; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; import co.yixiang.yshop.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; import co.yixiang.yshop.module.system.controller.admin.user.vo.user.*; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import jakarta.validation.Valid; import java.io.InputStream; import java.util.*; /** * 后台用户 Service 接口 * * @author yshop */ public interface AdminUserService { /** * 创建用户 * * @param createReqVO 用户信息 * @return 用户编号 */ Long createUser(@Valid UserSaveReqVO createReqVO); /** * 修改用户 * * @param updateReqVO 用户信息 */ void updateUser(@Valid UserSaveReqVO updateReqVO); /** * 更新用户的最后登陆信息 * * @param id 用户编号 * @param loginIp 登陆 IP */ void updateUserLogin(Long id, String loginIp); /** * 修改用户个人信息 * * @param id 用户编号 * @param reqVO 用户个人信息 */ void updateUserProfile(Long id, @Valid UserProfileUpdateReqVO reqVO); /** * 修改用户个人密码 * * @param id 用户编号 * @param reqVO 更新用户个人密码 */ void updateUserPassword(Long id, @Valid UserProfileUpdatePasswordReqVO reqVO); /** * 更新用户头像 * * @param id 用户 id * @param avatarFile 头像文件 */ String updateUserAvatar(Long id, InputStream avatarFile) throws Exception; /** * 修改密码 * * @param id 用户编号 * @param password 密码 */ void updateUserPassword(Long id, String password); /** * 修改状态 * * @param id 用户编号 * @param status 状态 */ void updateUserStatus(Long id, Integer status); /** * 删除用户 * * @param id 用户编号 */ void deleteUser(Long id); /** * 通过用户名查询用户 * * @param username 用户名 * @return 用户对象信息 */ AdminUserDO getUserByUsername(String username); /** * 通过手机号获取用户 * * @param mobile 手机号 * @return 用户对象信息 */ AdminUserDO getUserByMobile(String mobile); /** * 获得用户分页列表 * * @param reqVO 分页条件 * @return 分页列表 */ PageResult getUserPage(UserPageReqVO reqVO); /** * 通过用户 ID 查询用户 * * @param id 用户ID * @return 用户对象信息 */ AdminUserDO getUser(Long id); /** * 获得指定部门的用户数组 * * @param deptIds 部门数组 * @return 用户数组 */ List getUserListByDeptIds(Collection deptIds); /** * 获得指定岗位的用户数组 * * @param postIds 岗位数组 * @return 用户数组 */ List getUserListByPostIds(Collection postIds); /** * 获得用户列表 * * @param ids 用户编号数组 * @return 用户列表 */ List getUserList(Collection ids); /** * 校验用户们是否有效。如下情况,视为无效: * 1. 用户编号不存在 * 2. 用户被禁用 * * @param ids 用户编号数组 */ void validateUserList(Collection ids); /** * 获得用户 Map * * @param ids 用户编号数组 * @return 用户 Map */ default Map getUserMap(Collection ids) { if (CollUtil.isEmpty(ids)) { return new HashMap<>(); } return CollectionUtils.convertMap(getUserList(ids), AdminUserDO::getId); } /** * 获得用户列表,基于昵称模糊匹配 * * @param nickname 昵称 * @return 用户列表 */ List getUserListByNickname(String nickname); /** * 批量导入用户 * * @param importUsers 导入用户列表 * @param isUpdateSupport 是否支持更新 * @return 导入结果 */ UserImportRespVO importUserList(List importUsers, boolean isUpdateSupport); /** * 获得指定状态的用户们 * * @param status 状态 * @return 用户们 */ List getUserListByStatus(Integer status); /** * 判断密码是否匹配 * * @param rawPassword 未加密的密码 * @param encodedPassword 加密后的密码 * @return 是否匹配 */ boolean isPasswordMatch(String rawPassword, String encodedPassword); } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/service/user/AdminUserServiceImpl.java ================================================ package co.yixiang.yshop.module.system.service.user; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.exception.ErrorCode; import co.yixiang.yshop.framework.common.exception.ServiceException; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.framework.datapermission.core.util.DataPermissionUtils; import co.yixiang.yshop.module.infra.api.file.FileApi; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; import co.yixiang.yshop.module.store.dal.mysql.storeshop.StoreShopMapper; import co.yixiang.yshop.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; import co.yixiang.yshop.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; import co.yixiang.yshop.module.system.controller.admin.user.vo.user.UserImportExcelVO; import co.yixiang.yshop.module.system.controller.admin.user.vo.user.UserImportRespVO; import co.yixiang.yshop.module.system.controller.admin.user.vo.user.UserPageReqVO; import co.yixiang.yshop.module.system.controller.admin.user.vo.user.UserSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.dept.DeptDO; import co.yixiang.yshop.module.system.dal.dataobject.dept.UserPostDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.module.system.dal.mysql.dept.UserPostMapper; import co.yixiang.yshop.module.system.dal.mysql.user.AdminUserMapper; import co.yixiang.yshop.module.system.service.dept.DeptService; import co.yixiang.yshop.module.system.service.dept.PostService; import co.yixiang.yshop.module.system.service.permission.PermissionService; import co.yixiang.yshop.module.system.service.tenant.TenantService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.google.common.annotations.VisibleForTesting; import com.mzt.logapi.context.LogRecordContext; import com.mzt.logapi.service.impl.DiffParseFunction; import com.mzt.logapi.starter.annotation.LogRecord; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.io.InputStream; import java.time.LocalDateTime; import java.util.*; import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertList; import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertSet; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; import static co.yixiang.yshop.module.system.enums.LogRecordConstants.*; /** * 后台用户 Service 实现类 * * @author yshop */ @Service("adminUserService") @Slf4j public class AdminUserServiceImpl implements AdminUserService { @Value("${sys.user.init-password:yshopyuanma}") private String userInitPassword; @Resource private AdminUserMapper userMapper; @Resource private DeptService deptService; @Resource private PostService postService; @Resource private PermissionService permissionService; @Resource private PasswordEncoder passwordEncoder; @Resource @Lazy // 延迟,避免循环依赖报错 private TenantService tenantService; @Resource private UserPostMapper userPostMapper; @Resource private FileApi fileApi; @Resource private StoreShopMapper storeShopMapper; @Override @Transactional(rollbackFor = Exception.class) @LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_CREATE_SUB_TYPE, bizNo = "{{#user.id}}", success = SYSTEM_USER_CREATE_SUCCESS) public Long createUser(UserSaveReqVO createReqVO) { // 1.1 校验账户配合 tenantService.handleTenantInfo(tenant -> { long count = userMapper.selectCount(); if (count >= tenant.getAccountCount()) { throw exception(USER_COUNT_MAX, tenant.getAccountCount()); } }); // 1.2 校验正确性 validateUserForCreateOrUpdate(null, createReqVO.getUsername(), createReqVO.getMobile(), createReqVO.getEmail(), createReqVO.getDeptId(), createReqVO.getPostIds()); // 2.1 插入用户 AdminUserDO user = BeanUtils.toBean(createReqVO, AdminUserDO.class); user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启 user.setPassword(encodePassword(createReqVO.getPassword())); // 加密密码 userMapper.insert(user); // 2.2 插入关联岗位 if (CollectionUtil.isNotEmpty(user.getPostIds())) { userPostMapper.insertBatch(convertList(user.getPostIds(), postId -> new UserPostDO().setUserId(user.getId()).setPostId(postId))); } // 3. 记录操作日志上下文 LogRecordContext.putVariable("user", user); return user.getId(); } @Override @Transactional(rollbackFor = Exception.class) @LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}", success = SYSTEM_USER_UPDATE_SUCCESS) public void updateUser(UserSaveReqVO updateReqVO) { updateReqVO.setPassword(null); // 特殊:此处不更新密码 // 1. 校验正确性 AdminUserDO oldUser = validateUserForCreateOrUpdate(updateReqVO.getId(), updateReqVO.getUsername(), updateReqVO.getMobile(), updateReqVO.getEmail(), updateReqVO.getDeptId(), updateReqVO.getPostIds()); // 2.1 更新用户 AdminUserDO updateObj = BeanUtils.toBean(updateReqVO, AdminUserDO.class); userMapper.updateById(updateObj); // 2.2 更新岗位 updateUserPost(updateReqVO, updateObj); // 3. 记录操作日志上下文 LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldUser, UserSaveReqVO.class)); LogRecordContext.putVariable("user", oldUser); } private void updateUserPost(UserSaveReqVO reqVO, AdminUserDO updateObj) { Long userId = reqVO.getId(); Set dbPostIds = convertSet(userPostMapper.selectListByUserId(userId), UserPostDO::getPostId); // 计算新增和删除的岗位编号 Set postIds = CollUtil.emptyIfNull(updateObj.getPostIds()); Collection createPostIds = CollUtil.subtract(postIds, dbPostIds); Collection deletePostIds = CollUtil.subtract(dbPostIds, postIds); // 执行新增和删除。对于已经授权的岗位,不用做任何处理 if (!CollectionUtil.isEmpty(createPostIds)) { userPostMapper.insertBatch(convertList(createPostIds, postId -> new UserPostDO().setUserId(userId).setPostId(postId))); } if (!CollectionUtil.isEmpty(deletePostIds)) { userPostMapper.deleteByUserIdAndPostId(userId, deletePostIds); } } @Override public void updateUserLogin(Long id, String loginIp) { userMapper.updateById(new AdminUserDO().setId(id).setLoginIp(loginIp).setLoginDate(LocalDateTime.now())); } @Override public void updateUserProfile(Long id, UserProfileUpdateReqVO reqVO) { // 校验正确性 validateUserExists(id); validateEmailUnique(id, reqVO.getEmail()); validateMobileUnique(id, reqVO.getMobile()); // 执行更新 userMapper.updateById(BeanUtils.toBean(reqVO, AdminUserDO.class).setId(id)); } @Override public void updateUserPassword(Long id, UserProfileUpdatePasswordReqVO reqVO) { // 校验旧密码密码 validateOldPassword(id, reqVO.getOldPassword()); // 执行更新 AdminUserDO updateObj = new AdminUserDO().setId(id); updateObj.setPassword(encodePassword(reqVO.getNewPassword())); // 加密密码 userMapper.updateById(updateObj); } @Override public String updateUserAvatar(Long id, InputStream avatarFile) { validateUserExists(id); // 存储文件 String avatar = fileApi.createFile(IoUtil.readBytes(avatarFile)); // 更新路径 AdminUserDO sysUserDO = new AdminUserDO(); sysUserDO.setId(id); sysUserDO.setAvatar(avatar); userMapper.updateById(sysUserDO); return avatar; } @Override @LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_UPDATE_PASSWORD_SUB_TYPE, bizNo = "{{#id}}", success = SYSTEM_USER_UPDATE_PASSWORD_SUCCESS) public void updateUserPassword(Long id, String password) { // 1. 校验用户存在 AdminUserDO user = validateUserExists(id); // 2. 更新密码 AdminUserDO updateObj = new AdminUserDO(); updateObj.setId(id); updateObj.setPassword(encodePassword(password)); // 加密密码 userMapper.updateById(updateObj); // 3. 记录操作日志上下文 LogRecordContext.putVariable("user", user); LogRecordContext.putVariable("newPassword", updateObj.getPassword()); } @Override public void updateUserStatus(Long id, Integer status) { // 校验用户存在 validateUserExists(id); // 更新状态 AdminUserDO updateObj = new AdminUserDO(); updateObj.setId(id); updateObj.setStatus(status); userMapper.updateById(updateObj); } @Override @Transactional(rollbackFor = Exception.class) @LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_DELETE_SUB_TYPE, bizNo = "{{#id}}", success = SYSTEM_USER_DELETE_SUCCESS) public void deleteUser(Long id) { // 1. 校验用户存在 AdminUserDO user = validateUserExists(id); getShop(id); // 2.1 删除用户 userMapper.deleteById(id); // 2.2 删除用户关联数据 permissionService.processUserDeleted(id); // 2.2 删除用户岗位 userPostMapper.deleteByUserId(id); // 3. 记录操作日志上下文 LogRecordContext.putVariable("user", user); } private void getShop(Long userId) { StoreShopDO storeShopDO = storeShopMapper.selectOne(new LambdaQueryWrapper() .apply(userId > 0, "FIND_IN_SET ('" + userId + "',admin_id)")); if(storeShopDO != null){ throw exception(new ErrorCode(20241013,"当前门店下:" + storeShopDO.getName() + "绑定等有管理员不可以删除")); } } @Override public AdminUserDO getUserByUsername(String username) { return userMapper.selectByUsername(username); } @Override public AdminUserDO getUserByMobile(String mobile) { return userMapper.selectByMobile(mobile); } @Override public PageResult getUserPage(UserPageReqVO reqVO) { return userMapper.selectPage(reqVO, getDeptCondition(reqVO.getDeptId())); } @Override public AdminUserDO getUser(Long id) { return userMapper.selectById(id); } @Override public List getUserListByDeptIds(Collection deptIds) { if (CollUtil.isEmpty(deptIds)) { return Collections.emptyList(); } return userMapper.selectListByDeptIds(deptIds); } @Override public List getUserListByPostIds(Collection postIds) { if (CollUtil.isEmpty(postIds)) { return Collections.emptyList(); } Set userIds = convertSet(userPostMapper.selectListByPostIds(postIds), UserPostDO::getUserId); if (CollUtil.isEmpty(userIds)) { return Collections.emptyList(); } return userMapper.selectBatchIds(userIds); } @Override public List getUserList(Collection ids) { if (CollUtil.isEmpty(ids)) { return Collections.emptyList(); } return userMapper.selectBatchIds(ids); } @Override public void validateUserList(Collection ids) { if (CollUtil.isEmpty(ids)) { return; } // 获得岗位信息 List users = userMapper.selectBatchIds(ids); Map userMap = CollectionUtils.convertMap(users, AdminUserDO::getId); // 校验 ids.forEach(id -> { AdminUserDO user = userMap.get(id); if (user == null) { throw exception(USER_NOT_EXISTS); } if (!CommonStatusEnum.ENABLE.getStatus().equals(user.getStatus())) { throw exception(USER_IS_DISABLE, user.getNickname()); } }); } @Override public List getUserListByNickname(String nickname) { return userMapper.selectListByNickname(nickname); } /** * 获得部门条件:查询指定部门的子部门编号们,包括自身 * @param deptId 部门编号 * @return 部门编号集合 */ private Set getDeptCondition(Long deptId) { if (deptId == null) { return Collections.emptySet(); } Set deptIds = convertSet(deptService.getChildDeptList(deptId), DeptDO::getId); deptIds.add(deptId); // 包括自身 return deptIds; } private AdminUserDO validateUserForCreateOrUpdate(Long id, String username, String mobile, String email, Long deptId, Set postIds) { // 关闭数据权限,避免因为没有数据权限,查询不到数据,进而导致唯一校验不正确 return DataPermissionUtils.executeIgnore(() -> { // 校验用户存在 AdminUserDO user = validateUserExists(id); // 校验用户名唯一 validateUsernameUnique(id, username); // 校验手机号唯一 validateMobileUnique(id, mobile); // 校验邮箱唯一 validateEmailUnique(id, email); // 校验部门处于开启状态 deptService.validateDeptList(CollectionUtils.singleton(deptId)); // 校验岗位处于开启状态 postService.validatePostList(postIds); return user; }); } @VisibleForTesting AdminUserDO validateUserExists(Long id) { if (id == null) { return null; } AdminUserDO user = userMapper.selectById(id); if (user == null) { throw exception(USER_NOT_EXISTS); } return user; } @VisibleForTesting void validateUsernameUnique(Long id, String username) { if (StrUtil.isBlank(username)) { return; } AdminUserDO user = userMapper.selectByUsername(username); if (user == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的用户 if (id == null) { throw exception(USER_USERNAME_EXISTS); } if (!user.getId().equals(id)) { throw exception(USER_USERNAME_EXISTS); } } @VisibleForTesting void validateEmailUnique(Long id, String email) { if (StrUtil.isBlank(email)) { return; } AdminUserDO user = userMapper.selectByEmail(email); if (user == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的用户 if (id == null) { throw exception(USER_EMAIL_EXISTS); } if (!user.getId().equals(id)) { throw exception(USER_EMAIL_EXISTS); } } @VisibleForTesting void validateMobileUnique(Long id, String mobile) { if (StrUtil.isBlank(mobile)) { return; } AdminUserDO user = userMapper.selectByMobile(mobile); if (user == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的用户 if (id == null) { throw exception(USER_MOBILE_EXISTS); } if (!user.getId().equals(id)) { throw exception(USER_MOBILE_EXISTS); } } /** * 校验旧密码 * @param id 用户 id * @param oldPassword 旧密码 */ @VisibleForTesting void validateOldPassword(Long id, String oldPassword) { AdminUserDO user = userMapper.selectById(id); if (user == null) { throw exception(USER_NOT_EXISTS); } if (!isPasswordMatch(oldPassword, user.getPassword())) { throw exception(USER_PASSWORD_FAILED); } } @Override @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入 public UserImportRespVO importUserList(List importUsers, boolean isUpdateSupport) { if (CollUtil.isEmpty(importUsers)) { throw exception(USER_IMPORT_LIST_IS_EMPTY); } UserImportRespVO respVO = UserImportRespVO.builder().createUsernames(new ArrayList<>()) .updateUsernames(new ArrayList<>()).failureUsernames(new LinkedHashMap<>()).build(); importUsers.forEach(importUser -> { // 校验,判断是否有不符合的原因 try { validateUserForCreateOrUpdate(null, null, importUser.getMobile(), importUser.getEmail(), importUser.getDeptId(), null); } catch (ServiceException ex) { respVO.getFailureUsernames().put(importUser.getUsername(), ex.getMessage()); return; } // 判断如果不存在,在进行插入 AdminUserDO existUser = userMapper.selectByUsername(importUser.getUsername()); if (existUser == null) { userMapper.insert(BeanUtils.toBean(importUser, AdminUserDO.class) .setPassword(encodePassword(userInitPassword)).setPostIds(new HashSet<>())); // 设置默认密码及空岗位编号数组 respVO.getCreateUsernames().add(importUser.getUsername()); return; } // 如果存在,判断是否允许更新 if (!isUpdateSupport) { respVO.getFailureUsernames().put(importUser.getUsername(), USER_USERNAME_EXISTS.getMsg()); return; } AdminUserDO updateUser = BeanUtils.toBean(importUser, AdminUserDO.class); updateUser.setId(existUser.getId()); userMapper.updateById(updateUser); respVO.getUpdateUsernames().add(importUser.getUsername()); }); return respVO; } @Override public List getUserListByStatus(Integer status) { return userMapper.selectListByStatus(status); } @Override public boolean isPasswordMatch(String rawPassword, String encodedPassword) { return passwordEncoder.matches(rawPassword, encodedPassword); } /** * 对密码进行加密 * * @param password 密码 * @return 加密后的密码 */ private String encodePassword(String password) { return passwordEncoder.encode(password); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/java/co/yixiang/yshop/module/system/util/oauth2/OAuth2Utils.java ================================================ package co.yixiang.yshop.module.system.util.oauth2; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.util.http.HttpUtils; import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.*; /** * OAuth2 相关的工具类 * * @author yshop */ public class OAuth2Utils { /** * 构建授权码模式下,重定向的 URI * * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 getSuccessfulRedirect 方法 * * @param redirectUri 重定向 URI * @param authorizationCode 授权码 * @param state 状态 * @return 授权码模式下的重定向 URI */ public static String buildAuthorizationCodeRedirectUri(String redirectUri, String authorizationCode, String state) { Map query = new LinkedHashMap<>(); query.put("code", authorizationCode); if (state != null) { query.put("state", state); } return HttpUtils.append(redirectUri, query, null, false); } /** * 构建简化模式下,重定向的 URI * * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 appendAccessToken 方法 * * @param redirectUri 重定向 URI * @param accessToken 访问令牌 * @param state 状态 * @param expireTime 过期时间 * @param scopes 授权范围 * @param additionalInformation 附加信息 * @return 简化授权模式下的重定向 URI */ public static String buildImplicitRedirectUri(String redirectUri, String accessToken, String state, LocalDateTime expireTime, Collection scopes, Map additionalInformation) { Map vars = new LinkedHashMap(); Map keys = new HashMap(); vars.put("access_token", accessToken); vars.put("token_type", SecurityFrameworkUtils.AUTHORIZATION_BEARER.toLowerCase()); if (state != null) { vars.put("state", state); } if (expireTime != null) { vars.put("expires_in", getExpiresIn(expireTime)); } if (CollUtil.isNotEmpty(scopes)) { vars.put("scope", buildScopeStr(scopes)); } if (CollUtil.isNotEmpty(additionalInformation)) { for (String key : additionalInformation.keySet()) { Object value = additionalInformation.get(key); if (value != null) { keys.put("extra_" + key, key); vars.put("extra_" + key, value); } } } // Do not include the refresh token (even if there is one) return HttpUtils.append(redirectUri, vars, keys, true); } public static String buildUnsuccessfulRedirect(String redirectUri, String responseType, String state, String error, String description) { Map query = new LinkedHashMap(); query.put("error", error); query.put("error_description", description); if (state != null) { query.put("state", state); } return HttpUtils.append(redirectUri, query, null, !responseType.contains("code")); } public static long getExpiresIn(LocalDateTime expireTime) { return LocalDateTimeUtil.between(LocalDateTime.now(), expireTime, ChronoUnit.SECONDS); } public static String buildScopeStr(Collection scopes) { return CollUtil.join(scopes, " "); } public static List buildScopes(String scope) { return StrUtil.split(scope, ' '); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/main/resources/META-INF/services/com.xingyuv.captcha.service.CaptchaCacheService ================================================ co.yixiang.yshop.module.system.framework.captcha.core.RedisCaptchaServiceImpl ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/controller/admin/oauth2/OAuth2OpenControllerTest.java ================================================ package co.yixiang.yshop.module.system.controller.admin.oauth2; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.map.MapUtil; import co.yixiang.yshop.framework.common.core.KeyValue; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.exception.ErrorCode; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.framework.common.util.collection.SetUtils; import co.yixiang.yshop.framework.common.util.object.ObjectUtils; import co.yixiang.yshop.framework.test.core.ut.BaseMockitoUnitTest; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAuthorizeInfoRespVO; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2ClientDO; import co.yixiang.yshop.module.system.enums.oauth2.OAuth2GrantTypeEnum; import co.yixiang.yshop.module.system.service.oauth2.OAuth2ApproveService; import co.yixiang.yshop.module.system.service.oauth2.OAuth2ClientService; import co.yixiang.yshop.module.system.service.oauth2.OAuth2GrantService; import co.yixiang.yshop.module.system.service.oauth2.OAuth2TokenService; import org.assertj.core.util.Lists; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import jakarta.servlet.http.HttpServletRequest; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import static co.yixiang.yshop.framework.common.util.collection.SetUtils.asSet; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomPojo; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomString; import static java.util.Arrays.asList; import static org.hamcrest.CoreMatchers.anyOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * {@link OAuth2OpenController} 的单元测试 * * @author yshop */ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest { @InjectMocks private OAuth2OpenController oauth2OpenController; @Mock private OAuth2GrantService oauth2GrantService; @Mock private OAuth2ClientService oauth2ClientService; @Mock private OAuth2ApproveService oauth2ApproveService; @Mock private OAuth2TokenService oauth2TokenService; @Test public void testPostAccessToken_authorizationCode() { // 准备参数 String granType = OAuth2GrantTypeEnum.AUTHORIZATION_CODE.getGrantType(); String code = randomString(); String redirectUri = randomString(); String state = randomString(); HttpServletRequest request = mockRequest("test_client_id", "test_client_secret"); // mock 方法(client) OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("test_client_id"); when(oauth2ClientService.validOAuthClientFromCache(eq("test_client_id"), eq("test_client_secret"), eq(granType), eq(new ArrayList<>()), eq(redirectUri))).thenReturn(client); // mock 方法(访问令牌) OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) .setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 30000L, ChronoUnit.MILLIS)); when(oauth2GrantService.grantAuthorizationCodeForAccessToken(eq("test_client_id"), eq(code), eq(redirectUri), eq(state))).thenReturn(accessTokenDO); // 调用 CommonResult result = oauth2OpenController.postAccessToken(request, granType, code, redirectUri, state, null, null, null, null); // 断言 assertEquals(0, result.getCode()); assertPojoEquals(accessTokenDO, result.getData()); assertTrue(ObjectUtils.equalsAny(result.getData().getExpiresIn(), 29L, 30L)); // 执行过程会过去几毫秒 } @Test public void testPostAccessToken_password() { // 准备参数 String granType = OAuth2GrantTypeEnum.PASSWORD.getGrantType(); String username = randomString(); String password = randomString(); String scope = "write read"; HttpServletRequest request = mockRequest("test_client_id", "test_client_secret"); // mock 方法(client) OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("test_client_id"); when(oauth2ClientService.validOAuthClientFromCache(eq("test_client_id"), eq("test_client_secret"), eq(granType), eq(Lists.newArrayList("write", "read")), isNull())).thenReturn(client); // mock 方法(访问令牌) OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) .setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 30000L, ChronoUnit.MILLIS)); when(oauth2GrantService.grantPassword(eq(username), eq(password), eq("test_client_id"), eq(Lists.newArrayList("write", "read")))).thenReturn(accessTokenDO); // 调用 CommonResult result = oauth2OpenController.postAccessToken(request, granType, null, null, null, username, password, scope, null); // 断言 assertEquals(0, result.getCode()); assertPojoEquals(accessTokenDO, result.getData()); assertTrue(ObjectUtils.equalsAny(result.getData().getExpiresIn(), 29L, 30L)); // 执行过程会过去几毫秒 } @Test public void testPostAccessToken_refreshToken() { // 准备参数 String granType = OAuth2GrantTypeEnum.REFRESH_TOKEN.getGrantType(); String refreshToken = randomString(); String password = randomString(); HttpServletRequest request = mockRequest("test_client_id", "test_client_secret"); // mock 方法(client) OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("test_client_id"); when(oauth2ClientService.validOAuthClientFromCache(eq("test_client_id"), eq("test_client_secret"), eq(granType), eq(Lists.newArrayList()), isNull())).thenReturn(client); // mock 方法(访问令牌) OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) .setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 30000L, ChronoUnit.MILLIS)); when(oauth2GrantService.grantRefreshToken(eq(refreshToken), eq("test_client_id"))).thenReturn(accessTokenDO); // 调用 CommonResult result = oauth2OpenController.postAccessToken(request, granType, null, null, null, null, password, null, refreshToken); // 断言 assertEquals(0, result.getCode()); assertPojoEquals(accessTokenDO, result.getData()); assertTrue(ObjectUtils.equalsAny(result.getData().getExpiresIn(), 29L, 30L)); // 执行过程会过去几毫秒 } @Test public void testPostAccessToken_implicit() { // 调用,并断言 assertServiceException(() -> oauth2OpenController.postAccessToken(null, OAuth2GrantTypeEnum.IMPLICIT.getGrantType(), null, null, null, null, null, null, null), new ErrorCode(400, "Token 接口不支持 implicit 授权模式")); } @Test public void testRevokeToken() { // 准备参数 HttpServletRequest request = mockRequest("demo_client_id", "demo_client_secret"); String token = randomString(); // mock 方法(client) OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("demo_client_id"); when(oauth2ClientService.validOAuthClientFromCache(eq("demo_client_id"), eq("demo_client_secret"), isNull(), isNull(), isNull())).thenReturn(client); // mock 方法(移除) when(oauth2GrantService.revokeToken(eq("demo_client_id"), eq(token))).thenReturn(true); // 调用 CommonResult result = oauth2OpenController.revokeToken(request, token); // 断言 assertEquals(0, result.getCode()); assertTrue(result.getData()); } @Test public void testCheckToken() { // 准备参数 HttpServletRequest request = mockRequest("demo_client_id", "demo_client_secret"); String token = randomString(); // mock 方法 OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class).setUserType(UserTypeEnum.ADMIN.getValue()).setExpiresTime(LocalDateTimeUtil.of(1653485731195L)); when(oauth2TokenService.checkAccessToken(eq(token))).thenReturn(accessTokenDO); // 调用 CommonResult result = oauth2OpenController.checkToken(request, token); // 断言 assertEquals(0, result.getCode()); assertPojoEquals(accessTokenDO, result.getData()); assertEquals(1653485731L, result.getData().getExp()); // 执行过程会过去几毫秒 } @Test public void testAuthorize() { // 准备参数 String clientId = randomString(); // mock 方法(client) OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("demo_client_id").setScopes(ListUtil.toList("read", "write", "all")); when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(client); // mock 方法(approve) List approves = asList( randomPojo(OAuth2ApproveDO.class).setScope("read").setApproved(true), randomPojo(OAuth2ApproveDO.class).setScope("write").setApproved(false)); when(oauth2ApproveService.getApproveList(isNull(), eq(UserTypeEnum.ADMIN.getValue()), eq(clientId))).thenReturn(approves); // 调用 CommonResult result = oauth2OpenController.authorize(clientId); // 断言 assertEquals(0, result.getCode()); assertPojoEquals(client, result.getData().getClient()); assertEquals(new KeyValue<>("read", true), result.getData().getScopes().get(0)); assertEquals(new KeyValue<>("write", false), result.getData().getScopes().get(1)); assertEquals(new KeyValue<>("all", false), result.getData().getScopes().get(2)); } @Test public void testApproveOrDeny_grantTypeError() { // 调用,并断言 assertServiceException(() -> oauth2OpenController.approveOrDeny(randomString(), null, null, null, null, null), new ErrorCode(400, "response_type 参数值只允许 code 和 token")); } @Test // autoApprove = true,但是不通过 public void testApproveOrDeny_autoApproveNo() { // 准备参数 String responseType = "code"; String clientId = randomString(); String scope = "{\"read\": true, \"write\": false}"; String redirectUri = randomString(); String state = randomString(); // mock 方法 OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class); when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("authorization_code"), eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client); // 调用 CommonResult result = oauth2OpenController.approveOrDeny(responseType, clientId, scope, redirectUri, true, state); // 断言 assertEquals(0, result.getCode()); assertNull(result.getData()); } @Test // autoApprove = false,但是不通过 public void testApproveOrDeny_ApproveNo() { // 准备参数 String responseType = "token"; String clientId = randomString(); String scope = "{\"read\": true, \"write\": false}"; String redirectUri = "https://www.yixiang.co"; String state = "test"; // mock 方法 OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class); when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("implicit"), eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client); // 调用 CommonResult result = oauth2OpenController.approveOrDeny(responseType, clientId, scope, redirectUri, false, state); // 断言 assertEquals(0, result.getCode()); assertEquals("https://www.yixiang.co#error=access_denied&error_description=User%20denied%20access&state=test", result.getData()); } @Test // autoApprove = true,通过 + token public void testApproveOrDeny_autoApproveWithToken() { // 准备参数 String responseType = "token"; String clientId = randomString(); String scope = "{\"read\": true, \"write\": false}"; String redirectUri = "https://www.yixiang.co"; String state = "test"; // mock 方法(client) OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId(clientId).setAdditionalInformation(null); when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("implicit"), eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client); // mock 方法(场景一) when(oauth2ApproveService.checkForPreApproval(isNull(), eq(UserTypeEnum.ADMIN.getValue()), eq(clientId), eq(SetUtils.asSet("read", "write")))).thenReturn(true); // mock 方法(访问令牌) OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) .setAccessToken("test_access_token").setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 30010L, ChronoUnit.MILLIS)); when(oauth2GrantService.grantImplicit(isNull(), eq(UserTypeEnum.ADMIN.getValue()), eq(clientId), eq(ListUtil.toList("read")))).thenReturn(accessTokenDO); // 调用 CommonResult result = oauth2OpenController.approveOrDeny(responseType, clientId, scope, redirectUri, true, state); // 断言 assertEquals(0, result.getCode()); assertThat(result.getData(), anyOf( // 29 和 30 都有一定概率,主要是时间计算 is("https://www.yixiang.co#access_token=test_access_token&token_type=bearer&state=test&expires_in=29&scope=read"), is("https://www.yixiang.co#access_token=test_access_token&token_type=bearer&state=test&expires_in=30&scope=read") )); } @Test // autoApprove = false,通过 + code public void testApproveOrDeny_approveWithCode() { // 准备参数 String responseType = "code"; String clientId = randomString(); String scope = "{\"read\": true, \"write\": false}"; String redirectUri = "https://www.yixiang.co"; String state = "test"; // mock 方法(client) OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId(clientId).setAdditionalInformation(null); when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("authorization_code"), eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client); // mock 方法(场景二) when(oauth2ApproveService.updateAfterApproval(isNull(), eq(UserTypeEnum.ADMIN.getValue()), eq(clientId), eq(MapUtil.builder(new LinkedHashMap()).put("read", true).put("write", false).build()))) .thenReturn(true); // mock 方法(访问令牌) String authorizationCode = "test_code"; when(oauth2GrantService.grantAuthorizationCodeForCode(isNull(), eq(UserTypeEnum.ADMIN.getValue()), eq(clientId), eq(ListUtil.toList("read")), eq(redirectUri), eq(state))).thenReturn(authorizationCode); // 调用 CommonResult result = oauth2OpenController.approveOrDeny(responseType, clientId, scope, redirectUri, false, state); // 断言 assertEquals(0, result.getCode()); assertEquals("https://www.yixiang.co?code=test_code&state=test", result.getData()); } private HttpServletRequest mockRequest(String clientId, String secret) { HttpServletRequest request = mock(HttpServletRequest.class); when(request.getParameter(eq("client_id"))).thenReturn(clientId); when(request.getParameter(eq("client_secret"))).thenReturn(secret); return request; } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java ================================================ package co.yixiang.yshop.module.system.framework.sms.core.client.impl; import cn.hutool.core.util.ReflectUtil; import co.yixiang.yshop.framework.common.core.KeyValue; import co.yixiang.yshop.framework.common.util.collection.MapUtils; import co.yixiang.yshop.framework.test.core.ut.BaseMockitoUnitTest; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsSendRespDTO; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; import co.yixiang.yshop.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; import co.yixiang.yshop.module.system.framework.sms.core.property.SmsChannelProperties; import com.aliyuncs.IAcsClient; import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest; import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateResponse; import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse; import com.google.common.collect.Lists; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatcher; import org.mockito.InjectMocks; import org.mockito.Mock; import java.time.LocalDateTime; import java.util.List; import static co.yixiang.yshop.framework.common.util.json.JsonUtils.toJsonString; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.when; /** * {@link AliyunSmsClient} 的单元测试 * * @author yshop */ public class AliyunSmsClientTest extends BaseMockitoUnitTest { private final SmsChannelProperties properties = new SmsChannelProperties() .setApiKey(randomString()) // 随机一个 apiKey,避免构建报错 .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 .setSignature("yshop"); @InjectMocks private final AliyunSmsClient smsClient = new AliyunSmsClient(properties); @Mock private IAcsClient client; @Test public void testDoInit() { // 准备参数 // mock 方法 // 调用 smsClient.doInit(); // 断言 assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "acsClient")); } @Test public void tesSendSms_success() throws Throwable { // 准备参数 Long sendLogId = randomLongId(); String mobile = randomString(); String apiTemplateId = randomString(); List> templateParams = Lists.newArrayList( new KeyValue<>("code", 1234), new KeyValue<>("op", "login")); // mock 方法 SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> o.setCode("OK")); when(client.getAcsResponse(argThat((ArgumentMatcher) acsRequest -> { assertEquals(mobile, acsRequest.getPhoneNumbers()); assertEquals(properties.getSignature(), acsRequest.getSignName()); assertEquals(apiTemplateId, acsRequest.getTemplateCode()); assertEquals(toJsonString(MapUtils.convertMap(templateParams)), acsRequest.getTemplateParam()); assertEquals(sendLogId.toString(), acsRequest.getOutId()); return true; }))).thenReturn(response); // 调用 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams); // 断言 assertTrue(result.getSuccess()); assertEquals(response.getRequestId(), result.getApiRequestId()); assertEquals(response.getCode(), result.getApiCode()); assertEquals(response.getMessage(), result.getApiMsg()); assertEquals(response.getBizId(), result.getSerialNo()); } @Test public void tesSendSms_fail() throws Throwable { // 准备参数 Long sendLogId = randomLongId(); String mobile = randomString(); String apiTemplateId = randomString(); List> templateParams = Lists.newArrayList( new KeyValue<>("code", 1234), new KeyValue<>("op", "login")); // mock 方法 SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> o.setCode("ERROR")); when(client.getAcsResponse(argThat((ArgumentMatcher) acsRequest -> { assertEquals(mobile, acsRequest.getPhoneNumbers()); assertEquals(properties.getSignature(), acsRequest.getSignName()); assertEquals(apiTemplateId, acsRequest.getTemplateCode()); assertEquals(toJsonString(MapUtils.convertMap(templateParams)), acsRequest.getTemplateParam()); assertEquals(sendLogId.toString(), acsRequest.getOutId()); return true; }))).thenReturn(response); // 调用 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams); // 断言 assertFalse(result.getSuccess()); assertEquals(response.getRequestId(), result.getApiRequestId()); assertEquals(response.getCode(), result.getApiCode()); assertEquals(response.getMessage(), result.getApiMsg()); assertEquals(response.getBizId(), result.getSerialNo()); } @Test public void testParseSmsReceiveStatus() { // 准备参数 String text = "[\n" + " {\n" + " \"phone_number\" : \"13900000001\",\n" + " \"send_time\" : \"2017-01-01 11:12:13\",\n" + " \"report_time\" : \"2017-02-02 22:23:24\",\n" + " \"success\" : true,\n" + " \"err_code\" : \"DELIVERED\",\n" + " \"err_msg\" : \"用户接收成功\",\n" + " \"sms_size\" : \"1\",\n" + " \"biz_id\" : \"12345\",\n" + " \"out_id\" : \"67890\"\n" + " }\n" + "]"; // mock 方法 // 调用 List statuses = smsClient.parseSmsReceiveStatus(text); // 断言 assertEquals(1, statuses.size()); assertTrue(statuses.get(0).getSuccess()); assertEquals("DELIVERED", statuses.get(0).getErrorCode()); assertEquals("用户接收成功", statuses.get(0).getErrorMsg()); assertEquals("13900000001", statuses.get(0).getMobile()); assertEquals(LocalDateTime.of(2017, 2, 2, 22, 23, 24), statuses.get(0).getReceiveTime()); assertEquals("12345", statuses.get(0).getSerialNo()); assertEquals(67890L, statuses.get(0).getLogId()); } @Test public void testGetSmsTemplate() throws Throwable { // 准备参数 String apiTemplateId = randomString(); // mock 方法 QuerySmsTemplateResponse response = randomPojo(QuerySmsTemplateResponse.class, o -> { o.setCode("OK"); o.setTemplateStatus(1); // 设置模板通过 }); when(client.getAcsResponse(argThat((ArgumentMatcher) acsRequest -> { assertEquals(apiTemplateId, acsRequest.getTemplateCode()); return true; }))).thenReturn(response); // 调用 SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId); // 断言 assertEquals(response.getTemplateCode(), result.getId()); assertEquals(response.getTemplateContent(), result.getContent()); assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus()); assertEquals(response.getReason(), result.getAuditReason()); } @Test public void testConvertSmsTemplateAuditStatus() { assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(), smsClient.convertSmsTemplateAuditStatus(0)); assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), smsClient.convertSmsTemplateAuditStatus(1)); assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(), smsClient.convertSmsTemplateAuditStatus(2)); assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus(3), "未知审核状态(3)"); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java ================================================ package co.yixiang.yshop.module.system.framework.sms.core.client.impl; import cn.hutool.core.util.ReflectUtil; import co.yixiang.yshop.framework.common.core.KeyValue; import co.yixiang.yshop.framework.common.util.collection.ArrayUtils; import co.yixiang.yshop.framework.common.util.collection.MapUtils; import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.framework.test.core.ut.BaseMockitoUnitTest; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsSendRespDTO; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; import co.yixiang.yshop.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; import co.yixiang.yshop.module.system.framework.sms.core.property.SmsChannelProperties; import com.google.common.collect.Lists; import com.tencentcloudapi.sms.v20210111.SmsClient; import com.tencentcloudapi.sms.v20210111.models.DescribeSmsTemplateListResponse; import com.tencentcloudapi.sms.v20210111.models.DescribeTemplateListStatus; import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse; import com.tencentcloudapi.sms.v20210111.models.SendStatus; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import static co.yixiang.yshop.framework.common.util.json.JsonUtils.toJsonString; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.when; /** * {@link TencentSmsClient} 的单元测试 * * @author shiwp */ public class TencentSmsClientTest extends BaseMockitoUnitTest { private final SmsChannelProperties properties = new SmsChannelProperties() .setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey,避免构建报错 .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 .setSignature("yshop"); @InjectMocks private TencentSmsClient smsClient = new TencentSmsClient(properties); @Mock private SmsClient client; @Test public void testDoInit() { // 准备参数 // mock 方法 // 调用 smsClient.doInit(); // 断言 assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client")); } @Test public void testRefresh() { // 准备参数 SmsChannelProperties p = new SmsChannelProperties() .setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey,避免构建报错 .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 .setSignature("yshop"); // 调用 smsClient.refresh(p); // 断言 assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client")); } @Test public void testDoSendSms_success() throws Throwable { // 准备参数 Long sendLogId = randomLongId(); String mobile = randomString(); String apiTemplateId = randomString(); List> templateParams = Lists.newArrayList( new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); String requestId = randomString(); String serialNo = randomString(); // mock 方法 SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> { o.setRequestId(requestId); SendStatus[] sendStatuses = new SendStatus[1]; o.setSendStatusSet(sendStatuses); SendStatus sendStatus = new SendStatus(); sendStatuses[0] = sendStatus; sendStatus.setCode(TencentSmsClient.API_CODE_SUCCESS); sendStatus.setMessage("send success"); sendStatus.setSerialNo(serialNo); }); when(client.SendSms(argThat(request -> { assertEquals(mobile, request.getPhoneNumberSet()[0]); assertEquals(properties.getSignature(), request.getSignName()); assertEquals(apiTemplateId, request.getTemplateId()); assertEquals(toJsonString(ArrayUtils.toArray(new ArrayList<>(MapUtils.convertMap(templateParams).values()), String::valueOf)), toJsonString(request.getTemplateParamSet())); assertEquals(sendLogId, ReflectUtil.getFieldValue(JsonUtils.parseObject(request.getSessionContext(), TencentSmsClient.SessionContext.class), "logId")); return true; }))).thenReturn(response); // 调用 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams); // 断言 assertTrue(result.getSuccess()); assertEquals(response.getRequestId(), result.getApiRequestId()); assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode()); assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg()); assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getSerialNo()); } @Test public void testDoSendSms_fail() throws Throwable { // 准备参数 Long sendLogId = randomLongId(); String mobile = randomString(); String apiTemplateId = randomString(); List> templateParams = Lists.newArrayList( new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); String requestId = randomString(); String serialNo = randomString(); // mock 方法 SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> { o.setRequestId(requestId); SendStatus[] sendStatuses = new SendStatus[1]; o.setSendStatusSet(sendStatuses); SendStatus sendStatus = new SendStatus(); sendStatuses[0] = sendStatus; sendStatus.setCode("ERROR"); sendStatus.setMessage("send success"); sendStatus.setSerialNo(serialNo); }); when(client.SendSms(argThat(request -> { assertEquals(mobile, request.getPhoneNumberSet()[0]); assertEquals(properties.getSignature(), request.getSignName()); assertEquals(apiTemplateId, request.getTemplateId()); assertEquals(toJsonString(ArrayUtils.toArray(new ArrayList<>(MapUtils.convertMap(templateParams).values()), String::valueOf)), toJsonString(request.getTemplateParamSet())); assertEquals(sendLogId, ReflectUtil.getFieldValue(JsonUtils.parseObject(request.getSessionContext(), TencentSmsClient.SessionContext.class), "logId")); return true; }))).thenReturn(response); // 调用 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams); // 断言 assertFalse(result.getSuccess()); assertEquals(response.getRequestId(), result.getApiRequestId()); assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode()); assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg()); assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getSerialNo()); } @Test public void testParseSmsReceiveStatus() { // 准备参数 String text = "[\n" + " {\n" + " \"user_receive_time\": \"2015-10-17 08:03:04\",\n" + " \"nationcode\": \"86\",\n" + " \"mobile\": \"13900000001\",\n" + " \"report_status\": \"SUCCESS\",\n" + " \"errmsg\": \"DELIVRD\",\n" + " \"description\": \"用户短信送达成功\",\n" + " \"sid\": \"12345\",\n" + " \"ext\": {\"logId\":\"67890\"}\n" + " }\n" + "]"; // mock 方法 // 调用 List statuses = smsClient.parseSmsReceiveStatus(text); // 断言 assertEquals(1, statuses.size()); assertTrue(statuses.get(0).getSuccess()); assertEquals("DELIVRD", statuses.get(0).getErrorCode()); assertEquals("用户短信送达成功", statuses.get(0).getErrorMsg()); assertEquals("13900000001", statuses.get(0).getMobile()); assertEquals(LocalDateTime.of(2015, 10, 17, 8, 3, 4), statuses.get(0).getReceiveTime()); assertEquals("12345", statuses.get(0).getSerialNo()); assertEquals(67890L, statuses.get(0).getLogId()); } @Test public void testGetSmsTemplate() throws Throwable { // 准备参数 Long apiTemplateId = randomLongId(); String requestId = randomString(); // mock 方法 DescribeSmsTemplateListResponse response = randomPojo(DescribeSmsTemplateListResponse.class, o -> { DescribeTemplateListStatus[] describeTemplateListStatuses = new DescribeTemplateListStatus[1]; DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus(); templateStatus.setTemplateId(apiTemplateId); templateStatus.setStatusCode(0L);// 设置模板通过 describeTemplateListStatuses[0] = templateStatus; o.setDescribeTemplateStatusSet(describeTemplateListStatuses); o.setRequestId(requestId); }); when(client.DescribeSmsTemplateList(argThat(request -> { assertEquals(apiTemplateId, request.getTemplateIdSet()[0]); return true; }))).thenReturn(response); // 调用 SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId.toString()); // 断言 assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateId().toString(), result.getId()); assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateContent(), result.getContent()); assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus()); assertEquals(response.getDescribeTemplateStatusSet()[0].getReviewReply(), result.getAuditReason()); } @Test public void testConvertSmsTemplateAuditStatus() { assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), smsClient.convertSmsTemplateAuditStatus(0)); assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(), smsClient.convertSmsTemplateAuditStatus(1)); assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(), smsClient.convertSmsTemplateAuditStatus(-1)); assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus(3), "未知审核状态(3)"); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/auth/AdminAuthServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.auth; import cn.hutool.core.util.ReflectUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.api.sms.SmsCodeApi; import co.yixiang.yshop.module.system.api.social.dto.SocialUserBindReqDTO; import co.yixiang.yshop.module.system.api.social.dto.SocialUserRespDTO; import co.yixiang.yshop.module.system.controller.admin.auth.vo.*; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.module.system.enums.logger.LoginLogTypeEnum; import co.yixiang.yshop.module.system.enums.logger.LoginResultEnum; import co.yixiang.yshop.module.system.enums.sms.SmsSceneEnum; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import co.yixiang.yshop.module.system.service.logger.LoginLogService; import co.yixiang.yshop.module.system.service.member.MemberService; import co.yixiang.yshop.module.system.service.oauth2.OAuth2TokenService; import co.yixiang.yshop.module.system.service.social.SocialUserService; import co.yixiang.yshop.module.system.service.user.AdminUserService; import com.xingyuv.captcha.model.common.ResponseModel; import com.xingyuv.captcha.service.CaptchaService; import jakarta.annotation.Resource; import jakarta.validation.ConstraintViolationException; import jakarta.validation.Validation; import jakarta.validation.Validator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import static cn.hutool.core.util.RandomUtil.randomEle; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomPojo; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomString; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @Import(AdminAuthServiceImpl.class) public class AdminAuthServiceImplTest extends BaseDbUnitTest { @Resource private AdminAuthServiceImpl authService; @MockBean private AdminUserService userService; @MockBean private CaptchaService captchaService; @MockBean private LoginLogService loginLogService; @MockBean private SocialUserService socialUserService; @MockBean private SmsCodeApi smsCodeApi; @MockBean private OAuth2TokenService oauth2TokenService; @MockBean private MemberService memberService; @MockBean private Validator validator; @BeforeEach public void setUp() { ReflectUtil.setFieldValue(authService, "captchaEnable", true); // 注入一个 Validator 对象 ReflectUtil.setFieldValue(authService, "validator", Validation.buildDefaultValidatorFactory().getValidator()); } @Test public void testAuthenticate_success() { // 准备参数 String username = randomString(); String password = randomString(); // mock user 数据 AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setUsername(username) .setPassword(password).setStatus(CommonStatusEnum.ENABLE.getStatus())); when(userService.getUserByUsername(eq(username))).thenReturn(user); // mock password 匹配 when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true); // 调用 AdminUserDO loginUser = authService.authenticate(username, password); // 校验 assertPojoEquals(user, loginUser); } @Test public void testAuthenticate_userNotFound() { // 准备参数 String username = randomString(); String password = randomString(); // 调用, 并断言异常 assertServiceException(() -> authService.authenticate(username, password), AUTH_LOGIN_BAD_CREDENTIALS); verify(loginLogService).createLoginLog( argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) && o.getResult().equals(LoginResultEnum.BAD_CREDENTIALS.getResult()) && o.getUserId() == null) ); } @Test public void testAuthenticate_badCredentials() { // 准备参数 String username = randomString(); String password = randomString(); // mock user 数据 AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setUsername(username) .setPassword(password).setStatus(CommonStatusEnum.ENABLE.getStatus())); when(userService.getUserByUsername(eq(username))).thenReturn(user); // 调用, 并断言异常 assertServiceException(() -> authService.authenticate(username, password), AUTH_LOGIN_BAD_CREDENTIALS); verify(loginLogService).createLoginLog( argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) && o.getResult().equals(LoginResultEnum.BAD_CREDENTIALS.getResult()) && o.getUserId().equals(user.getId())) ); } @Test public void testAuthenticate_userDisabled() { // 准备参数 String username = randomString(); String password = randomString(); // mock user 数据 AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setUsername(username) .setPassword(password).setStatus(CommonStatusEnum.DISABLE.getStatus())); when(userService.getUserByUsername(eq(username))).thenReturn(user); // mock password 匹配 when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true); // 调用, 并断言异常 assertServiceException(() -> authService.authenticate(username, password), AUTH_LOGIN_USER_DISABLED); verify(loginLogService).createLoginLog( argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) && o.getResult().equals(LoginResultEnum.USER_DISABLED.getResult()) && o.getUserId().equals(user.getId())) ); } @Test public void testLogin_success() { // 准备参数 AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class, o -> o.setUsername("test_username").setPassword("test_password") .setSocialType(randomEle(SocialTypeEnum.values()).getType())); // mock 验证码正确 ReflectUtil.setFieldValue(authService, "captchaEnable", false); // mock user 数据 AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(1L).setUsername("test_username") .setPassword("test_password").setStatus(CommonStatusEnum.ENABLE.getStatus())); when(userService.getUserByUsername(eq("test_username"))).thenReturn(user); // mock password 匹配 when(userService.isPasswordMatch(eq("test_password"), eq(user.getPassword()))).thenReturn(true); // mock 缓存登录用户到 Redis OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) .setUserType(UserTypeEnum.ADMIN.getValue())); when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull())) .thenReturn(accessTokenDO); // 调用,并校验 AuthLoginRespVO loginRespVO = authService.login(reqVO); assertPojoEquals(accessTokenDO, loginRespVO); // 校验调用参数 verify(loginLogService).createLoginLog( argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) && o.getResult().equals(LoginResultEnum.SUCCESS.getResult()) && o.getUserId().equals(user.getId())) ); verify(socialUserService).bindSocialUser(eq(new SocialUserBindReqDTO( user.getId(), UserTypeEnum.ADMIN.getValue(), reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState()))); } @Test public void testSendSmsCode() { // 准备参数 String mobile = randomString(); Integer scene = randomEle(SmsSceneEnum.values()).getScene(); AuthSmsSendReqVO reqVO = new AuthSmsSendReqVO(mobile, scene); // mock 方法(用户信息) AdminUserDO user = randomPojo(AdminUserDO.class); when(userService.getUserByMobile(eq(mobile))).thenReturn(user); // 调用 authService.sendSmsCode(reqVO); // 断言 verify(smsCodeApi).sendSmsCode(argThat(sendReqDTO -> { assertEquals(mobile, sendReqDTO.getMobile()); assertEquals(scene, sendReqDTO.getScene()); return true; })); } @Test public void testSmsLogin_success() { // 准备参数 String mobile = randomString(); String code = randomString(); AuthSmsLoginReqVO reqVO = new AuthSmsLoginReqVO(mobile, code); // mock 方法(用户信息) AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(1L)); when(userService.getUserByMobile(eq(mobile))).thenReturn(user); // mock 缓存登录用户到 Redis OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) .setUserType(UserTypeEnum.ADMIN.getValue())); when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull())) .thenReturn(accessTokenDO); // 调用,并断言 AuthLoginRespVO loginRespVO = authService.smsLogin(reqVO); assertPojoEquals(accessTokenDO, loginRespVO); // 断言调用 verify(loginLogService).createLoginLog( argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_MOBILE.getType()) && o.getResult().equals(LoginResultEnum.SUCCESS.getResult()) && o.getUserId().equals(user.getId())) ); } @Test public void testSocialLogin_success() { // 准备参数 AuthSocialLoginReqVO reqVO = randomPojo(AuthSocialLoginReqVO.class); // mock 方法(绑定的用户编号) Long userId = 1L; when(socialUserService.getSocialUserByCode(eq(UserTypeEnum.ADMIN.getValue()), eq(reqVO.getType()), eq(reqVO.getCode()), eq(reqVO.getState()))).thenReturn(new SocialUserRespDTO(randomString(), randomString(), randomString(), userId)); // mock(用户) AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(userId)); when(userService.getUser(eq(userId))).thenReturn(user); // mock 缓存登录用户到 Redis OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) .setUserType(UserTypeEnum.ADMIN.getValue())); when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull())) .thenReturn(accessTokenDO); // 调用,并断言 AuthLoginRespVO loginRespVO = authService.socialLogin(reqVO); assertPojoEquals(accessTokenDO, loginRespVO); // 断言调用 verify(loginLogService).createLoginLog( argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_SOCIAL.getType()) && o.getResult().equals(LoginResultEnum.SUCCESS.getResult()) && o.getUserId().equals(user.getId())) ); } @Test public void testValidateCaptcha_successWithEnable() { // 准备参数 AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); // mock 验证码打开 ReflectUtil.setFieldValue(authService, "captchaEnable", true); // mock 验证通过 when(captchaService.verification(argThat(captchaVO -> { assertEquals(reqVO.getCaptchaVerification(), captchaVO.getCaptchaVerification()); return true; }))).thenReturn(ResponseModel.success()); // 调用,无需断言 authService.validateCaptcha(reqVO); } @Test public void testValidateCaptcha_successWithDisable() { // 准备参数 AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); // mock 验证码关闭 ReflectUtil.setFieldValue(authService, "captchaEnable", false); // 调用,无需断言 authService.validateCaptcha(reqVO); } @Test public void testValidateCaptcha_constraintViolationException() { // 准备参数 AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class).setCaptchaVerification(null); // mock 验证码打开 ReflectUtil.setFieldValue(authService, "captchaEnable", true); // 调用,并断言异常 assertThrows(ConstraintViolationException.class, () -> authService.validateCaptcha(reqVO), "验证码不能为空"); } @Test public void testCaptcha_fail() { // 准备参数 AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); // mock 验证码打开 ReflectUtil.setFieldValue(authService, "captchaEnable", true); // mock 验证通过 when(captchaService.verification(argThat(captchaVO -> { assertEquals(reqVO.getCaptchaVerification(), captchaVO.getCaptchaVerification()); return true; }))).thenReturn(ResponseModel.errorMsg("就是不对")); // 调用, 并断言异常 assertServiceException(() -> authService.validateCaptcha(reqVO), AUTH_LOGIN_CAPTCHA_CODE_ERROR, "就是不对"); // 校验调用参数 verify(loginLogService).createLoginLog( argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) && o.getResult().equals(LoginResultEnum.CAPTCHA_CODE_ERROR.getResult())) ); } @Test public void testRefreshToken() { // 准备参数 String refreshToken = randomString(); // mock 方法 OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class); when(oauth2TokenService.refreshAccessToken(eq(refreshToken), eq("default"))) .thenReturn(accessTokenDO); // 调用 AuthLoginRespVO loginRespVO = authService.refreshToken(refreshToken); // 断言 assertPojoEquals(accessTokenDO, loginRespVO); } @Test public void testLogout_success() { // 准备参数 String token = randomString(); // mock OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) .setUserType(UserTypeEnum.ADMIN.getValue())); when(oauth2TokenService.removeAccessToken(eq(token))).thenReturn(accessTokenDO); // 调用 authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType()); // 校验调用参数 verify(loginLogService).createLoginLog( argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGOUT_SELF.getType()) && o.getResult().equals(LoginResultEnum.SUCCESS.getResult())) ); // 调用,并校验 } @Test public void testLogout_fail() { // 准备参数 String token = randomString(); // 调用 authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType()); // 校验调用参数 verify(loginLogService, never()).createLoginLog(any()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/dept/DeptServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.dept; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.util.object.ObjectUtils; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.controller.admin.dept.vo.dept.DeptListReqVO; import co.yixiang.yshop.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.dept.DeptDO; import co.yixiang.yshop.module.system.dal.mysql.dept.DeptMapper; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.util.Arrays; import java.util.List; import java.util.Set; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.*; /** * {@link DeptServiceImpl} 的单元测试类 * * @author niudehua */ @Import(DeptServiceImpl.class) public class DeptServiceImplTest extends BaseDbUnitTest { @Resource private DeptServiceImpl deptService; @Resource private DeptMapper deptMapper; @Test public void testCreateDept() { // 准备参数 DeptSaveReqVO reqVO = randomPojo(DeptSaveReqVO.class, o -> { o.setId(null); // 防止 id 被设置 o.setParentId(DeptDO.PARENT_ID_ROOT); o.setStatus(randomCommonStatus()); }); // 调用 Long deptId = deptService.createDept(reqVO); // 断言 assertNotNull(deptId); // 校验记录的属性是否正确 DeptDO deptDO = deptMapper.selectById(deptId); assertPojoEquals(reqVO, deptDO, "id"); } @Test public void testUpdateDept() { // mock 数据 DeptDO dbDeptDO = randomPojo(DeptDO.class, o -> o.setStatus(randomCommonStatus())); deptMapper.insert(dbDeptDO);// @Sql: 先插入出一条存在的数据 // 准备参数 DeptSaveReqVO reqVO = randomPojo(DeptSaveReqVO.class, o -> { // 设置更新的 ID o.setParentId(DeptDO.PARENT_ID_ROOT); o.setId(dbDeptDO.getId()); o.setStatus(randomCommonStatus()); }); // 调用 deptService.updateDept(reqVO); // 校验是否更新正确 DeptDO deptDO = deptMapper.selectById(reqVO.getId()); // 获取最新的 assertPojoEquals(reqVO, deptDO); } @Test public void testDeleteDept_success() { // mock 数据 DeptDO dbDeptDO = randomPojo(DeptDO.class); deptMapper.insert(dbDeptDO);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbDeptDO.getId(); // 调用 deptService.deleteDept(id); // 校验数据不存在了 assertNull(deptMapper.selectById(id)); } @Test public void testDeleteDept_exitsChildren() { // mock 数据 DeptDO parentDept = randomPojo(DeptDO.class); deptMapper.insert(parentDept);// @Sql: 先插入出一条存在的数据 // 准备参数 DeptDO childrenDeptDO = randomPojo(DeptDO.class, o -> { o.setParentId(parentDept.getId()); o.setStatus(randomCommonStatus()); }); // 插入子部门 deptMapper.insert(childrenDeptDO); // 调用, 并断言异常 assertServiceException(() -> deptService.deleteDept(parentDept.getId()), DEPT_EXITS_CHILDREN); } @Test public void testValidateDeptExists_notFound() { // 准备参数 Long id = randomLongId(); // 调用, 并断言异常 assertServiceException(() -> deptService.validateDeptExists(id), DEPT_NOT_FOUND); } @Test public void testValidateParentDept_parentError() { // 准备参数 Long id = randomLongId(); // 调用, 并断言异常 assertServiceException(() -> deptService.validateParentDept(id, id), DEPT_PARENT_ERROR); } @Test public void testValidateParentDept_parentIsChild() { // mock 数据(父节点) DeptDO parentDept = randomPojo(DeptDO.class); deptMapper.insert(parentDept); // mock 数据(子节点) DeptDO childDept = randomPojo(DeptDO.class, o -> { o.setParentId(parentDept.getId()); }); deptMapper.insert(childDept); // 准备参数 Long id = parentDept.getId(); Long parentId = childDept.getId(); // 调用, 并断言异常 assertServiceException(() -> deptService.validateParentDept(id, parentId), DEPT_PARENT_IS_CHILD); } @Test public void testValidateNameUnique_duplicate() { // mock 数据 DeptDO deptDO = randomPojo(DeptDO.class); deptMapper.insert(deptDO); // 准备参数 Long id = randomLongId(); Long parentId = deptDO.getParentId(); String name = deptDO.getName(); // 调用, 并断言异常 assertServiceException(() -> deptService.validateDeptNameUnique(id, parentId, name), DEPT_NAME_DUPLICATE); } @Test public void testGetDept() { // mock 数据 DeptDO deptDO = randomPojo(DeptDO.class); deptMapper.insert(deptDO); // 准备参数 Long id = deptDO.getId(); // 调用 DeptDO dbDept = deptService.getDept(id); // 断言 assertEquals(deptDO, dbDept); } @Test public void testGetDeptList_ids() { // mock 数据 DeptDO deptDO01 = randomPojo(DeptDO.class); deptMapper.insert(deptDO01); DeptDO deptDO02 = randomPojo(DeptDO.class); deptMapper.insert(deptDO02); // 准备参数 List ids = Arrays.asList(deptDO01.getId(), deptDO02.getId()); // 调用 List deptDOList = deptService.getDeptList(ids); // 断言 assertEquals(2, deptDOList.size()); assertEquals(deptDO01, deptDOList.get(0)); assertEquals(deptDO02, deptDOList.get(1)); } @Test public void testGetDeptList_reqVO() { // mock 数据 DeptDO dept = randomPojo(DeptDO.class, o -> { // 等会查询到 o.setName("开发部"); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); }); deptMapper.insert(dept); // 测试 name 不匹配 deptMapper.insert(ObjectUtils.cloneIgnoreId(dept, o -> o.setName("发"))); // 测试 status 不匹配 deptMapper.insert(ObjectUtils.cloneIgnoreId(dept, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); // 准备参数 DeptListReqVO reqVO = new DeptListReqVO(); reqVO.setName("开"); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 调用 List sysDeptDOS = deptService.getDeptList(reqVO); // 断言 assertEquals(1, sysDeptDOS.size()); assertPojoEquals(dept, sysDeptDOS.get(0)); } @Test public void testGetChildDeptList() { // mock 数据(1 级别子节点) DeptDO dept1 = randomPojo(DeptDO.class, o -> o.setName("1")); deptMapper.insert(dept1); DeptDO dept2 = randomPojo(DeptDO.class, o -> o.setName("2")); deptMapper.insert(dept2); // mock 数据(2 级子节点) DeptDO dept1a = randomPojo(DeptDO.class, o -> o.setName("1-a").setParentId(dept1.getId())); deptMapper.insert(dept1a); DeptDO dept2a = randomPojo(DeptDO.class, o -> o.setName("2-a").setParentId(dept2.getId())); deptMapper.insert(dept2a); // 准备参数 Long id = dept1.getParentId(); // 调用 List result = deptService.getChildDeptList(id); // 断言 assertEquals(result.size(), 2); assertPojoEquals(dept1, result.get(0)); assertPojoEquals(dept1a, result.get(1)); } @Test public void testGetChildDeptListFromCache() { // mock 数据(1 级别子节点) DeptDO dept1 = randomPojo(DeptDO.class, o -> o.setName("1")); deptMapper.insert(dept1); DeptDO dept2 = randomPojo(DeptDO.class, o -> o.setName("2")); deptMapper.insert(dept2); // mock 数据(2 级子节点) DeptDO dept1a = randomPojo(DeptDO.class, o -> o.setName("1-a").setParentId(dept1.getId())); deptMapper.insert(dept1a); DeptDO dept2a = randomPojo(DeptDO.class, o -> o.setName("2-a").setParentId(dept2.getId())); deptMapper.insert(dept2a); // 准备参数 Long id = dept1.getParentId(); // 调用 Set result = deptService.getChildDeptIdListFromCache(id); // 断言 assertEquals(result.size(), 2); assertTrue(result.contains(dept1.getId())); assertTrue(result.contains(dept1a.getId())); } @Test public void testValidateDeptList_success() { // mock 数据 DeptDO deptDO = randomPojo(DeptDO.class).setStatus(CommonStatusEnum.ENABLE.getStatus()); deptMapper.insert(deptDO); // 准备参数 List ids = singletonList(deptDO.getId()); // 调用,无需断言 deptService.validateDeptList(ids); } @Test public void testValidateDeptList_notFound() { // 准备参数 List ids = singletonList(randomLongId()); // 调用, 并断言异常 assertServiceException(() -> deptService.validateDeptList(ids), DEPT_NOT_FOUND); } @Test public void testValidateDeptList_notEnable() { // mock 数据 DeptDO deptDO = randomPojo(DeptDO.class).setStatus(CommonStatusEnum.DISABLE.getStatus()); deptMapper.insert(deptDO); // 准备参数 List ids = singletonList(deptDO.getId()); // 调用, 并断言异常 assertServiceException(() -> deptService.validateDeptList(ids), DEPT_NOT_ENABLE, deptDO.getName()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/dept/PostServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.dept; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.collection.ArrayUtils; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.controller.admin.dept.vo.post.PostPageReqVO; import co.yixiang.yshop.module.system.controller.admin.dept.vo.post.PostSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.dept.PostDO; import co.yixiang.yshop.module.system.dal.mysql.dept.PostMapper; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.util.Arrays; import java.util.List; import java.util.function.Consumer; import static cn.hutool.core.util.RandomUtil.randomEle; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.*; /** * {@link PostServiceImpl} 的单元测试类 * * @author niudehua */ @Import(PostServiceImpl.class) public class PostServiceImplTest extends BaseDbUnitTest { @Resource private PostServiceImpl postService; @Resource private PostMapper postMapper; @Test public void testCreatePost_success() { // 准备参数 PostSaveReqVO reqVO = randomPojo(PostSaveReqVO.class, o -> o.setStatus(randomEle(CommonStatusEnum.values()).getStatus())) .setId(null); // 防止 id 被设置 // 调用 Long postId = postService.createPost(reqVO); // 断言 assertNotNull(postId); // 校验记录的属性是否正确 PostDO post = postMapper.selectById(postId); assertPojoEquals(reqVO, post, "id"); } @Test public void testUpdatePost_success() { // mock 数据 PostDO postDO = randomPostDO(); postMapper.insert(postDO);// @Sql: 先插入出一条存在的数据 // 准备参数 PostSaveReqVO reqVO = randomPojo(PostSaveReqVO.class, o -> { // 设置更新的 ID o.setId(postDO.getId()); o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); }); // 调用 postService.updatePost(reqVO); // 校验是否更新正确 PostDO post = postMapper.selectById(reqVO.getId()); assertPojoEquals(reqVO, post); } @Test public void testDeletePost_success() { // mock 数据 PostDO postDO = randomPostDO(); postMapper.insert(postDO); // 准备参数 Long id = postDO.getId(); // 调用 postService.deletePost(id); assertNull(postMapper.selectById(id)); } @Test public void testValidatePost_notFoundForDelete() { // 准备参数 Long id = randomLongId(); // 调用, 并断言异常 assertServiceException(() -> postService.deletePost(id), POST_NOT_FOUND); } @Test public void testValidatePost_nameDuplicateForCreate() { // mock 数据 PostDO postDO = randomPostDO(); postMapper.insert(postDO);// @Sql: 先插入出一条存在的数据 // 准备参数 PostSaveReqVO reqVO = randomPojo(PostSaveReqVO.class, // 模拟 name 重复 o -> o.setName(postDO.getName())); assertServiceException(() -> postService.createPost(reqVO), POST_NAME_DUPLICATE); } @Test public void testValidatePost_codeDuplicateForUpdate() { // mock 数据 PostDO postDO = randomPostDO(); postMapper.insert(postDO); // mock 数据:稍后模拟重复它的 code PostDO codePostDO = randomPostDO(); postMapper.insert(codePostDO); // 准备参数 PostSaveReqVO reqVO = randomPojo(PostSaveReqVO.class, o -> { // 设置更新的 ID o.setId(postDO.getId()); // 模拟 code 重复 o.setCode(codePostDO.getCode()); }); // 调用, 并断言异常 assertServiceException(() -> postService.updatePost(reqVO), POST_CODE_DUPLICATE); } @Test public void testGetPostPage() { // mock 数据 PostDO postDO = randomPojo(PostDO.class, o -> { o.setName("码仔"); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); }); postMapper.insert(postDO); // 测试 name 不匹配 postMapper.insert(cloneIgnoreId(postDO, o -> o.setName("程序员"))); // 测试 status 不匹配 postMapper.insert(cloneIgnoreId(postDO, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); // 准备参数 PostPageReqVO reqVO = new PostPageReqVO(); reqVO.setName("码"); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 调用 PageResult pageResult = postService.getPostPage(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(postDO, pageResult.getList().get(0)); } @Test public void testGetPostList() { // mock 数据 PostDO postDO01 = randomPojo(PostDO.class); postMapper.insert(postDO01); // 测试 id 不匹配 PostDO postDO02 = randomPojo(PostDO.class); postMapper.insert(postDO02); // 准备参数 List ids = singletonList(postDO01.getId()); // 调用 List list = postService.getPostList(ids); // 断言 assertEquals(1, list.size()); assertPojoEquals(postDO01, list.get(0)); } @Test public void testGetPostList_idsAndStatus() { // mock 数据 PostDO postDO01 = randomPojo(PostDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); postMapper.insert(postDO01); // 测试 status 不匹配 PostDO postDO02 = randomPojo(PostDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); postMapper.insert(postDO02); // 准备参数 List ids = Arrays.asList(postDO01.getId(), postDO02.getId()); // 调用 List list = postService.getPostList(ids, singletonList(CommonStatusEnum.ENABLE.getStatus())); // 断言 assertEquals(1, list.size()); assertPojoEquals(postDO01, list.get(0)); } @Test public void testGetPost() { // mock 数据 PostDO dbPostDO = randomPostDO(); postMapper.insert(dbPostDO); // 准备参数 Long id = dbPostDO.getId(); // 调用 PostDO post = postService.getPost(id); // 断言 assertNotNull(post); assertPojoEquals(dbPostDO, post); } @Test public void testValidatePostList_success() { // mock 数据 PostDO postDO = randomPostDO().setStatus(CommonStatusEnum.ENABLE.getStatus()); postMapper.insert(postDO); // 准备参数 List ids = singletonList(postDO.getId()); // 调用,无需断言 postService.validatePostList(ids); } @Test public void testValidatePostList_notFound() { // 准备参数 List ids = singletonList(randomLongId()); // 调用, 并断言异常 assertServiceException(() -> postService.validatePostList(ids), POST_NOT_FOUND); } @Test public void testValidatePostList_notEnable() { // mock 数据 PostDO postDO = randomPostDO().setStatus(CommonStatusEnum.DISABLE.getStatus()); postMapper.insert(postDO); // 准备参数 List ids = singletonList(postDO.getId()); // 调用, 并断言异常 assertServiceException(() -> postService.validatePostList(ids), POST_NOT_ENABLE, postDO.getName()); } @SafeVarargs private static PostDO randomPostDO(Consumer... consumers) { Consumer consumer = (o) -> { o.setStatus(randomCommonStatus()); // 保证 status 的范围 }; return randomPojo(PostDO.class, ArrayUtils.append(consumer, consumers)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/dict/DictDataServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.dict; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.collection.ArrayUtils; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.controller.admin.dict.vo.data.DictDataPageReqVO; import co.yixiang.yshop.module.system.controller.admin.dict.vo.data.DictDataSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.dict.DictDataDO; import co.yixiang.yshop.module.system.dal.dataobject.dict.DictTypeDO; import co.yixiang.yshop.module.system.dal.mysql.dict.DictDataMapper; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.util.List; import java.util.function.Consumer; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; @Import(DictDataServiceImpl.class) public class DictDataServiceImplTest extends BaseDbUnitTest { @Resource private DictDataServiceImpl dictDataService; @Resource private DictDataMapper dictDataMapper; @MockBean private DictTypeService dictTypeService; @Test public void testGetDictDataList() { // mock 数据 DictDataDO dictDataDO01 = randomDictDataDO().setDictType("yshop").setSort(2) .setStatus(CommonStatusEnum.ENABLE.getStatus()); dictDataMapper.insert(dictDataDO01); DictDataDO dictDataDO02 = randomDictDataDO().setDictType("yshop").setSort(1) .setStatus(CommonStatusEnum.ENABLE.getStatus()); dictDataMapper.insert(dictDataDO02); DictDataDO dictDataDO03 = randomDictDataDO().setDictType("yshop").setSort(3) .setStatus(CommonStatusEnum.DISABLE.getStatus()); dictDataMapper.insert(dictDataDO03); DictDataDO dictDataDO04 = randomDictDataDO().setDictType("yshop2").setSort(3) .setStatus(CommonStatusEnum.DISABLE.getStatus()); dictDataMapper.insert(dictDataDO04); // 准备参数 Integer status = CommonStatusEnum.ENABLE.getStatus(); String dictType = "yshop"; // 调用 List dictDataDOList = dictDataService.getDictDataList(status, dictType); // 断言 assertEquals(2, dictDataDOList.size()); assertPojoEquals(dictDataDO02, dictDataDOList.get(0)); assertPojoEquals(dictDataDO01, dictDataDOList.get(1)); } @Test public void testGetDictDataPage() { // mock 数据 DictDataDO dbDictData = randomPojo(DictDataDO.class, o -> { // 等会查询到 o.setLabel("yshop"); o.setDictType("yshop"); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); }); dictDataMapper.insert(dbDictData); // 测试 label 不匹配 dictDataMapper.insert(cloneIgnoreId(dbDictData, o -> o.setLabel("艿"))); // 测试 dictType 不匹配 dictDataMapper.insert(cloneIgnoreId(dbDictData, o -> o.setDictType("nai"))); // 测试 status 不匹配 dictDataMapper.insert(cloneIgnoreId(dbDictData, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); // 准备参数 DictDataPageReqVO reqVO = new DictDataPageReqVO(); reqVO.setLabel("芋"); reqVO.setDictType("yshop"); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 调用 PageResult pageResult = dictDataService.getDictDataPage(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbDictData, pageResult.getList().get(0)); } @Test public void testGetDictData() { // mock 数据 DictDataDO dbDictData = randomDictDataDO(); dictDataMapper.insert(dbDictData); // 准备参数 Long id = dbDictData.getId(); // 调用 DictDataDO dictData = dictDataService.getDictData(id); // 断言 assertPojoEquals(dbDictData, dictData); } @Test public void testCreateDictData_success() { // 准备参数 DictDataSaveReqVO reqVO = randomPojo(DictDataSaveReqVO.class, o -> o.setStatus(randomCommonStatus())) .setId(null); // 防止 id 被赋值 // mock 方法 when(dictTypeService.getDictType(eq(reqVO.getDictType()))).thenReturn(randomDictTypeDO(reqVO.getDictType())); // 调用 Long dictDataId = dictDataService.createDictData(reqVO); // 断言 assertNotNull(dictDataId); // 校验记录的属性是否正确 DictDataDO dictData = dictDataMapper.selectById(dictDataId); assertPojoEquals(reqVO, dictData, "id"); } @Test public void testUpdateDictData_success() { // mock 数据 DictDataDO dbDictData = randomDictDataDO(); dictDataMapper.insert(dbDictData);// @Sql: 先插入出一条存在的数据 // 准备参数 DictDataSaveReqVO reqVO = randomPojo(DictDataSaveReqVO.class, o -> { o.setId(dbDictData.getId()); // 设置更新的 ID o.setStatus(randomCommonStatus()); }); // mock 方法,字典类型 when(dictTypeService.getDictType(eq(reqVO.getDictType()))).thenReturn(randomDictTypeDO(reqVO.getDictType())); // 调用 dictDataService.updateDictData(reqVO); // 校验是否更新正确 DictDataDO dictData = dictDataMapper.selectById(reqVO.getId()); // 获取最新的 assertPojoEquals(reqVO, dictData); } @Test public void testDeleteDictData_success() { // mock 数据 DictDataDO dbDictData = randomDictDataDO(); dictDataMapper.insert(dbDictData);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbDictData.getId(); // 调用 dictDataService.deleteDictData(id); // 校验数据不存在了 assertNull(dictDataMapper.selectById(id)); } @Test public void testValidateDictDataExists_success() { // mock 数据 DictDataDO dbDictData = randomDictDataDO(); dictDataMapper.insert(dbDictData);// @Sql: 先插入出一条存在的数据 // 调用成功 dictDataService.validateDictDataExists(dbDictData.getId()); } @Test public void testValidateDictDataExists_notExists() { assertServiceException(() -> dictDataService.validateDictDataExists(randomLongId()), DICT_DATA_NOT_EXISTS); } @Test public void testValidateDictTypeExists_success() { // mock 方法,数据类型被禁用 String type = randomString(); when(dictTypeService.getDictType(eq(type))).thenReturn(randomDictTypeDO(type)); // 调用, 成功 dictDataService.validateDictTypeExists(type); } @Test public void testValidateDictTypeExists_notExists() { assertServiceException(() -> dictDataService.validateDictTypeExists(randomString()), DICT_TYPE_NOT_EXISTS); } @Test public void testValidateDictTypeExists_notEnable() { // mock 方法,数据类型被禁用 String dictType = randomString(); when(dictTypeService.getDictType(eq(dictType))).thenReturn( randomPojo(DictTypeDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); // 调用, 并断言异常 assertServiceException(() -> dictDataService.validateDictTypeExists(dictType), DICT_TYPE_NOT_ENABLE); } @Test public void testValidateDictDataValueUnique_success() { // 调用,成功 dictDataService.validateDictDataValueUnique(randomLongId(), randomString(), randomString()); } @Test public void testValidateDictDataValueUnique_valueDuplicateForCreate() { // 准备参数 String dictType = randomString(); String value = randomString(); // mock 数据 dictDataMapper.insert(randomDictDataDO(o -> { o.setDictType(dictType); o.setValue(value); })); // 调用,校验异常 assertServiceException(() -> dictDataService.validateDictDataValueUnique(null, dictType, value), DICT_DATA_VALUE_DUPLICATE); } @Test public void testValidateDictDataValueUnique_valueDuplicateForUpdate() { // 准备参数 Long id = randomLongId(); String dictType = randomString(); String value = randomString(); // mock 数据 dictDataMapper.insert(randomDictDataDO(o -> { o.setDictType(dictType); o.setValue(value); })); // 调用,校验异常 assertServiceException(() -> dictDataService.validateDictDataValueUnique(id, dictType, value), DICT_DATA_VALUE_DUPLICATE); } @Test public void testGetDictDataCountByDictType() { // mock 数据 dictDataMapper.insert(randomDictDataDO(o -> o.setDictType("yshop"))); dictDataMapper.insert(randomDictDataDO(o -> o.setDictType("tudou"))); dictDataMapper.insert(randomDictDataDO(o -> o.setDictType("yshop"))); // 准备参数 String dictType = "yshop"; // 调用 long count = dictDataService.getDictDataCountByDictType(dictType); // 校验 assertEquals(2L, count); } @Test public void testValidateDictDataList_success() { // mock 数据 DictDataDO dictDataDO = randomDictDataDO().setStatus(CommonStatusEnum.ENABLE.getStatus()); dictDataMapper.insert(dictDataDO); // 准备参数 String dictType = dictDataDO.getDictType(); List values = singletonList(dictDataDO.getValue()); // 调用,无需断言 dictDataService.validateDictDataList(dictType, values); } @Test public void testValidateDictDataList_notFound() { // 准备参数 String dictType = randomString(); List values = singletonList(randomString()); // 调用, 并断言异常 assertServiceException(() -> dictDataService.validateDictDataList(dictType, values), DICT_DATA_NOT_EXISTS); } @Test public void testValidateDictDataList_notEnable() { // mock 数据 DictDataDO dictDataDO = randomDictDataDO().setStatus(CommonStatusEnum.DISABLE.getStatus()); dictDataMapper.insert(dictDataDO); // 准备参数 String dictType = dictDataDO.getDictType(); List values = singletonList(dictDataDO.getValue()); // 调用, 并断言异常 assertServiceException(() -> dictDataService.validateDictDataList(dictType, values), DICT_DATA_NOT_ENABLE, dictDataDO.getLabel()); } @Test public void testGetDictData_dictType() { // mock 数据 DictDataDO dictDataDO = randomDictDataDO().setDictType("yshop").setValue("1"); dictDataMapper.insert(dictDataDO); DictDataDO dictDataDO02 = randomDictDataDO().setDictType("yshop").setValue("2"); dictDataMapper.insert(dictDataDO02); // 准备参数 String dictType = "yshop"; String value = "1"; // 调用 DictDataDO dbDictData = dictDataService.getDictData(dictType, value); // 断言 assertEquals(dictDataDO, dbDictData); } @Test public void testParseDictData() { // mock 数据 DictDataDO dictDataDO = randomDictDataDO().setDictType("yshop").setLabel("1"); dictDataMapper.insert(dictDataDO); DictDataDO dictDataDO02 = randomDictDataDO().setDictType("yshop").setLabel("2"); dictDataMapper.insert(dictDataDO02); // 准备参数 String dictType = "yshop"; String label = "1"; // 调用 DictDataDO dbDictData = dictDataService.parseDictData(dictType, label); // 断言 assertEquals(dictDataDO, dbDictData); } // ========== 随机对象 ========== @SafeVarargs private static DictDataDO randomDictDataDO(Consumer... consumers) { Consumer consumer = (o) -> { o.setStatus(randomCommonStatus()); // 保证 status 的范围 }; return randomPojo(DictDataDO.class, ArrayUtils.append(consumer, consumers)); } /** * 生成一个有效的字典类型 * * @param type 字典类型 * @return DictTypeDO 对象 */ private static DictTypeDO randomDictTypeDO(String type) { return randomPojo(DictTypeDO.class, o -> { o.setType(type); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 保证 status 是开启 }); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/dict/DictTypeServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.dict; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.collection.ArrayUtils; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.controller.admin.dict.vo.type.DictTypePageReqVO; import co.yixiang.yshop.module.system.controller.admin.dict.vo.type.DictTypeSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.dict.DictTypeDO; import co.yixiang.yshop.module.system.dal.mysql.dict.DictTypeMapper; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.util.List; import java.util.function.Consumer; import static cn.hutool.core.util.RandomUtil.randomEle; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildTime; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; @Import(DictTypeServiceImpl.class) public class DictTypeServiceImplTest extends BaseDbUnitTest { @Resource private DictTypeServiceImpl dictTypeService; @Resource private DictTypeMapper dictTypeMapper; @MockBean private DictDataService dictDataService; @Test public void testGetDictTypePage() { // mock 数据 DictTypeDO dbDictType = randomPojo(DictTypeDO.class, o -> { // 等会查询到 o.setName("yshop"); o.setType("yshop"); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setCreateTime(buildTime(2021, 1, 15)); }); dictTypeMapper.insert(dbDictType); // 测试 name 不匹配 dictTypeMapper.insert(cloneIgnoreId(dbDictType, o -> o.setName("tudou"))); // 测试 type 不匹配 dictTypeMapper.insert(cloneIgnoreId(dbDictType, o -> o.setType("土豆"))); // 测试 status 不匹配 dictTypeMapper.insert(cloneIgnoreId(dbDictType, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); // 测试 createTime 不匹配 dictTypeMapper.insert(cloneIgnoreId(dbDictType, o -> o.setCreateTime(buildTime(2021, 1, 1)))); // 准备参数 DictTypePageReqVO reqVO = new DictTypePageReqVO(); reqVO.setName("nai"); reqVO.setType("艿"); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); reqVO.setCreateTime(buildBetweenTime(2021, 1, 10, 2021, 1, 20)); // 调用 PageResult pageResult = dictTypeService.getDictTypePage(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbDictType, pageResult.getList().get(0)); } @Test public void testGetDictType_id() { // mock 数据 DictTypeDO dbDictType = randomDictTypeDO(); dictTypeMapper.insert(dbDictType); // 准备参数 Long id = dbDictType.getId(); // 调用 DictTypeDO dictType = dictTypeService.getDictType(id); // 断言 assertNotNull(dictType); assertPojoEquals(dbDictType, dictType); } @Test public void testGetDictType_type() { // mock 数据 DictTypeDO dbDictType = randomDictTypeDO(); dictTypeMapper.insert(dbDictType); // 准备参数 String type = dbDictType.getType(); // 调用 DictTypeDO dictType = dictTypeService.getDictType(type); // 断言 assertNotNull(dictType); assertPojoEquals(dbDictType, dictType); } @Test public void testCreateDictType_success() { // 准备参数 DictTypeSaveReqVO reqVO = randomPojo(DictTypeSaveReqVO.class, o -> o.setStatus(randomEle(CommonStatusEnum.values()).getStatus())) .setId(null); // 避免 id 被赋值 // 调用 Long dictTypeId = dictTypeService.createDictType(reqVO); // 断言 assertNotNull(dictTypeId); // 校验记录的属性是否正确 DictTypeDO dictType = dictTypeMapper.selectById(dictTypeId); assertPojoEquals(reqVO, dictType, "id"); } @Test public void testUpdateDictType_success() { // mock 数据 DictTypeDO dbDictType = randomDictTypeDO(); dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据 // 准备参数 DictTypeSaveReqVO reqVO = randomPojo(DictTypeSaveReqVO.class, o -> { o.setId(dbDictType.getId()); // 设置更新的 ID o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); }); // 调用 dictTypeService.updateDictType(reqVO); // 校验是否更新正确 DictTypeDO dictType = dictTypeMapper.selectById(reqVO.getId()); // 获取最新的 assertPojoEquals(reqVO, dictType); } @Test public void testDeleteDictType_success() { // mock 数据 DictTypeDO dbDictType = randomDictTypeDO(); dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbDictType.getId(); // 调用 dictTypeService.deleteDictType(id); // 校验数据不存在了 assertNull(dictTypeMapper.selectById(id)); } @Test public void testDeleteDictType_hasChildren() { // mock 数据 DictTypeDO dbDictType = randomDictTypeDO(); dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbDictType.getId(); // mock 方法 when(dictDataService.getDictDataCountByDictType(eq(dbDictType.getType()))).thenReturn(1L); // 调用, 并断言异常 assertServiceException(() -> dictTypeService.deleteDictType(id), DICT_TYPE_HAS_CHILDREN); } @Test public void testGetDictTypeList() { // 准备参数 DictTypeDO dictTypeDO01 = randomDictTypeDO(); dictTypeMapper.insert(dictTypeDO01); DictTypeDO dictTypeDO02 = randomDictTypeDO(); dictTypeMapper.insert(dictTypeDO02); // mock 方法 // 调用 List dictTypeDOList = dictTypeService.getDictTypeList(); // 断言 assertEquals(2, dictTypeDOList.size()); assertPojoEquals(dictTypeDO01, dictTypeDOList.get(0)); assertPojoEquals(dictTypeDO02, dictTypeDOList.get(1)); } @Test public void testValidateDictDataExists_success() { // mock 数据 DictTypeDO dbDictType = randomDictTypeDO(); dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据 // 调用成功 dictTypeService.validateDictTypeExists(dbDictType.getId()); } @Test public void testValidateDictDataExists_notExists() { assertServiceException(() -> dictTypeService.validateDictTypeExists(randomLongId()), DICT_TYPE_NOT_EXISTS); } @Test public void testValidateDictTypeUnique_success() { // 调用,成功 dictTypeService.validateDictTypeUnique(randomLongId(), randomString()); } @Test public void testValidateDictTypeUnique_valueDuplicateForCreate() { // 准备参数 String type = randomString(); // mock 数据 dictTypeMapper.insert(randomDictTypeDO(o -> o.setType(type))); // 调用,校验异常 assertServiceException(() -> dictTypeService.validateDictTypeUnique(null, type), DICT_TYPE_TYPE_DUPLICATE); } @Test public void testValidateDictTypeUnique_valueDuplicateForUpdate() { // 准备参数 Long id = randomLongId(); String type = randomString(); // mock 数据 dictTypeMapper.insert(randomDictTypeDO(o -> o.setType(type))); // 调用,校验异常 assertServiceException(() -> dictTypeService.validateDictTypeUnique(id, type), DICT_TYPE_TYPE_DUPLICATE); } @Test public void testValidateDictTypNameUnique_success() { // 调用,成功 dictTypeService.validateDictTypeNameUnique(randomLongId(), randomString()); } @Test public void testValidateDictTypeNameUnique_nameDuplicateForCreate() { // 准备参数 String name = randomString(); // mock 数据 dictTypeMapper.insert(randomDictTypeDO(o -> o.setName(name))); // 调用,校验异常 assertServiceException(() -> dictTypeService.validateDictTypeNameUnique(null, name), DICT_TYPE_NAME_DUPLICATE); } @Test public void testValidateDictTypeNameUnique_nameDuplicateForUpdate() { // 准备参数 Long id = randomLongId(); String name = randomString(); // mock 数据 dictTypeMapper.insert(randomDictTypeDO(o -> o.setName(name))); // 调用,校验异常 assertServiceException(() -> dictTypeService.validateDictTypeNameUnique(id, name), DICT_TYPE_NAME_DUPLICATE); } // ========== 随机对象 ========== @SafeVarargs private static DictTypeDO randomDictTypeDO(Consumer... consumers) { Consumer consumer = (o) -> { o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 }; return randomPojo(DictTypeDO.class, ArrayUtils.append(consumer, consumers)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/logger/LoginLogServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.logger; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.api.logger.dto.LoginLogCreateReqDTO; import co.yixiang.yshop.module.system.controller.admin.logger.vo.loginlog.LoginLogPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.logger.LoginLogDO; import co.yixiang.yshop.module.system.dal.mysql.logger.LoginLogMapper; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildTime; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomPojo; import static co.yixiang.yshop.module.system.enums.logger.LoginResultEnum.CAPTCHA_CODE_ERROR; import static co.yixiang.yshop.module.system.enums.logger.LoginResultEnum.SUCCESS; import static org.junit.jupiter.api.Assertions.assertEquals; @Import(LoginLogServiceImpl.class) public class LoginLogServiceImplTest extends BaseDbUnitTest { @Resource private LoginLogServiceImpl loginLogService; @Resource private LoginLogMapper loginLogMapper; @Test public void testGetLoginLogPage() { // mock 数据 LoginLogDO loginLogDO = randomPojo(LoginLogDO.class, o -> { o.setUserIp("192.168.199.16"); o.setUsername("wang"); o.setResult(SUCCESS.getResult()); o.setCreateTime(buildTime(2021, 3, 6)); }); loginLogMapper.insert(loginLogDO); // 测试 status 不匹配 loginLogMapper.insert(cloneIgnoreId(loginLogDO, o -> o.setResult(CAPTCHA_CODE_ERROR.getResult()))); // 测试 ip 不匹配 loginLogMapper.insert(cloneIgnoreId(loginLogDO, o -> o.setUserIp("192.168.128.18"))); // 测试 username 不匹配 loginLogMapper.insert(cloneIgnoreId(loginLogDO, o -> o.setUsername("yshop"))); // 测试 createTime 不匹配 loginLogMapper.insert(cloneIgnoreId(loginLogDO, o -> o.setCreateTime(buildTime(2021, 2, 6)))); // 构造调用参数 LoginLogPageReqVO reqVO = new LoginLogPageReqVO(); reqVO.setUsername("wang"); reqVO.setUserIp("192.168.199"); reqVO.setStatus(true); reqVO.setCreateTime(buildBetweenTime(2021, 3, 5, 2021, 3, 7)); // 调用 PageResult pageResult = loginLogService.getLoginLogPage(reqVO); // 断言,只查到了一条符合条件的 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(loginLogDO, pageResult.getList().get(0)); } @Test public void testCreateLoginLog() { LoginLogCreateReqDTO reqDTO = randomPojo(LoginLogCreateReqDTO.class); // 调用 loginLogService.createLoginLog(reqDTO); // 断言 LoginLogDO loginLogDO = loginLogMapper.selectOne(null); assertPojoEquals(reqDTO, loginLogDO); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/logger/OperateLogServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.logger; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.framework.test.core.util.RandomUtils; import co.yixiang.yshop.module.system.api.logger.dto.OperateLogCreateReqDTO; import co.yixiang.yshop.module.system.api.logger.dto.OperateLogPageReqDTO; import co.yixiang.yshop.module.system.controller.admin.logger.vo.operatelog.OperateLogPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.logger.OperateLogDO; import co.yixiang.yshop.module.system.dal.mysql.logger.OperateLogMapper; import jakarta.annotation.Resource; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildTime; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static org.junit.jupiter.api.Assertions.assertEquals; @Import({OperateLogServiceImpl.class}) public class OperateLogServiceImplTest extends BaseDbUnitTest { @Resource private OperateLogService operateLogServiceImpl; @Resource private OperateLogMapper operateLogMapper; @Test public void testCreateOperateLog() { OperateLogCreateReqDTO reqVO = RandomUtils.randomPojo(OperateLogCreateReqDTO.class); // 调研 operateLogServiceImpl.createOperateLog(reqVO); // 断言 OperateLogDO operateLogDO = operateLogMapper.selectOne(null); assertPojoEquals(reqVO, operateLogDO); } @Test public void testGetOperateLogPage_vo() { // 构造操作日志 OperateLogDO operateLogDO = RandomUtils.randomPojo(OperateLogDO.class, o -> { o.setUserId(2048L); o.setBizId(999L); o.setType("订单"); o.setSubType("创建订单"); o.setAction("修改编号为 1 的用户信息"); o.setCreateTime(buildTime(2021, 3, 6)); }); operateLogMapper.insert(operateLogDO); // 测试 userId 不匹配 operateLogMapper.insert(cloneIgnoreId(operateLogDO, o -> o.setUserId(1024L))); // 测试 bizId 不匹配 operateLogMapper.insert(cloneIgnoreId(operateLogDO, o -> o.setBizId(888L))); // 测试 type 不匹配 operateLogMapper.insert(cloneIgnoreId(operateLogDO, o -> o.setType("退款"))); // 测试 subType 不匹配 operateLogMapper.insert(cloneIgnoreId(operateLogDO, o -> o.setSubType("创建退款"))); // 测试 action 不匹配 operateLogMapper.insert(cloneIgnoreId(operateLogDO, o -> o.setAction("修改编号为 1 退款信息"))); // 测试 createTime 不匹配 operateLogMapper.insert(cloneIgnoreId(operateLogDO, o -> o.setCreateTime(buildTime(2021, 2, 6)))); // 构造调用参数 OperateLogPageReqVO reqVO = new OperateLogPageReqVO(); reqVO.setUserId(2048L); reqVO.setBizId(999L); reqVO.setType("订"); reqVO.setSubType("订单"); reqVO.setAction("用户信息"); reqVO.setCreateTime(buildBetweenTime(2021, 3, 5, 2021, 3, 7)); // 调用 PageResult pageResult = operateLogServiceImpl.getOperateLogPage(reqVO); // 断言,只查到了一条符合条件的 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(operateLogDO, pageResult.getList().get(0)); } @Test public void testGetOperateLogPage_dto() { // 构造操作日志 OperateLogDO operateLogDO = RandomUtils.randomPojo(OperateLogDO.class, o -> { o.setUserId(2048L); o.setBizId(999L); o.setType("订单"); }); operateLogMapper.insert(operateLogDO); // 测试 userId 不匹配 operateLogMapper.insert(cloneIgnoreId(operateLogDO, o -> o.setUserId(1024L))); // 测试 bizId 不匹配 operateLogMapper.insert(cloneIgnoreId(operateLogDO, o -> o.setBizId(888L))); // 测试 type 不匹配 operateLogMapper.insert(cloneIgnoreId(operateLogDO, o -> o.setType("退款"))); // 构造调用参数 OperateLogPageReqDTO reqDTO = new OperateLogPageReqDTO(); reqDTO.setUserId(2048L); reqDTO.setBizId(999L); reqDTO.setType("订单"); // 调用 PageResult pageResult = operateLogServiceImpl.getOperateLogPage(reqDTO); // 断言,只查到了一条符合条件的 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(operateLogDO, pageResult.getList().get(0)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/mail/MailAccountServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.mail; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.controller.admin.mail.vo.account.MailAccountPageReqVO; import co.yixiang.yshop.module.system.controller.admin.mail.vo.account.MailAccountSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailAccountDO; import co.yixiang.yshop.module.system.dal.mysql.mail.MailAccountMapper; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.util.List; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.MAIL_ACCOUNT_NOT_EXISTS; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; /** * {@link MailAccountServiceImpl} 的单元测试类 * * @author yshop */ @Import(MailAccountServiceImpl.class) public class MailAccountServiceImplTest extends BaseDbUnitTest { @Resource private MailAccountServiceImpl mailAccountService; @Resource private MailAccountMapper mailAccountMapper; @MockBean private MailTemplateService mailTemplateService; @Test public void testCreateMailAccount_success() { // 准备参数 MailAccountSaveReqVO reqVO = randomPojo(MailAccountSaveReqVO.class, o -> o.setMail(randomEmail())) .setId(null); // 防止 id 被赋值 // 调用 Long mailAccountId = mailAccountService.createMailAccount(reqVO); // 断言 assertNotNull(mailAccountId); // 校验记录的属性是否正确 MailAccountDO mailAccount = mailAccountMapper.selectById(mailAccountId); assertPojoEquals(reqVO, mailAccount, "id"); } @Test public void testUpdateMailAccount_success() { // mock 数据 MailAccountDO dbMailAccount = randomPojo(MailAccountDO.class); mailAccountMapper.insert(dbMailAccount);// @Sql: 先插入出一条存在的数据 // 准备参数 MailAccountSaveReqVO reqVO = randomPojo(MailAccountSaveReqVO.class, o -> { o.setId(dbMailAccount.getId()); // 设置更新的 ID o.setMail(randomEmail()); }); // 调用 mailAccountService.updateMailAccount(reqVO); // 校验是否更新正确 MailAccountDO mailAccount = mailAccountMapper.selectById(reqVO.getId()); // 获取最新的 assertPojoEquals(reqVO, mailAccount); } @Test public void testUpdateMailAccount_notExists() { // 准备参数 MailAccountSaveReqVO reqVO = randomPojo(MailAccountSaveReqVO.class); // 调用, 并断言异常 assertServiceException(() -> mailAccountService.updateMailAccount(reqVO), MAIL_ACCOUNT_NOT_EXISTS); } @Test public void testDeleteMailAccount_success() { // mock 数据 MailAccountDO dbMailAccount = randomPojo(MailAccountDO.class); mailAccountMapper.insert(dbMailAccount);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbMailAccount.getId(); // mock 方法(无关联模版) when(mailTemplateService.getMailTemplateCountByAccountId(eq(id))).thenReturn(0L); // 调用 mailAccountService.deleteMailAccount(id); // 校验数据不存在了 assertNull(mailAccountMapper.selectById(id)); } @Test public void testGetMailAccountFromCache() { // mock 数据 MailAccountDO dbMailAccount = randomPojo(MailAccountDO.class); mailAccountMapper.insert(dbMailAccount);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbMailAccount.getId(); // 调用 MailAccountDO mailAccount = mailAccountService.getMailAccountFromCache(id); // 断言 assertPojoEquals(dbMailAccount, mailAccount); } @Test public void testDeleteMailAccount_notExists() { // 准备参数 Long id = randomLongId(); // 调用, 并断言异常 assertServiceException(() -> mailAccountService.deleteMailAccount(id), MAIL_ACCOUNT_NOT_EXISTS); } @Test public void testGetMailAccountPage() { // mock 数据 MailAccountDO dbMailAccount = randomPojo(MailAccountDO.class, o -> { // 等会查询到 o.setMail("768@qq.com"); o.setUsername("yshop"); }); mailAccountMapper.insert(dbMailAccount); // 测试 mail 不匹配 mailAccountMapper.insert(cloneIgnoreId(dbMailAccount, o -> o.setMail("788@qq.com"))); // 测试 username 不匹配 mailAccountMapper.insert(cloneIgnoreId(dbMailAccount, o -> o.setUsername("tudou"))); // 准备参数 MailAccountPageReqVO reqVO = new MailAccountPageReqVO(); reqVO.setMail("768"); reqVO.setUsername("yu"); // 调用 PageResult pageResult = mailAccountService.getMailAccountPage(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbMailAccount, pageResult.getList().get(0)); } @Test public void testGetMailAccount() { // mock 数据 MailAccountDO dbMailAccount = randomPojo(MailAccountDO.class); mailAccountMapper.insert(dbMailAccount);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbMailAccount.getId(); // 调用 MailAccountDO mailAccount = mailAccountService.getMailAccount(id); // 断言 assertPojoEquals(dbMailAccount, mailAccount); } @Test public void testGetMailAccountList() { // mock 数据 MailAccountDO dbMailAccount01 = randomPojo(MailAccountDO.class); mailAccountMapper.insert(dbMailAccount01); MailAccountDO dbMailAccount02 = randomPojo(MailAccountDO.class); mailAccountMapper.insert(dbMailAccount02); // 准备参数 // 调用 List list = mailAccountService.getMailAccountList(); // 断言 assertEquals(2, list.size()); assertPojoEquals(dbMailAccount01, list.get(0)); assertPojoEquals(dbMailAccount02, list.get(1)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/mail/MailLogServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.mail; import cn.hutool.core.map.MapUtil; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.controller.admin.mail.vo.log.MailLogPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailAccountDO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailLogDO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailTemplateDO; import co.yixiang.yshop.module.system.dal.mysql.mail.MailLogMapper; import co.yixiang.yshop.module.system.enums.mail.MailSendStatusEnum; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.util.Map; import static cn.hutool.core.util.RandomUtil.randomEle; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildTime; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static org.junit.jupiter.api.Assertions.*; /** * {@link MailLogServiceImpl} 的单元测试类 * * @author yshop */ @Import(MailLogServiceImpl.class) public class MailLogServiceImplTest extends BaseDbUnitTest { @Resource private MailLogServiceImpl mailLogService; @Resource private MailLogMapper mailLogMapper; @Test public void testCreateMailLog() { // 准备参数 Long userId = randomLongId(); Integer userType = randomEle(UserTypeEnum.values()).getValue(); String toMail = randomEmail(); MailAccountDO account = randomPojo(MailAccountDO.class); MailTemplateDO template = randomPojo(MailTemplateDO.class); String templateContent = randomString(); Map templateParams = randomTemplateParams(); Boolean isSend = true; // mock 方法 // 调用 Long logId = mailLogService.createMailLog(userId, userType, toMail, account, template, templateContent, templateParams, isSend); // 断言 MailLogDO log = mailLogMapper.selectById(logId); assertNotNull(log); assertEquals(MailSendStatusEnum.INIT.getStatus(), log.getSendStatus()); assertEquals(userId, log.getUserId()); assertEquals(userType, log.getUserType()); assertEquals(toMail, log.getToMail()); assertEquals(account.getId(), log.getAccountId()); assertEquals(account.getMail(), log.getFromMail()); assertEquals(template.getId(), log.getTemplateId()); assertEquals(template.getCode(), log.getTemplateCode()); assertEquals(template.getNickname(), log.getTemplateNickname()); assertEquals(template.getTitle(), log.getTemplateTitle()); assertEquals(templateContent, log.getTemplateContent()); assertEquals(templateParams, log.getTemplateParams()); } @Test public void testUpdateMailSendResult_success() { // mock 数据 MailLogDO log = randomPojo(MailLogDO.class, o -> { o.setSendStatus(MailSendStatusEnum.INIT.getStatus()); o.setSendTime(null).setSendMessageId(null).setSendException(null) .setTemplateParams(randomTemplateParams()); }); mailLogMapper.insert(log); // 准备参数 Long logId = log.getId(); String messageId = randomString(); // 调用 mailLogService.updateMailSendResult(logId, messageId, null); // 断言 MailLogDO dbLog = mailLogMapper.selectById(logId); assertEquals(MailSendStatusEnum.SUCCESS.getStatus(), dbLog.getSendStatus()); assertNotNull(dbLog.getSendTime()); assertEquals(messageId, dbLog.getSendMessageId()); assertNull(dbLog.getSendException()); } @Test public void testUpdateMailSendResult_exception() { // mock 数据 MailLogDO log = randomPojo(MailLogDO.class, o -> { o.setSendStatus(MailSendStatusEnum.INIT.getStatus()); o.setSendTime(null).setSendMessageId(null).setSendException(null) .setTemplateParams(randomTemplateParams()); }); mailLogMapper.insert(log); // 准备参数 Long logId = log.getId(); Exception exception = new NullPointerException("测试异常"); // 调用 mailLogService.updateMailSendResult(logId, null, exception); // 断言 MailLogDO dbLog = mailLogMapper.selectById(logId); assertEquals(MailSendStatusEnum.FAILURE.getStatus(), dbLog.getSendStatus()); assertNotNull(dbLog.getSendTime()); assertNull(dbLog.getSendMessageId()); assertEquals("NullPointerException: 测试异常", dbLog.getSendException()); } @Test public void testGetMailLog() { // mock 数据 MailLogDO dbMailLog = randomPojo(MailLogDO.class, o -> o.setTemplateParams(randomTemplateParams())); mailLogMapper.insert(dbMailLog); // 准备参数 Long id = dbMailLog.getId(); // 调用 MailLogDO mailLog = mailLogService.getMailLog(id); // 断言 assertPojoEquals(dbMailLog, mailLog); } @Test public void testGetMailLogPage() { // mock 数据 MailLogDO dbMailLog = randomPojo(MailLogDO.class, o -> { // 等会查询到 o.setUserId(1L); o.setUserType(UserTypeEnum.ADMIN.getValue()); o.setToMail("768@qq.com"); o.setAccountId(10L); o.setTemplateId(100L); o.setSendStatus(MailSendStatusEnum.INIT.getStatus()); o.setSendTime(buildTime(2023, 2, 10)); o.setTemplateParams(randomTemplateParams()); }); mailLogMapper.insert(dbMailLog); // 测试 userId 不匹配 mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setUserId(2L))); // 测试 userType 不匹配 mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); // 测试 toMail 不匹配 mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setToMail("788@.qq.com"))); // 测试 accountId 不匹配 mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setAccountId(11L))); // 测试 templateId 不匹配 mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setTemplateId(101L))); // 测试 sendStatus 不匹配 mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setSendStatus(MailSendStatusEnum.SUCCESS.getStatus()))); // 测试 sendTime 不匹配 mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setSendTime(buildTime(2023, 3, 10)))); // 准备参数 MailLogPageReqVO reqVO = new MailLogPageReqVO(); reqVO.setUserId(1L); reqVO.setUserType(UserTypeEnum.ADMIN.getValue()); reqVO.setToMail("768"); reqVO.setAccountId(10L); reqVO.setTemplateId(100L); reqVO.setSendStatus(MailSendStatusEnum.INIT.getStatus()); reqVO.setSendTime((buildBetweenTime(2023, 2, 1, 2023, 2, 15))); // 调用 PageResult pageResult = mailLogService.getMailLogPage(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbMailLog, pageResult.getList().get(0)); } private static Map randomTemplateParams() { return MapUtil.builder().put(randomString(), randomString()) .put(randomString(), randomString()).build(); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/mail/MailSendServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.mail; import cn.hutool.core.map.MapUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.test.core.ut.BaseMockitoUnitTest; import co.yixiang.yshop.framework.test.core.util.RandomUtils; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailAccountDO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailTemplateDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.module.system.mq.message.mail.MailSendMessage; import co.yixiang.yshop.module.system.mq.producer.mail.MailProducer; import co.yixiang.yshop.module.system.service.member.MemberService; import co.yixiang.yshop.module.system.service.user.AdminUserService; import org.assertj.core.util.Lists; import org.dromara.hutool.extra.mail.*; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; import java.util.HashMap; import java.util.Map; import static cn.hutool.core.util.RandomUtil.randomEle; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; public class MailSendServiceImplTest extends BaseMockitoUnitTest { @InjectMocks private MailSendServiceImpl mailSendService; @Mock private AdminUserService adminUserService; @Mock private MemberService memberService; @Mock private MailAccountService mailAccountService; @Mock private MailTemplateService mailTemplateService; @Mock private MailLogService mailLogService; @Mock private MailProducer mailProducer; /** * 用于快速测试你的邮箱账号是否正常 */ @Test @Disabled public void testDemo() { MailAccount mailAccount = new MailAccount() // .setFrom("奥特曼 ") .setFrom("ydym_test@163.com") // 邮箱地址 .setHost("smtp.163.com").setPort(465).setSslEnable(true) // SMTP 服务器 .setAuth(true).setUser("ydym_test@163.com").setPass("WBZTEINMIFVRYSOE".toCharArray()); // 登录账号密码 String messageId = MailUtil.send(mailAccount, "7685413@qq.com", "主题", "内容", false); System.out.println("发送结果:" + messageId); } @Test public void testSendSingleMailToAdmin() { // 准备参数 Long userId = randomLongId(); String templateCode = RandomUtils.randomString(); Map templateParams = MapUtil.builder().put("code", "1234") .put("op", "login").build(); // mock adminUserService 的方法 AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setMobile("15601691300")); when(adminUserService.getUser(eq(userId))).thenReturn(user); // mock MailTemplateService 的方法 MailTemplateDO template = randomPojo(MailTemplateDO.class, o -> { o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setContent("验证码为{code}, 操作为{op}"); o.setParams(Lists.newArrayList("code", "op")); }); when(mailTemplateService.getMailTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); String title = RandomUtils.randomString(); when(mailTemplateService.formatMailTemplateContent(eq(template.getTitle()), eq(templateParams))) .thenReturn(title); String content = RandomUtils.randomString(); when(mailTemplateService.formatMailTemplateContent(eq(template.getContent()), eq(templateParams))) .thenReturn(content); // mock MailAccountService 的方法 MailAccountDO account = randomPojo(MailAccountDO.class); when(mailAccountService.getMailAccountFromCache(eq(template.getAccountId()))).thenReturn(account); // mock MailLogService 的方法 Long mailLogId = randomLongId(); when(mailLogService.createMailLog(eq(userId), eq(UserTypeEnum.ADMIN.getValue()), eq(user.getEmail()), eq(account), eq(template), eq(content), eq(templateParams), eq(true))).thenReturn(mailLogId); // 调用 Long resultMailLogId = mailSendService.sendSingleMailToAdmin(null, userId, templateCode, templateParams); // 断言 assertEquals(mailLogId, resultMailLogId); // 断言调用 verify(mailProducer).sendMailSendMessage(eq(mailLogId), eq(user.getEmail()), eq(account.getId()), eq(template.getNickname()), eq(title), eq(content)); } @Test public void testSendSingleMailToMember() { // 准备参数 Long userId = randomLongId(); String templateCode = RandomUtils.randomString(); Map templateParams = MapUtil.builder().put("code", "1234") .put("op", "login").build(); // mock memberService 的方法 String mail = randomEmail(); when(memberService.getMemberUserEmail(eq(userId))).thenReturn(mail); // mock MailTemplateService 的方法 MailTemplateDO template = randomPojo(MailTemplateDO.class, o -> { o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setContent("验证码为{code}, 操作为{op}"); o.setParams(Lists.newArrayList("code", "op")); }); when(mailTemplateService.getMailTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); String title = RandomUtils.randomString(); when(mailTemplateService.formatMailTemplateContent(eq(template.getTitle()), eq(templateParams))) .thenReturn(title); String content = RandomUtils.randomString(); when(mailTemplateService.formatMailTemplateContent(eq(template.getContent()), eq(templateParams))) .thenReturn(content); // mock MailAccountService 的方法 MailAccountDO account = randomPojo(MailAccountDO.class); when(mailAccountService.getMailAccountFromCache(eq(template.getAccountId()))).thenReturn(account); // mock MailLogService 的方法 Long mailLogId = randomLongId(); when(mailLogService.createMailLog(eq(userId), eq(UserTypeEnum.MEMBER.getValue()), eq(mail), eq(account), eq(template), eq(content), eq(templateParams), eq(true))).thenReturn(mailLogId); // 调用 Long resultMailLogId = mailSendService.sendSingleMailToMember(null, userId, templateCode, templateParams); // 断言 assertEquals(mailLogId, resultMailLogId); // 断言调用 verify(mailProducer).sendMailSendMessage(eq(mailLogId), eq(mail), eq(account.getId()), eq(template.getNickname()), eq(title), eq(content)); } /** * 发送成功,当短信模板开启时 */ @Test public void testSendSingleMail_successWhenMailTemplateEnable() { // 准备参数 String mail = randomEmail(); Long userId = randomLongId(); Integer userType = randomEle(UserTypeEnum.values()).getValue(); String templateCode = RandomUtils.randomString(); Map templateParams = MapUtil.builder().put("code", "1234") .put("op", "login").build(); // mock MailTemplateService 的方法 MailTemplateDO template = randomPojo(MailTemplateDO.class, o -> { o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setContent("验证码为{code}, 操作为{op}"); o.setParams(Lists.newArrayList("code", "op")); }); when(mailTemplateService.getMailTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); String title = RandomUtils.randomString(); when(mailTemplateService.formatMailTemplateContent(eq(template.getTitle()), eq(templateParams))) .thenReturn(title); String content = RandomUtils.randomString(); when(mailTemplateService.formatMailTemplateContent(eq(template.getContent()), eq(templateParams))) .thenReturn(content); // mock MailAccountService 的方法 MailAccountDO account = randomPojo(MailAccountDO.class); when(mailAccountService.getMailAccountFromCache(eq(template.getAccountId()))).thenReturn(account); // mock MailLogService 的方法 Long mailLogId = randomLongId(); when(mailLogService.createMailLog(eq(userId), eq(userType), eq(mail), eq(account), eq(template), eq(content), eq(templateParams), eq(true))).thenReturn(mailLogId); // 调用 Long resultMailLogId = mailSendService.sendSingleMail(mail, userId, userType, templateCode, templateParams); // 断言 assertEquals(mailLogId, resultMailLogId); // 断言调用 verify(mailProducer).sendMailSendMessage(eq(mailLogId), eq(mail), eq(account.getId()), eq(template.getNickname()), eq(title), eq(content)); } /** * 发送成功,当短信模板关闭时 */ @Test public void testSendSingleMail_successWhenSmsTemplateDisable() { // 准备参数 String mail = randomEmail(); Long userId = randomLongId(); Integer userType = randomEle(UserTypeEnum.values()).getValue(); String templateCode = RandomUtils.randomString(); Map templateParams = MapUtil.builder().put("code", "1234") .put("op", "login").build(); // mock MailTemplateService 的方法 MailTemplateDO template = randomPojo(MailTemplateDO.class, o -> { o.setStatus(CommonStatusEnum.DISABLE.getStatus()); o.setContent("验证码为{code}, 操作为{op}"); o.setParams(Lists.newArrayList("code", "op")); }); when(mailTemplateService.getMailTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); String title = RandomUtils.randomString(); when(mailTemplateService.formatMailTemplateContent(eq(template.getTitle()), eq(templateParams))) .thenReturn(title); String content = RandomUtils.randomString(); when(mailTemplateService.formatMailTemplateContent(eq(template.getContent()), eq(templateParams))) .thenReturn(content); // mock MailAccountService 的方法 MailAccountDO account = randomPojo(MailAccountDO.class); when(mailAccountService.getMailAccountFromCache(eq(template.getAccountId()))).thenReturn(account); // mock MailLogService 的方法 Long mailLogId = randomLongId(); when(mailLogService.createMailLog(eq(userId), eq(userType), eq(mail), eq(account), eq(template), eq(content), eq(templateParams), eq(false))).thenReturn(mailLogId); // 调用 Long resultMailLogId = mailSendService.sendSingleMail(mail, userId, userType, templateCode, templateParams); // 断言 assertEquals(mailLogId, resultMailLogId); // 断言调用 verify(mailProducer, times(0)).sendMailSendMessage(anyLong(), anyString(), anyLong(), anyString(), anyString(), anyString()); } @Test public void testValidateMailTemplateValid_notExists() { // 准备参数 String templateCode = RandomUtils.randomString(); // mock 方法 // 调用,并断言异常 assertServiceException(() -> mailSendService.validateMailTemplate(templateCode), MAIL_TEMPLATE_NOT_EXISTS); } @Test public void testValidateTemplateParams_paramMiss() { // 准备参数 MailTemplateDO template = randomPojo(MailTemplateDO.class, o -> o.setParams(Lists.newArrayList("code"))); Map templateParams = new HashMap<>(); // mock 方法 // 调用,并断言异常 assertServiceException(() -> mailSendService.validateTemplateParams(template, templateParams), MAIL_SEND_TEMPLATE_PARAM_MISS, "code"); } @Test public void testValidateMail_notExists() { // 准备参数 // mock 方法 // 调用,并断言异常 assertServiceException(() -> mailSendService.validateMail(null), MAIL_SEND_MAIL_NOT_EXISTS); } @Test public void testDoSendMail_success() { try (final MockedStatic mailUtilMock = mockStatic(MailUtil.class)) { // 准备参数 MailSendMessage message = randomPojo(MailSendMessage.class, o -> o.setNickname("yshop")); // mock 方法(获得邮箱账号) MailAccountDO account = randomPojo(MailAccountDO.class, o -> o.setMail("7685@qq.com")); when(mailAccountService.getMailAccountFromCache(eq(message.getAccountId()))) .thenReturn(account); // mock 方法(发送邮件) String messageId = randomString(); mailUtilMock.when(() -> MailUtil.send( argThat(mailAccount -> { assertEquals("yshop <7685@qq.com>", mailAccount.getFrom()); assertTrue(mailAccount.isAuth()); assertEquals(account.getUsername(), mailAccount.getUser()); assertArrayEquals(account.getPassword().toCharArray(), mailAccount.getPass()); assertEquals(account.getHost(), mailAccount.getHost()); assertEquals(account.getPort(), mailAccount.getPort()); assertEquals(account.getSslEnable(), mailAccount.isSslEnable()); return true; }), eq(message.getMail()), eq(message.getTitle()), eq(message.getContent()), eq(true))) .thenReturn(messageId); // 调用 mailSendService.doSendMail(message); // 断言 verify(mailLogService).updateMailSendResult(eq(message.getLogId()), eq(messageId), isNull()); } } @Test public void testDoSendMail_exception() { try (MockedStatic mailUtilMock = mockStatic(MailUtil.class)) { // 准备参数 MailSendMessage message = randomPojo(MailSendMessage.class, o -> o.setNickname("yshop")); // mock 方法(获得邮箱账号) MailAccountDO account = randomPojo(MailAccountDO.class, o -> o.setMail("7685@qq.com")); when(mailAccountService.getMailAccountFromCache(eq(message.getAccountId()))) .thenReturn(account); // mock 方法(发送邮件) Exception e = new NullPointerException("啦啦啦"); mailUtilMock.when(() -> MailUtil.send(argThat(mailAccount -> { assertEquals("yshop <7685@qq.com>", mailAccount.getFrom()); assertTrue(mailAccount.isAuth()); assertEquals(account.getUsername(), mailAccount.getUser()); assertArrayEquals(account.getPassword().toCharArray(), mailAccount.getPass()); assertEquals(account.getHost(), mailAccount.getHost()); assertEquals(account.getPort(), mailAccount.getPort()); assertEquals(account.getSslEnable(), mailAccount.isSslEnable()); return true; }), eq(message.getMail()), eq(message.getTitle()), eq(message.getContent()), eq(true))).thenThrow(e); // 调用 mailSendService.doSendMail(message); // 断言 verify(mailLogService).updateMailSendResult(eq(message.getLogId()), isNull(), same(e)); } } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/mail/MailTemplateServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.mail; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.controller.admin.mail.vo.template.MailTemplateSaveReqVO; import co.yixiang.yshop.module.system.controller.admin.mail.vo.template.MailTemplatePageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.mail.MailTemplateDO; import co.yixiang.yshop.module.system.dal.mysql.mail.MailTemplateMapper; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.util.HashMap; import java.util.List; import java.util.Map; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildTime; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomLongId; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomPojo; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.MAIL_TEMPLATE_NOT_EXISTS; import static org.junit.jupiter.api.Assertions.*; /** * {@link MailTemplateServiceImpl} 的单元测试类 * * @author yshop */ @Import(MailTemplateServiceImpl.class) public class MailTemplateServiceImplTest extends BaseDbUnitTest { @Resource private MailTemplateServiceImpl mailTemplateService; @Resource private MailTemplateMapper mailTemplateMapper; @Test public void testCreateMailTemplate_success() { // 准备参数 MailTemplateSaveReqVO reqVO = randomPojo(MailTemplateSaveReqVO.class) .setId(null); // 防止 id 被赋值 // 调用 Long mailTemplateId = mailTemplateService.createMailTemplate(reqVO); // 断言 assertNotNull(mailTemplateId); // 校验记录的属性是否正确 MailTemplateDO mailTemplate = mailTemplateMapper.selectById(mailTemplateId); assertPojoEquals(reqVO, mailTemplate, "id"); } @Test public void testUpdateMailTemplate_success() { // mock 数据 MailTemplateDO dbMailTemplate = randomPojo(MailTemplateDO.class); mailTemplateMapper.insert(dbMailTemplate);// @Sql: 先插入出一条存在的数据 // 准备参数 MailTemplateSaveReqVO reqVO = randomPojo(MailTemplateSaveReqVO.class, o -> { o.setId(dbMailTemplate.getId()); // 设置更新的 ID }); // 调用 mailTemplateService.updateMailTemplate(reqVO); // 校验是否更新正确 MailTemplateDO mailTemplate = mailTemplateMapper.selectById(reqVO.getId()); // 获取最新的 assertPojoEquals(reqVO, mailTemplate); } @Test public void testUpdateMailTemplate_notExists() { // 准备参数 MailTemplateSaveReqVO reqVO = randomPojo(MailTemplateSaveReqVO.class); // 调用, 并断言异常 assertServiceException(() -> mailTemplateService.updateMailTemplate(reqVO), MAIL_TEMPLATE_NOT_EXISTS); } @Test public void testDeleteMailTemplate_success() { // mock 数据 MailTemplateDO dbMailTemplate = randomPojo(MailTemplateDO.class); mailTemplateMapper.insert(dbMailTemplate);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbMailTemplate.getId(); // 调用 mailTemplateService.deleteMailTemplate(id); // 校验数据不存在了 assertNull(mailTemplateMapper.selectById(id)); } @Test public void testDeleteMailTemplate_notExists() { // 准备参数 Long id = randomLongId(); // 调用, 并断言异常 assertServiceException(() -> mailTemplateService.deleteMailTemplate(id), MAIL_TEMPLATE_NOT_EXISTS); } @Test public void testGetMailTemplatePage() { // mock 数据 MailTemplateDO dbMailTemplate = randomPojo(MailTemplateDO.class, o -> { // 等会查询到 o.setName("源码"); o.setCode("test_01"); o.setAccountId(1L); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setCreateTime(buildTime(2023, 2, 3)); }); mailTemplateMapper.insert(dbMailTemplate); // 测试 name 不匹配 mailTemplateMapper.insert(cloneIgnoreId(dbMailTemplate, o -> o.setName("yshop"))); // 测试 code 不匹配 mailTemplateMapper.insert(cloneIgnoreId(dbMailTemplate, o -> o.setCode("test_02"))); // 测试 accountId 不匹配 mailTemplateMapper.insert(cloneIgnoreId(dbMailTemplate, o -> o.setAccountId(2L))); // 测试 status 不匹配 mailTemplateMapper.insert(cloneIgnoreId(dbMailTemplate, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); // 测试 createTime 不匹配 mailTemplateMapper.insert(cloneIgnoreId(dbMailTemplate, o -> o.setCreateTime(buildTime(2023, 1, 5)))); // 准备参数 MailTemplatePageReqVO reqVO = new MailTemplatePageReqVO(); reqVO.setName("源"); reqVO.setCode("est_01"); reqVO.setAccountId(1L); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 5)); // 调用 PageResult pageResult = mailTemplateService.getMailTemplatePage(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbMailTemplate, pageResult.getList().get(0)); } @Test public void testGetMailTemplateList() { // mock 数据 MailTemplateDO dbMailTemplate01 = randomPojo(MailTemplateDO.class); mailTemplateMapper.insert(dbMailTemplate01); MailTemplateDO dbMailTemplate02 = randomPojo(MailTemplateDO.class); mailTemplateMapper.insert(dbMailTemplate02); // 调用 List list = mailTemplateService.getMailTemplateList(); // 断言 assertEquals(2, list.size()); assertEquals(dbMailTemplate01, list.get(0)); assertEquals(dbMailTemplate02, list.get(1)); } @Test public void testGetMailTemplate() { // mock 数据 MailTemplateDO dbMailTemplate = randomPojo(MailTemplateDO.class); mailTemplateMapper.insert(dbMailTemplate); // 准备参数 Long id = dbMailTemplate.getId(); // 调用 MailTemplateDO mailTemplate = mailTemplateService.getMailTemplate(id); // 断言 assertPojoEquals(dbMailTemplate, mailTemplate); } @Test public void testGetMailTemplateByCodeFromCache() { // mock 数据 MailTemplateDO dbMailTemplate = randomPojo(MailTemplateDO.class); mailTemplateMapper.insert(dbMailTemplate); // 准备参数 String code = dbMailTemplate.getCode(); // 调用 MailTemplateDO mailTemplate = mailTemplateService.getMailTemplateByCodeFromCache(code); // 断言 assertPojoEquals(dbMailTemplate, mailTemplate); } @Test public void testFormatMailTemplateContent() { // 准备参数 Map params = new HashMap<>(); params.put("name", "小红"); params.put("what", "饭"); // 调用,并断言 assertEquals("小红,你好,饭吃了吗?", mailTemplateService.formatMailTemplateContent("{name},你好,{what}吃了吗?", params)); } @Test public void testCountByAccountId() { // mock 数据 MailTemplateDO dbMailTemplate = randomPojo(MailTemplateDO.class); mailTemplateMapper.insert(dbMailTemplate); // 测试 accountId 不匹配 mailTemplateMapper.insert(cloneIgnoreId(dbMailTemplate, o -> o.setAccountId(2L))); // 准备参数 Long accountId = dbMailTemplate.getAccountId(); // 调用 long count = mailTemplateService.getMailTemplateCountByAccountId(accountId); // 断言 assertEquals(1, count); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/notice/NoticeServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.notice; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.controller.admin.notice.vo.NoticePageReqVO; import co.yixiang.yshop.module.system.controller.admin.notice.vo.NoticeSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.notice.NoticeDO; import co.yixiang.yshop.module.system.dal.mysql.notice.NoticeMapper; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomLongId; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomPojo; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.NOTICE_NOT_FOUND; import static org.junit.jupiter.api.Assertions.*; @Import(NoticeServiceImpl.class) class NoticeServiceImplTest extends BaseDbUnitTest { @Resource private NoticeServiceImpl noticeService; @Resource private NoticeMapper noticeMapper; @Test public void testGetNoticePage_success() { // 插入前置数据 NoticeDO dbNotice = randomPojo(NoticeDO.class, o -> { o.setTitle("尼古拉斯赵四来啦!"); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); }); noticeMapper.insert(dbNotice); // 测试 title 不匹配 noticeMapper.insert(cloneIgnoreId(dbNotice, o -> o.setTitle("尼古拉斯凯奇也来啦!"))); // 测试 status 不匹配 noticeMapper.insert(cloneIgnoreId(dbNotice, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); // 准备参数 NoticePageReqVO reqVO = new NoticePageReqVO(); reqVO.setTitle("尼古拉斯赵四来啦!"); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 调用 PageResult pageResult = noticeService.getNoticePage(reqVO); // 验证查询结果经过筛选 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbNotice, pageResult.getList().get(0)); } @Test public void testGetNotice_success() { // 插入前置数据 NoticeDO dbNotice = randomPojo(NoticeDO.class); noticeMapper.insert(dbNotice); // 查询 NoticeDO notice = noticeService.getNotice(dbNotice.getId()); // 验证插入与读取对象是否一致 assertNotNull(notice); assertPojoEquals(dbNotice, notice); } @Test public void testCreateNotice_success() { // 准备参数 NoticeSaveReqVO reqVO = randomPojo(NoticeSaveReqVO.class) .setId(null); // 避免 id 被赋值 // 调用 Long noticeId = noticeService.createNotice(reqVO); // 校验插入属性是否正确 assertNotNull(noticeId); NoticeDO notice = noticeMapper.selectById(noticeId); assertPojoEquals(reqVO, notice, "id"); } @Test public void testUpdateNotice_success() { // 插入前置数据 NoticeDO dbNoticeDO = randomPojo(NoticeDO.class); noticeMapper.insert(dbNoticeDO); // 准备更新参数 NoticeSaveReqVO reqVO = randomPojo(NoticeSaveReqVO.class, o -> o.setId(dbNoticeDO.getId())); // 更新 noticeService.updateNotice(reqVO); // 检验是否更新成功 NoticeDO notice = noticeMapper.selectById(reqVO.getId()); assertPojoEquals(reqVO, notice); } @Test public void testDeleteNotice_success() { // 插入前置数据 NoticeDO dbNotice = randomPojo(NoticeDO.class); noticeMapper.insert(dbNotice); // 删除 noticeService.deleteNotice(dbNotice.getId()); // 检查是否删除成功 assertNull(noticeMapper.selectById(dbNotice.getId())); } @Test public void testValidateNoticeExists_success() { // 插入前置数据 NoticeDO dbNotice = randomPojo(NoticeDO.class); noticeMapper.insert(dbNotice); // 成功调用 noticeService.validateNoticeExists(dbNotice.getId()); } @Test public void testValidateNoticeExists_noExists() { assertServiceException(() -> noticeService.validateNoticeExists(randomLongId()), NOTICE_NOT_FOUND); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/notify/NotifyMessageServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.notify; import cn.hutool.core.map.MapUtil; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.mybatis.core.enums.SqlConstants; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.controller.admin.notify.vo.message.NotifyMessageMyPageReqVO; import co.yixiang.yshop.module.system.controller.admin.notify.vo.message.NotifyMessagePageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.notify.NotifyMessageDO; import co.yixiang.yshop.module.system.dal.dataobject.notify.NotifyTemplateDO; import co.yixiang.yshop.module.system.dal.mysql.notify.NotifyMessageMapper; import com.baomidou.mybatisplus.annotation.DbType; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import static cn.hutool.core.util.RandomUtil.randomEle; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildTime; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static org.junit.jupiter.api.Assertions.*; /** * {@link NotifyMessageServiceImpl} 的单元测试类 * * @author yshop */ @Import(NotifyMessageServiceImpl.class) public class NotifyMessageServiceImplTest extends BaseDbUnitTest { @Resource private NotifyMessageServiceImpl notifyMessageService; @Resource private NotifyMessageMapper notifyMessageMapper; @Test public void testCreateNotifyMessage_success() { // 准备参数 Long userId = randomLongId(); Integer userType = randomEle(UserTypeEnum.values()).getValue(); NotifyTemplateDO template = randomPojo(NotifyTemplateDO.class); String templateContent = randomString(); Map templateParams = randomTemplateParams(); // mock 方法 // 调用 Long messageId = notifyMessageService.createNotifyMessage(userId, userType, template, templateContent, templateParams); // 断言 NotifyMessageDO message = notifyMessageMapper.selectById(messageId); assertNotNull(message); assertEquals(userId, message.getUserId()); assertEquals(userType, message.getUserType()); assertEquals(template.getId(), message.getTemplateId()); assertEquals(template.getCode(), message.getTemplateCode()); assertEquals(template.getType(), message.getTemplateType()); assertEquals(template.getNickname(), message.getTemplateNickname()); assertEquals(templateContent, message.getTemplateContent()); assertEquals(templateParams, message.getTemplateParams()); assertEquals(false, message.getReadStatus()); assertNull(message.getReadTime()); } @Test public void testGetNotifyMessagePage() { // mock 数据 NotifyMessageDO dbNotifyMessage = randomPojo(NotifyMessageDO.class, o -> { // 等会查询到 o.setUserId(1L); o.setUserType(UserTypeEnum.ADMIN.getValue()); o.setTemplateCode("test_01"); o.setTemplateType(10); o.setCreateTime(buildTime(2022, 1, 2)); o.setTemplateParams(randomTemplateParams()); }); notifyMessageMapper.insert(dbNotifyMessage); // 测试 userId 不匹配 notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserId(2L))); // 测试 userType 不匹配 notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); // 测试 templateCode 不匹配 notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setTemplateCode("test_11"))); // 测试 templateType 不匹配 notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setTemplateType(20))); // 测试 createTime 不匹配 notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setCreateTime(buildTime(2022, 2, 1)))); // 准备参数 NotifyMessagePageReqVO reqVO = new NotifyMessagePageReqVO(); reqVO.setUserId(1L); reqVO.setUserType(UserTypeEnum.ADMIN.getValue()); reqVO.setTemplateCode("est_01"); reqVO.setTemplateType(10); reqVO.setCreateTime(buildBetweenTime(2022, 1, 1, 2022, 1, 10)); // 调用 PageResult pageResult = notifyMessageService.getNotifyMessagePage(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbNotifyMessage, pageResult.getList().get(0)); } @Test public void testGetNotifyMessage() { // mock 数据 NotifyMessageDO dbNotifyMessage = randomPojo(NotifyMessageDO.class, o -> o.setTemplateParams(randomTemplateParams())); notifyMessageMapper.insert(dbNotifyMessage); // 准备参数 Long id = dbNotifyMessage.getId(); // 调用 NotifyMessageDO notifyMessage = notifyMessageService.getNotifyMessage(id); assertPojoEquals(dbNotifyMessage, notifyMessage); } @Test public void testGetMyNotifyMessagePage() { // mock 数据 NotifyMessageDO dbNotifyMessage = randomPojo(NotifyMessageDO.class, o -> { // 等会查询到 o.setUserId(1L); o.setUserType(UserTypeEnum.ADMIN.getValue()); o.setReadStatus(true); o.setCreateTime(buildTime(2022, 1, 2)); o.setTemplateParams(randomTemplateParams()); }); notifyMessageMapper.insert(dbNotifyMessage); // 测试 userId 不匹配 notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserId(2L))); // 测试 userType 不匹配 notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); // 测试 readStatus 不匹配 notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setReadStatus(false))); // 测试 createTime 不匹配 notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setCreateTime(buildTime(2022, 2, 1)))); // 准备参数 Long userId = 1L; Integer userType = UserTypeEnum.ADMIN.getValue(); NotifyMessageMyPageReqVO reqVO = new NotifyMessageMyPageReqVO(); reqVO.setReadStatus(true); reqVO.setCreateTime(buildBetweenTime(2022, 1, 1, 2022, 1, 10)); // 调用 PageResult pageResult = notifyMessageService.getMyMyNotifyMessagePage(reqVO, userId, userType); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbNotifyMessage, pageResult.getList().get(0)); } @Test public void testGetUnreadNotifyMessageList() { SqlConstants.init(DbType.MYSQL); // mock 数据 NotifyMessageDO dbNotifyMessage = randomPojo(NotifyMessageDO.class, o -> { // 等会查询到 o.setUserId(1L); o.setUserType(UserTypeEnum.ADMIN.getValue()); o.setReadStatus(false); o.setTemplateParams(randomTemplateParams()); }); notifyMessageMapper.insert(dbNotifyMessage); // 测试 userId 不匹配 notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserId(2L))); // 测试 userType 不匹配 notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); // 测试 readStatus 不匹配 notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setReadStatus(true))); // 准备参数 Long userId = 1L; Integer userType = UserTypeEnum.ADMIN.getValue(); Integer size = 10; // 调用 List list = notifyMessageService.getUnreadNotifyMessageList(userId, userType, size); // 断言 assertEquals(1, list.size()); assertPojoEquals(dbNotifyMessage, list.get(0)); } @Test public void testGetUnreadNotifyMessageCount() { SqlConstants.init(DbType.MYSQL); // mock 数据 NotifyMessageDO dbNotifyMessage = randomPojo(NotifyMessageDO.class, o -> { // 等会查询到 o.setUserId(1L); o.setUserType(UserTypeEnum.ADMIN.getValue()); o.setReadStatus(false); o.setTemplateParams(randomTemplateParams()); }); notifyMessageMapper.insert(dbNotifyMessage); // 测试 userId 不匹配 notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserId(2L))); // 测试 userType 不匹配 notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); // 测试 readStatus 不匹配 notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setReadStatus(true))); // 准备参数 Long userId = 1L; Integer userType = UserTypeEnum.ADMIN.getValue(); // 调用,并断言 assertEquals(1, notifyMessageService.getUnreadNotifyMessageCount(userId, userType)); } @Test public void testUpdateNotifyMessageRead() { // mock 数据 NotifyMessageDO dbNotifyMessage = randomPojo(NotifyMessageDO.class, o -> { // 等会查询到 o.setUserId(1L); o.setUserType(UserTypeEnum.ADMIN.getValue()); o.setReadStatus(false); o.setReadTime(null); o.setTemplateParams(randomTemplateParams()); }); notifyMessageMapper.insert(dbNotifyMessage); // 测试 userId 不匹配 notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserId(2L))); // 测试 userType 不匹配 notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); // 测试 readStatus 不匹配 notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setReadStatus(true))); // 准备参数 Collection ids = Arrays.asList(dbNotifyMessage.getId(), dbNotifyMessage.getId() + 1, dbNotifyMessage.getId() + 2, dbNotifyMessage.getId() + 3); Long userId = 1L; Integer userType = UserTypeEnum.ADMIN.getValue(); // 调用 int updateCount = notifyMessageService.updateNotifyMessageRead(ids, userId, userType); // 断言 assertEquals(1, updateCount); NotifyMessageDO notifyMessage = notifyMessageMapper.selectById(dbNotifyMessage.getId()); assertTrue(notifyMessage.getReadStatus()); assertNotNull(notifyMessage.getReadTime()); } @Test public void testUpdateAllNotifyMessageRead() { // mock 数据 NotifyMessageDO dbNotifyMessage = randomPojo(NotifyMessageDO.class, o -> { // 等会查询到 o.setUserId(1L); o.setUserType(UserTypeEnum.ADMIN.getValue()); o.setReadStatus(false); o.setReadTime(null); o.setTemplateParams(randomTemplateParams()); }); notifyMessageMapper.insert(dbNotifyMessage); // 测试 userId 不匹配 notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserId(2L))); // 测试 userType 不匹配 notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); // 测试 readStatus 不匹配 notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setReadStatus(true))); // 准备参数 Long userId = 1L; Integer userType = UserTypeEnum.ADMIN.getValue(); // 调用 int updateCount = notifyMessageService.updateAllNotifyMessageRead(userId, userType); // 断言 assertEquals(1, updateCount); NotifyMessageDO notifyMessage = notifyMessageMapper.selectById(dbNotifyMessage.getId()); assertTrue(notifyMessage.getReadStatus()); assertNotNull(notifyMessage.getReadTime()); } private static Map randomTemplateParams() { return MapUtil.builder().put(randomString(), randomString()) .put(randomString(), randomString()).build(); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/notify/NotifySendServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.notify; import cn.hutool.core.map.MapUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.test.core.ut.BaseMockitoUnitTest; import co.yixiang.yshop.module.system.dal.dataobject.notify.NotifyTemplateDO; import org.assertj.core.util.Lists; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import java.util.HashMap; import java.util.Map; import static cn.hutool.core.util.RandomUtil.randomEle; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.NOTICE_NOT_FOUND; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.NOTIFY_SEND_TEMPLATE_PARAM_MISS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; class NotifySendServiceImplTest extends BaseMockitoUnitTest { @InjectMocks private NotifySendServiceImpl notifySendService; @Mock private NotifyTemplateService notifyTemplateService; @Mock private NotifyMessageService notifyMessageService; @Test public void testSendSingleNotifyToAdmin() { // 准备参数 Long userId = randomLongId(); String templateCode = randomString(); Map templateParams = MapUtil.builder().put("code", "1234") .put("op", "login").build(); // mock NotifyTemplateService 的方法 NotifyTemplateDO template = randomPojo(NotifyTemplateDO.class, o -> { o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setContent("验证码为{code}, 操作为{op}"); o.setParams(Lists.newArrayList("code", "op")); }); when(notifyTemplateService.getNotifyTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); String content = randomString(); when(notifyTemplateService.formatNotifyTemplateContent(eq(template.getContent()), eq(templateParams))) .thenReturn(content); // mock NotifyMessageService 的方法 Long messageId = randomLongId(); when(notifyMessageService.createNotifyMessage(eq(userId), eq(UserTypeEnum.ADMIN.getValue()), eq(template), eq(content), eq(templateParams))).thenReturn(messageId); // 调用 Long resultMessageId = notifySendService.sendSingleNotifyToAdmin(userId, templateCode, templateParams); // 断言 assertEquals(messageId, resultMessageId); } @Test public void testSendSingleNotifyToMember() { // 准备参数 Long userId = randomLongId(); String templateCode = randomString(); Map templateParams = MapUtil.builder().put("code", "1234") .put("op", "login").build(); // mock NotifyTemplateService 的方法 NotifyTemplateDO template = randomPojo(NotifyTemplateDO.class, o -> { o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setContent("验证码为{code}, 操作为{op}"); o.setParams(Lists.newArrayList("code", "op")); }); when(notifyTemplateService.getNotifyTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); String content = randomString(); when(notifyTemplateService.formatNotifyTemplateContent(eq(template.getContent()), eq(templateParams))) .thenReturn(content); // mock NotifyMessageService 的方法 Long messageId = randomLongId(); when(notifyMessageService.createNotifyMessage(eq(userId), eq(UserTypeEnum.MEMBER.getValue()), eq(template), eq(content), eq(templateParams))).thenReturn(messageId); // 调用 Long resultMessageId = notifySendService.sendSingleNotifyToMember(userId, templateCode, templateParams); // 断言 assertEquals(messageId, resultMessageId); } /** * 发送成功,当短信模板开启时 */ @Test public void testSendSingleNotify_successWhenMailTemplateEnable() { // 准备参数 Long userId = randomLongId(); Integer userType = randomEle(UserTypeEnum.values()).getValue(); String templateCode = randomString(); Map templateParams = MapUtil.builder().put("code", "1234") .put("op", "login").build(); // mock NotifyTemplateService 的方法 NotifyTemplateDO template = randomPojo(NotifyTemplateDO.class, o -> { o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setContent("验证码为{code}, 操作为{op}"); o.setParams(Lists.newArrayList("code", "op")); }); when(notifyTemplateService.getNotifyTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); String content = randomString(); when(notifyTemplateService.formatNotifyTemplateContent(eq(template.getContent()), eq(templateParams))) .thenReturn(content); // mock NotifyMessageService 的方法 Long messageId = randomLongId(); when(notifyMessageService.createNotifyMessage(eq(userId), eq(userType), eq(template), eq(content), eq(templateParams))).thenReturn(messageId); // 调用 Long resultMessageId = notifySendService.sendSingleNotify(userId, userType, templateCode, templateParams); // 断言 assertEquals(messageId, resultMessageId); } /** * 发送成功,当短信模板关闭时 */ @Test public void testSendSingleMail_successWhenSmsTemplateDisable() { // 准备参数 Long userId = randomLongId(); Integer userType = randomEle(UserTypeEnum.values()).getValue(); String templateCode = randomString(); Map templateParams = MapUtil.builder().put("code", "1234") .put("op", "login").build(); // mock NotifyTemplateService 的方法 NotifyTemplateDO template = randomPojo(NotifyTemplateDO.class, o -> { o.setStatus(CommonStatusEnum.DISABLE.getStatus()); o.setContent("验证码为{code}, 操作为{op}"); o.setParams(Lists.newArrayList("code", "op")); }); when(notifyTemplateService.getNotifyTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); // 调用 Long resultMessageId = notifySendService.sendSingleNotify(userId, userType, templateCode, templateParams); // 断言 assertNull(resultMessageId); verify(notifyTemplateService, never()).formatNotifyTemplateContent(anyString(), anyMap()); verify(notifyMessageService, never()).createNotifyMessage(anyLong(), anyInt(), any(), anyString(), anyMap()); } @Test public void testCheckMailTemplateValid_notExists() { // 准备参数 String templateCode = randomString(); // mock 方法 // 调用,并断言异常 assertServiceException(() -> notifySendService.validateNotifyTemplate(templateCode), NOTICE_NOT_FOUND); } @Test public void testCheckTemplateParams_paramMiss() { // 准备参数 NotifyTemplateDO template = randomPojo(NotifyTemplateDO.class, o -> o.setParams(Lists.newArrayList("code"))); Map templateParams = new HashMap<>(); // mock 方法 // 调用,并断言异常 assertServiceException(() -> notifySendService.validateTemplateParams(template, templateParams), NOTIFY_SEND_TEMPLATE_PARAM_MISS, "code"); } @Test public void testSendBatchNotify() { // 准备参数 // mock 方法 // 调用 UnsupportedOperationException exception = Assertions.assertThrows( UnsupportedOperationException.class, () -> notifySendService.sendBatchNotify(null, null, null, null, null) ); // 断言 assertEquals("暂时不支持该操作,感兴趣可以实现该功能哟!", exception.getMessage()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/notify/NotifyTemplateServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.notify; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.controller.admin.notify.vo.template.NotifyTemplatePageReqVO; import co.yixiang.yshop.module.system.controller.admin.notify.vo.template.NotifyTemplateSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.notify.NotifyTemplateDO; import co.yixiang.yshop.module.system.dal.mysql.notify.NotifyTemplateMapper; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.util.HashMap; import java.util.Map; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildTime; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.NOTIFY_TEMPLATE_NOT_EXISTS; import static org.junit.jupiter.api.Assertions.*; /** * {@link NotifyTemplateServiceImpl} 的单元测试类 * * @author yshop */ @Import(NotifyTemplateServiceImpl.class) public class NotifyTemplateServiceImplTest extends BaseDbUnitTest { @Resource private NotifyTemplateServiceImpl notifyTemplateService; @Resource private NotifyTemplateMapper notifyTemplateMapper; @Test public void testCreateNotifyTemplate_success() { // 准备参数 NotifyTemplateSaveReqVO reqVO = randomPojo(NotifyTemplateSaveReqVO.class, o -> o.setStatus(randomCommonStatus())) .setId(null); // 防止 id 被赋值 // 调用 Long notifyTemplateId = notifyTemplateService.createNotifyTemplate(reqVO); // 断言 assertNotNull(notifyTemplateId); // 校验记录的属性是否正确 NotifyTemplateDO notifyTemplate = notifyTemplateMapper.selectById(notifyTemplateId); assertPojoEquals(reqVO, notifyTemplate, "id"); } @Test public void testUpdateNotifyTemplate_success() { // mock 数据 NotifyTemplateDO dbNotifyTemplate = randomPojo(NotifyTemplateDO.class); notifyTemplateMapper.insert(dbNotifyTemplate);// @Sql: 先插入出一条存在的数据 // 准备参数 NotifyTemplateSaveReqVO reqVO = randomPojo(NotifyTemplateSaveReqVO.class, o -> { o.setId(dbNotifyTemplate.getId()); // 设置更新的 ID o.setStatus(randomCommonStatus()); }); // 调用 notifyTemplateService.updateNotifyTemplate(reqVO); // 校验是否更新正确 NotifyTemplateDO notifyTemplate = notifyTemplateMapper.selectById(reqVO.getId()); // 获取最新的 assertPojoEquals(reqVO, notifyTemplate); } @Test public void testUpdateNotifyTemplate_notExists() { // 准备参数 NotifyTemplateSaveReqVO reqVO = randomPojo(NotifyTemplateSaveReqVO.class); // 调用, 并断言异常 assertServiceException(() -> notifyTemplateService.updateNotifyTemplate(reqVO), NOTIFY_TEMPLATE_NOT_EXISTS); } @Test public void testDeleteNotifyTemplate_success() { // mock 数据 NotifyTemplateDO dbNotifyTemplate = randomPojo(NotifyTemplateDO.class); notifyTemplateMapper.insert(dbNotifyTemplate);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbNotifyTemplate.getId(); // 调用 notifyTemplateService.deleteNotifyTemplate(id); // 校验数据不存在了 assertNull(notifyTemplateMapper.selectById(id)); } @Test public void testDeleteNotifyTemplate_notExists() { // 准备参数 Long id = randomLongId(); // 调用, 并断言异常 assertServiceException(() -> notifyTemplateService.deleteNotifyTemplate(id), NOTIFY_TEMPLATE_NOT_EXISTS); } @Test public void testGetNotifyTemplatePage() { // mock 数据 NotifyTemplateDO dbNotifyTemplate = randomPojo(NotifyTemplateDO.class, o -> { // 等会查询到 o.setName("芋头"); o.setCode("test_01"); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setCreateTime(buildTime(2022, 2, 3)); }); notifyTemplateMapper.insert(dbNotifyTemplate); // 测试 name 不匹配 notifyTemplateMapper.insert(cloneIgnoreId(dbNotifyTemplate, o -> o.setName("投"))); // 测试 code 不匹配 notifyTemplateMapper.insert(cloneIgnoreId(dbNotifyTemplate, o -> o.setCode("test_02"))); // 测试 status 不匹配 notifyTemplateMapper.insert(cloneIgnoreId(dbNotifyTemplate, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); // 测试 createTime 不匹配 notifyTemplateMapper.insert(cloneIgnoreId(dbNotifyTemplate, o -> o.setCreateTime(buildTime(2022, 1, 5)))); // 准备参数 NotifyTemplatePageReqVO reqVO = new NotifyTemplatePageReqVO(); reqVO.setName("芋"); reqVO.setCode("est_01"); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); reqVO.setCreateTime(buildBetweenTime(2022, 2, 1, 2022, 2, 5)); // 调用 PageResult pageResult = notifyTemplateService.getNotifyTemplatePage(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbNotifyTemplate, pageResult.getList().get(0)); } @Test public void testGetNotifyTemplate() { // mock 数据 NotifyTemplateDO dbNotifyTemplate = randomPojo(NotifyTemplateDO.class); notifyTemplateMapper.insert(dbNotifyTemplate); // 准备参数 Long id = dbNotifyTemplate.getId(); // 调用 NotifyTemplateDO notifyTemplate = notifyTemplateService.getNotifyTemplate(id); // 断言 assertPojoEquals(dbNotifyTemplate, notifyTemplate); } @Test public void testGetNotifyTemplateByCodeFromCache() { // mock 数据 NotifyTemplateDO dbNotifyTemplate = randomPojo(NotifyTemplateDO.class); notifyTemplateMapper.insert(dbNotifyTemplate); // 准备参数 String code = dbNotifyTemplate.getCode(); // 调用 NotifyTemplateDO notifyTemplate = notifyTemplateService.getNotifyTemplateByCodeFromCache(code); // 断言 assertPojoEquals(dbNotifyTemplate, notifyTemplate); } @Test public void testFormatNotifyTemplateContent() { // 准备参数 Map params = new HashMap<>(); params.put("name", "小红"); params.put("what", "饭"); // 调用,并断言 assertEquals("小红,你好,饭吃了吗?", notifyTemplateService.formatNotifyTemplateContent("{name},你好,{what}吃了吗?", params)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/oauth2/OAuth2ApproveServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.oauth2; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.util.ObjectUtil; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.util.date.DateUtils; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2ClientDO; import co.yixiang.yshop.module.system.dal.mysql.oauth2.OAuth2ApproveMapper; import org.assertj.core.util.Lists; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.*; import static cn.hutool.core.util.RandomUtil.*; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomString; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; /** * {@link OAuth2ApproveServiceImpl} 的单元测试类 * * @author yshop */ @Import(OAuth2ApproveServiceImpl.class) public class OAuth2ApproveServiceImplTest extends BaseDbUnitTest { @Resource private OAuth2ApproveServiceImpl oauth2ApproveService; @Resource private OAuth2ApproveMapper oauth2ApproveMapper; @MockBean private OAuth2ClientService oauth2ClientService; @Test public void checkForPreApproval_clientAutoApprove() { // 准备参数 Long userId = randomLongId(); Integer userType = randomEle(UserTypeEnum.values()).getValue(); String clientId = randomString(); List requestedScopes = Lists.newArrayList("read"); // mock 方法 when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))) .thenReturn(randomPojo(OAuth2ClientDO.class).setAutoApproveScopes(requestedScopes)); // 调用 boolean success = oauth2ApproveService.checkForPreApproval(userId, userType, clientId, requestedScopes); // 断言 assertTrue(success); List result = oauth2ApproveMapper.selectList(); assertEquals(1, result.size()); assertEquals(userId, result.get(0).getUserId()); assertEquals(userType, result.get(0).getUserType()); assertEquals(clientId, result.get(0).getClientId()); assertEquals("read", result.get(0).getScope()); assertTrue(result.get(0).getApproved()); assertFalse(DateUtils.isExpired(result.get(0).getExpiresTime())); } @Test public void checkForPreApproval_approve() { // 准备参数 Long userId = randomLongId(); Integer userType = randomEle(UserTypeEnum.values()).getValue(); String clientId = randomString(); List requestedScopes = Lists.newArrayList("read"); // mock 方法 when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))) .thenReturn(randomPojo(OAuth2ClientDO.class).setAutoApproveScopes(null)); // mock 数据 OAuth2ApproveDO approve = randomPojo(OAuth2ApproveDO.class).setUserId(userId) .setUserType(userType).setClientId(clientId).setScope("read") .setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 1L, ChronoUnit.DAYS)).setApproved(true); // 同意 oauth2ApproveMapper.insert(approve); // 调用 boolean success = oauth2ApproveService.checkForPreApproval(userId, userType, clientId, requestedScopes); // 断言 assertTrue(success); } @Test public void checkForPreApproval_reject() { // 准备参数 Long userId = randomLongId(); Integer userType = randomEle(UserTypeEnum.values()).getValue(); String clientId = randomString(); List requestedScopes = Lists.newArrayList("read"); // mock 方法 when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))) .thenReturn(randomPojo(OAuth2ClientDO.class).setAutoApproveScopes(null)); // mock 数据 OAuth2ApproveDO approve = randomPojo(OAuth2ApproveDO.class).setUserId(userId) .setUserType(userType).setClientId(clientId).setScope("read") .setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 1L, ChronoUnit.DAYS)).setApproved(false); // 拒绝 oauth2ApproveMapper.insert(approve); // 调用 boolean success = oauth2ApproveService.checkForPreApproval(userId, userType, clientId, requestedScopes); // 断言 assertFalse(success); } @Test public void testUpdateAfterApproval_none() { // 准备参数 Long userId = randomLongId(); Integer userType = randomEle(UserTypeEnum.values()).getValue(); String clientId = randomString(); // 调用 boolean success = oauth2ApproveService.updateAfterApproval(userId, userType, clientId, null); // 断言 assertTrue(success); List result = oauth2ApproveMapper.selectList(); assertEquals(0, result.size()); } @Test public void testUpdateAfterApproval_approved() { // 准备参数 Long userId = randomLongId(); Integer userType = randomEle(UserTypeEnum.values()).getValue(); String clientId = randomString(); Map requestedScopes = new LinkedHashMap<>(); // 有序,方便判断 requestedScopes.put("read", true); requestedScopes.put("write", false); // mock 方法 // 调用 boolean success = oauth2ApproveService.updateAfterApproval(userId, userType, clientId, requestedScopes); // 断言 assertTrue(success); List result = oauth2ApproveMapper.selectList(); assertEquals(2, result.size()); // read assertEquals(userId, result.get(0).getUserId()); assertEquals(userType, result.get(0).getUserType()); assertEquals(clientId, result.get(0).getClientId()); assertEquals("read", result.get(0).getScope()); assertTrue(result.get(0).getApproved()); assertFalse(DateUtils.isExpired(result.get(0).getExpiresTime())); // write assertEquals(userId, result.get(1).getUserId()); assertEquals(userType, result.get(1).getUserType()); assertEquals(clientId, result.get(1).getClientId()); assertEquals("write", result.get(1).getScope()); assertFalse(result.get(1).getApproved()); assertFalse(DateUtils.isExpired(result.get(1).getExpiresTime())); } @Test public void testUpdateAfterApproval_reject() { // 准备参数 Long userId = randomLongId(); Integer userType = randomEle(UserTypeEnum.values()).getValue(); String clientId = randomString(); Map requestedScopes = new LinkedHashMap<>(); requestedScopes.put("write", false); // mock 方法 // 调用 boolean success = oauth2ApproveService.updateAfterApproval(userId, userType, clientId, requestedScopes); // 断言 assertFalse(success); List result = oauth2ApproveMapper.selectList(); assertEquals(1, result.size()); // write assertEquals(userId, result.get(0).getUserId()); assertEquals(userType, result.get(0).getUserType()); assertEquals(clientId, result.get(0).getClientId()); assertEquals("write", result.get(0).getScope()); assertFalse(result.get(0).getApproved()); assertFalse(DateUtils.isExpired(result.get(0).getExpiresTime())); } @Test public void testGetApproveList() { // 准备参数 Long userId = 10L; Integer userType = UserTypeEnum.ADMIN.getValue(); String clientId = randomString(); // mock 数据 OAuth2ApproveDO approve = randomPojo(OAuth2ApproveDO.class).setUserId(userId) .setUserType(userType).setClientId(clientId).setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 1L, ChronoUnit.DAYS)); oauth2ApproveMapper.insert(approve); // 未过期 oauth2ApproveMapper.insert(ObjectUtil.clone(approve).setId(null) .setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), -1L, ChronoUnit.DAYS))); // 已过期 // 调用 List result = oauth2ApproveService.getApproveList(userId, userType, clientId); // 断言 assertEquals(1, result.size()); assertPojoEquals(approve, result.get(0)); } @Test public void testSaveApprove_insert() { // 准备参数 Long userId = randomLongId(); Integer userType = randomEle(UserTypeEnum.values()).getValue(); String clientId = randomString(); String scope = randomString(); Boolean approved = randomBoolean(); LocalDateTime expireTime = LocalDateTime.ofInstant(randomDay(1, 30).toInstant(), ZoneId.systemDefault()); // mock 方法 // 调用 oauth2ApproveService.saveApprove(userId, userType, clientId, scope, approved, expireTime); // 断言 List result = oauth2ApproveMapper.selectList(); assertEquals(1, result.size()); assertEquals(userId, result.get(0).getUserId()); assertEquals(userType, result.get(0).getUserType()); assertEquals(clientId, result.get(0).getClientId()); assertEquals(scope, result.get(0).getScope()); assertEquals(approved, result.get(0).getApproved()); assertEquals(expireTime, result.get(0).getExpiresTime()); } @Test public void testSaveApprove_update() { // mock 数据 OAuth2ApproveDO approve = randomPojo(OAuth2ApproveDO.class); oauth2ApproveMapper.insert(approve); // 准备参数 Long userId = approve.getUserId(); Integer userType = approve.getUserType(); String clientId = approve.getClientId(); String scope = approve.getScope(); Boolean approved = randomBoolean(); LocalDateTime expireTime = LocalDateTime.ofInstant(randomDay(1, 30).toInstant(), ZoneId.systemDefault()); // mock 方法 // 调用 oauth2ApproveService.saveApprove(userId, userType, clientId, scope, approved, expireTime); // 断言 List result = oauth2ApproveMapper.selectList(); assertEquals(1, result.size()); assertEquals(approve.getId(), result.get(0).getId()); assertEquals(userId, result.get(0).getUserId()); assertEquals(userType, result.get(0).getUserType()); assertEquals(clientId, result.get(0).getClientId()); assertEquals(scope, result.get(0).getScope()); assertEquals(approved, result.get(0).getApproved()); assertEquals(expireTime, result.get(0).getExpiresTime()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/oauth2/OAuth2ClientServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.oauth2; import cn.hutool.extra.spring.SpringUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.client.OAuth2ClientSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2ClientDO; import co.yixiang.yshop.module.system.dal.mysql.oauth2.OAuth2ClientMapper; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.util.Collections; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mockStatic; /** * {@link OAuth2ClientServiceImpl} 的单元测试类 * * @author yshop */ @Import(OAuth2ClientServiceImpl.class) public class OAuth2ClientServiceImplTest extends BaseDbUnitTest { @Resource private OAuth2ClientServiceImpl oauth2ClientService; @Resource private OAuth2ClientMapper oauth2ClientMapper; @Test public void testCreateOAuth2Client_success() { // 准备参数 OAuth2ClientSaveReqVO reqVO = randomPojo(OAuth2ClientSaveReqVO.class, o -> o.setLogo(randomString())) .setId(null); // 防止 id 被赋值 // 调用 Long oauth2ClientId = oauth2ClientService.createOAuth2Client(reqVO); // 断言 assertNotNull(oauth2ClientId); // 校验记录的属性是否正确 OAuth2ClientDO oAuth2Client = oauth2ClientMapper.selectById(oauth2ClientId); assertPojoEquals(reqVO, oAuth2Client, "id"); } @Test public void testUpdateOAuth2Client_success() { // mock 数据 OAuth2ClientDO dbOAuth2Client = randomPojo(OAuth2ClientDO.class); oauth2ClientMapper.insert(dbOAuth2Client);// @Sql: 先插入出一条存在的数据 // 准备参数 OAuth2ClientSaveReqVO reqVO = randomPojo(OAuth2ClientSaveReqVO.class, o -> { o.setId(dbOAuth2Client.getId()); // 设置更新的 ID o.setLogo(randomString()); }); // 调用 oauth2ClientService.updateOAuth2Client(reqVO); // 校验是否更新正确 OAuth2ClientDO oAuth2Client = oauth2ClientMapper.selectById(reqVO.getId()); // 获取最新的 assertPojoEquals(reqVO, oAuth2Client); } @Test public void testUpdateOAuth2Client_notExists() { // 准备参数 OAuth2ClientSaveReqVO reqVO = randomPojo(OAuth2ClientSaveReqVO.class); // 调用, 并断言异常 assertServiceException(() -> oauth2ClientService.updateOAuth2Client(reqVO), OAUTH2_CLIENT_NOT_EXISTS); } @Test public void testDeleteOAuth2Client_success() { // mock 数据 OAuth2ClientDO dbOAuth2Client = randomPojo(OAuth2ClientDO.class); oauth2ClientMapper.insert(dbOAuth2Client);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbOAuth2Client.getId(); // 调用 oauth2ClientService.deleteOAuth2Client(id); // 校验数据不存在了 assertNull(oauth2ClientMapper.selectById(id)); } @Test public void testDeleteOAuth2Client_notExists() { // 准备参数 Long id = randomLongId(); // 调用, 并断言异常 assertServiceException(() -> oauth2ClientService.deleteOAuth2Client(id), OAUTH2_CLIENT_NOT_EXISTS); } @Test public void testValidateClientIdExists_withId() { // mock 数据 OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("tudou"); oauth2ClientMapper.insert(client); // 准备参数 Long id = randomLongId(); String clientId = "tudou"; // 调用,不会报错 assertServiceException(() -> oauth2ClientService.validateClientIdExists(id, clientId), OAUTH2_CLIENT_EXISTS); } @Test public void testValidateClientIdExists_noId() { // mock 数据 OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("tudou"); oauth2ClientMapper.insert(client); // 准备参数 String clientId = "tudou"; // 调用,不会报错 assertServiceException(() -> oauth2ClientService.validateClientIdExists(null, clientId), OAUTH2_CLIENT_EXISTS); } @Test public void testGetOAuth2Client() { // mock 数据 OAuth2ClientDO clientDO = randomPojo(OAuth2ClientDO.class); oauth2ClientMapper.insert(clientDO); // 准备参数 Long id = clientDO.getId(); // 调用,并断言 OAuth2ClientDO dbClientDO = oauth2ClientService.getOAuth2Client(id); assertPojoEquals(clientDO, dbClientDO); } @Test public void testGetOAuth2ClientFromCache() { // mock 数据 OAuth2ClientDO clientDO = randomPojo(OAuth2ClientDO.class); oauth2ClientMapper.insert(clientDO); // 准备参数 String clientId = clientDO.getClientId(); // 调用,并断言 OAuth2ClientDO dbClientDO = oauth2ClientService.getOAuth2ClientFromCache(clientId); assertPojoEquals(clientDO, dbClientDO); } @Test public void testGetOAuth2ClientPage() { // mock 数据 OAuth2ClientDO dbOAuth2Client = randomPojo(OAuth2ClientDO.class, o -> { // 等会查询到 o.setName("潜龙"); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); }); oauth2ClientMapper.insert(dbOAuth2Client); // 测试 name 不匹配 oauth2ClientMapper.insert(cloneIgnoreId(dbOAuth2Client, o -> o.setName("凤凰"))); // 测试 status 不匹配 oauth2ClientMapper.insert(cloneIgnoreId(dbOAuth2Client, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); // 准备参数 OAuth2ClientPageReqVO reqVO = new OAuth2ClientPageReqVO(); reqVO.setName("龙"); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 调用 PageResult pageResult = oauth2ClientService.getOAuth2ClientPage(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbOAuth2Client, pageResult.getList().get(0)); } @Test public void testValidOAuthClientFromCache() { try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(OAuth2ClientServiceImpl.class))) .thenReturn(oauth2ClientService); // mock 方法 OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("default") .setStatus(CommonStatusEnum.ENABLE.getStatus()); oauth2ClientMapper.insert(client); OAuth2ClientDO client02 = randomPojo(OAuth2ClientDO.class).setClientId("disable") .setStatus(CommonStatusEnum.DISABLE.getStatus()); oauth2ClientMapper.insert(client02); // 调用,并断言 assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache(randomString(), null, null, null, null), OAUTH2_CLIENT_NOT_EXISTS); assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache("disable", null, null, null, null), OAUTH2_CLIENT_DISABLE); assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache("default", randomString(), null, null, null), OAUTH2_CLIENT_CLIENT_SECRET_ERROR); assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache("default", null, randomString(), null, null), OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS); assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache("default", null, null, Collections.singleton(randomString()), null), OAUTH2_CLIENT_SCOPE_OVER); assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache("default", null, null, null, "test"), OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH, "test"); // 成功调用(1:参数完整) OAuth2ClientDO result = oauth2ClientService.validOAuthClientFromCache(client.getClientId(), client.getSecret(), client.getAuthorizedGrantTypes().get(0), client.getScopes(), client.getRedirectUris().get(0)); assertPojoEquals(client, result); // 成功调用(2:只有 clientId 参数) result = oauth2ClientService.validOAuthClientFromCache(client.getClientId()); assertPojoEquals(client, result); } } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/oauth2/OAuth2CodeServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.oauth2; import cn.hutool.core.util.RandomUtil; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.util.date.DateUtils; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2CodeDO; import co.yixiang.yshop.module.system.dal.mysql.oauth2.OAuth2CodeMapper; import org.assertj.core.util.Lists; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.time.LocalDateTime; import java.util.List; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.OAUTH2_CODE_EXPIRE; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.OAUTH2_CODE_NOT_EXISTS; import static org.junit.jupiter.api.Assertions.*; /** * {@link OAuth2CodeServiceImpl} 的单元测试类 * * @author yshop */ @Import(OAuth2CodeServiceImpl.class) class OAuth2CodeServiceImplTest extends BaseDbUnitTest { @Resource private OAuth2CodeServiceImpl oauth2CodeService; @Resource private OAuth2CodeMapper oauth2CodeMapper; @Test public void testCreateAuthorizationCode() { // 准备参数 Long userId = randomLongId(); Integer userType = RandomUtil.randomEle(UserTypeEnum.values()).getValue(); String clientId = randomString(); List scopes = Lists.newArrayList("read", "write"); String redirectUri = randomString(); String state = randomString(); // 调用 OAuth2CodeDO codeDO = oauth2CodeService.createAuthorizationCode(userId, userType, clientId, scopes, redirectUri, state); // 断言 OAuth2CodeDO dbCodeDO = oauth2CodeMapper.selectByCode(codeDO.getCode()); assertPojoEquals(codeDO, dbCodeDO, "createTime", "updateTime", "deleted"); assertEquals(userId, codeDO.getUserId()); assertEquals(userType, codeDO.getUserType()); assertEquals(clientId, codeDO.getClientId()); assertEquals(scopes, codeDO.getScopes()); assertEquals(redirectUri, codeDO.getRedirectUri()); assertEquals(state, codeDO.getState()); assertFalse(DateUtils.isExpired(codeDO.getExpiresTime())); } @Test public void testConsumeAuthorizationCode_null() { // 调用,并断言 assertServiceException(() -> oauth2CodeService.consumeAuthorizationCode(randomString()), OAUTH2_CODE_NOT_EXISTS); } @Test public void testConsumeAuthorizationCode_expired() { // 准备参数 String code = "test_code"; // mock 数据 OAuth2CodeDO codeDO = randomPojo(OAuth2CodeDO.class).setCode(code) .setExpiresTime(LocalDateTime.now().minusDays(1)); oauth2CodeMapper.insert(codeDO); // 调用,并断言 assertServiceException(() -> oauth2CodeService.consumeAuthorizationCode(code), OAUTH2_CODE_EXPIRE); } @Test public void testConsumeAuthorizationCode_success() { // 准备参数 String code = "test_code"; // mock 数据 OAuth2CodeDO codeDO = randomPojo(OAuth2CodeDO.class).setCode(code) .setExpiresTime(LocalDateTime.now().plusDays(1)); oauth2CodeMapper.insert(codeDO); // 调用 OAuth2CodeDO result = oauth2CodeService.consumeAuthorizationCode(code); assertPojoEquals(codeDO, result); assertNull(oauth2CodeMapper.selectByCode(code)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/oauth2/OAuth2GrantServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.oauth2; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.test.core.ut.BaseMockitoUnitTest; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2CodeDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.module.system.service.auth.AdminAuthService; import com.google.common.collect.Lists; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import java.util.List; import static cn.hutool.core.util.RandomUtil.randomEle; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static java.util.Collections.emptyList; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; /** * {@link OAuth2GrantServiceImpl} 的单元测试 * * @author yshop */ public class OAuth2GrantServiceImplTest extends BaseMockitoUnitTest { @InjectMocks private OAuth2GrantServiceImpl oauth2GrantService; @Mock private OAuth2TokenService oauth2TokenService; @Mock private OAuth2CodeService oauth2CodeService; @Mock private AdminAuthService adminAuthService; @Test public void testGrantImplicit() { // 准备参数 Long userId = randomLongId(); Integer userType = randomEle(UserTypeEnum.values()).getValue(); String clientId = randomString(); List scopes = Lists.newArrayList("read", "write"); // mock 方法 OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class); when(oauth2TokenService.createAccessToken(eq(userId), eq(userType), eq(clientId), eq(scopes))).thenReturn(accessTokenDO); // 调用,并断言 assertPojoEquals(accessTokenDO, oauth2GrantService.grantImplicit( userId, userType, clientId, scopes)); } @Test public void testGrantAuthorizationCodeForCode() { // 准备参数 Long userId = randomLongId(); Integer userType = randomEle(UserTypeEnum.values()).getValue(); String clientId = randomString(); List scopes = Lists.newArrayList("read", "write"); String redirectUri = randomString(); String state = randomString(); // mock 方法 OAuth2CodeDO codeDO = randomPojo(OAuth2CodeDO.class); when(oauth2CodeService.createAuthorizationCode(eq(userId), eq(userType), eq(clientId), eq(scopes), eq(redirectUri), eq(state))).thenReturn(codeDO); // 调用,并断言 assertEquals(codeDO.getCode(), oauth2GrantService.grantAuthorizationCodeForCode(userId, userType, clientId, scopes, redirectUri, state)); } @Test public void testGrantAuthorizationCodeForAccessToken() { // 准备参数 String clientId = randomString(); String code = randomString(); List scopes = Lists.newArrayList("read", "write"); String redirectUri = randomString(); String state = randomString(); // mock 方法(code) OAuth2CodeDO codeDO = randomPojo(OAuth2CodeDO.class, o -> { o.setClientId(clientId); o.setRedirectUri(redirectUri); o.setState(state); o.setScopes(scopes); }); when(oauth2CodeService.consumeAuthorizationCode(eq(code))).thenReturn(codeDO); // mock 方法(创建令牌) OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class); when(oauth2TokenService.createAccessToken(eq(codeDO.getUserId()), eq(codeDO.getUserType()), eq(codeDO.getClientId()), eq(codeDO.getScopes()))).thenReturn(accessTokenDO); // 调用,并断言 assertPojoEquals(accessTokenDO, oauth2GrantService.grantAuthorizationCodeForAccessToken( clientId, code, redirectUri, state)); } @Test public void testGrantPassword() { // 准备参数 String username = randomString(); String password = randomString(); String clientId = randomString(); List scopes = Lists.newArrayList("read", "write"); // mock 方法(认证) AdminUserDO user = randomPojo(AdminUserDO.class); when(adminAuthService.authenticate(eq(username), eq(password))).thenReturn(user); // mock 方法(访问令牌) OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class); when(oauth2TokenService.createAccessToken(eq(user.getId()), eq(UserTypeEnum.ADMIN.getValue()), eq(clientId), eq(scopes))).thenReturn(accessTokenDO); // 调用,并断言 assertPojoEquals(accessTokenDO, oauth2GrantService.grantPassword( username, password, clientId, scopes)); } @Test public void testGrantRefreshToken() { // 准备参数 String refreshToken = randomString(); String clientId = randomString(); // mock 方法 OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class); when(oauth2TokenService.refreshAccessToken(eq(refreshToken), eq(clientId))) .thenReturn(accessTokenDO); // 调用,并断言 assertPojoEquals(accessTokenDO, oauth2GrantService.grantRefreshToken( refreshToken, clientId)); } @Test public void testGrantClientCredentials() { assertThrows(UnsupportedOperationException.class, () -> oauth2GrantService.grantClientCredentials(randomString(), emptyList()), "暂时不支持 client_credentials 授权模式"); } @Test public void testRevokeToken_clientIdError() { // 准备参数 String clientId = randomString(); String accessToken = randomString(); // mock 方法 OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class); when(oauth2TokenService.getAccessToken(eq(accessToken))).thenReturn(accessTokenDO); // 调用,并断言 assertFalse(oauth2GrantService.revokeToken(clientId, accessToken)); } @Test public void testRevokeToken_success() { // 准备参数 String clientId = randomString(); String accessToken = randomString(); // mock 方法(访问令牌) OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class).setClientId(clientId); when(oauth2TokenService.getAccessToken(eq(accessToken))).thenReturn(accessTokenDO); // mock 方法(移除) when(oauth2TokenService.removeAccessToken(eq(accessToken))).thenReturn(accessTokenDO); // 调用,并断言 assertTrue(oauth2GrantService.revokeToken(clientId, accessToken)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/oauth2/OAuth2TokenServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.oauth2; import cn.hutool.core.date.LocalDateTimeUtil; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.exception.ErrorCode; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.date.DateUtils; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import co.yixiang.yshop.framework.test.core.ut.BaseDbAndRedisUnitTest; import co.yixiang.yshop.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2ClientDO; import co.yixiang.yshop.module.system.dal.dataobject.oauth2.OAuth2RefreshTokenDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.module.system.dal.mysql.oauth2.OAuth2AccessTokenMapper; import co.yixiang.yshop.module.system.dal.mysql.oauth2.OAuth2RefreshTokenMapper; import co.yixiang.yshop.module.system.dal.redis.oauth2.OAuth2AccessTokenRedisDAO; import co.yixiang.yshop.module.system.service.user.AdminUserService; import jakarta.annotation.Resource; import org.assertj.core.util.Lists; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import java.time.LocalDateTime; import java.util.List; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; /** * {@link OAuth2TokenServiceImpl} 的单元测试类 * * @author yshop */ @Import({OAuth2TokenServiceImpl.class, OAuth2AccessTokenRedisDAO.class}) public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest { @Resource private OAuth2TokenServiceImpl oauth2TokenService; @Resource private OAuth2AccessTokenMapper oauth2AccessTokenMapper; @Resource private OAuth2RefreshTokenMapper oauth2RefreshTokenMapper; @Resource private OAuth2AccessTokenRedisDAO oauth2AccessTokenRedisDAO; @MockBean private OAuth2ClientService oauth2ClientService; @MockBean private AdminUserService adminUserService; @Test public void testCreateAccessToken() { TenantContextHolder.setTenantId(0L); // 准备参数 Long userId = randomLongId(); Integer userType = UserTypeEnum.ADMIN.getValue(); String clientId = randomString(); List scopes = Lists.newArrayList("read", "write"); // mock 方法 OAuth2ClientDO clientDO = randomPojo(OAuth2ClientDO.class).setClientId(clientId) .setAccessTokenValiditySeconds(30).setRefreshTokenValiditySeconds(60); when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(clientDO); // mock 数据(用户) AdminUserDO user = randomPojo(AdminUserDO.class); when(adminUserService.getUser(userId)).thenReturn(user); // 调用 OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, userType, clientId, scopes); // 断言访问令牌 OAuth2AccessTokenDO dbAccessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessTokenDO.getAccessToken()); assertPojoEquals(accessTokenDO, dbAccessTokenDO, "createTime", "updateTime", "deleted"); assertEquals(userId, accessTokenDO.getUserId()); assertEquals(userType, accessTokenDO.getUserType()); assertEquals(2, accessTokenDO.getUserInfo().size()); assertEquals(user.getNickname(), accessTokenDO.getUserInfo().get("nickname")); assertEquals(user.getDeptId().toString(), accessTokenDO.getUserInfo().get("deptId")); assertEquals(clientId, accessTokenDO.getClientId()); assertEquals(scopes, accessTokenDO.getScopes()); assertFalse(DateUtils.isExpired(accessTokenDO.getExpiresTime())); // 断言访问令牌的缓存 OAuth2AccessTokenDO redisAccessTokenDO = oauth2AccessTokenRedisDAO.get(accessTokenDO.getAccessToken()); assertPojoEquals(accessTokenDO, redisAccessTokenDO, "createTime", "updateTime", "deleted"); // 断言刷新令牌 OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectList().get(0); assertPojoEquals(accessTokenDO, refreshTokenDO, "id", "expiresTime", "createTime", "updateTime", "deleted"); assertFalse(DateUtils.isExpired(refreshTokenDO.getExpiresTime())); } @Test public void testRefreshAccessToken_null() { // 准备参数 String refreshToken = randomString(); String clientId = randomString(); // mock 方法 // 调用,并断言 assertServiceException(() -> oauth2TokenService.refreshAccessToken(refreshToken, clientId), new ErrorCode(400, "无效的刷新令牌")); } @Test public void testRefreshAccessToken_clientIdError() { // 准备参数 String refreshToken = randomString(); String clientId = randomString(); // mock 方法 OAuth2ClientDO clientDO = randomPojo(OAuth2ClientDO.class).setClientId(clientId); when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(clientDO); // mock 数据(访问令牌) OAuth2RefreshTokenDO refreshTokenDO = randomPojo(OAuth2RefreshTokenDO.class) .setRefreshToken(refreshToken).setClientId("error"); oauth2RefreshTokenMapper.insert(refreshTokenDO); // 调用,并断言 assertServiceException(() -> oauth2TokenService.refreshAccessToken(refreshToken, clientId), new ErrorCode(400, "刷新令牌的客户端编号不正确")); } @Test public void testRefreshAccessToken_expired() { // 准备参数 String refreshToken = randomString(); String clientId = randomString(); // mock 方法 OAuth2ClientDO clientDO = randomPojo(OAuth2ClientDO.class).setClientId(clientId); when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(clientDO); // mock 数据(访问令牌) OAuth2RefreshTokenDO refreshTokenDO = randomPojo(OAuth2RefreshTokenDO.class) .setRefreshToken(refreshToken).setClientId(clientId) .setExpiresTime(LocalDateTime.now().minusDays(1)); oauth2RefreshTokenMapper.insert(refreshTokenDO); // 调用,并断言 assertServiceException(() -> oauth2TokenService.refreshAccessToken(refreshToken, clientId), new ErrorCode(401, "刷新令牌已过期")); assertEquals(0, oauth2RefreshTokenMapper.selectCount()); } @Test public void testRefreshAccessToken_success() { TenantContextHolder.setTenantId(0L); // 准备参数 String refreshToken = randomString(); String clientId = randomString(); // mock 方法 OAuth2ClientDO clientDO = randomPojo(OAuth2ClientDO.class).setClientId(clientId) .setAccessTokenValiditySeconds(30); when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(clientDO); // mock 数据(访问令牌) OAuth2RefreshTokenDO refreshTokenDO = randomPojo(OAuth2RefreshTokenDO.class) .setRefreshToken(refreshToken).setClientId(clientId) .setExpiresTime(LocalDateTime.now().plusDays(1)) .setUserType(UserTypeEnum.ADMIN.getValue()); oauth2RefreshTokenMapper.insert(refreshTokenDO); // mock 数据(访问令牌) OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class).setRefreshToken(refreshToken) .setUserType(refreshTokenDO.getUserType()); oauth2AccessTokenMapper.insert(accessTokenDO); oauth2AccessTokenRedisDAO.set(accessTokenDO); // mock 数据(用户) AdminUserDO user = randomPojo(AdminUserDO.class); when(adminUserService.getUser(refreshTokenDO.getUserId())).thenReturn(user); // 调用 OAuth2AccessTokenDO newAccessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, clientId); // 断言,老的访问令牌被删除 assertNull(oauth2AccessTokenMapper.selectByAccessToken(accessTokenDO.getAccessToken())); assertNull(oauth2AccessTokenRedisDAO.get(accessTokenDO.getAccessToken())); // 断言,新的访问令牌 OAuth2AccessTokenDO dbAccessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(newAccessTokenDO.getAccessToken()); assertPojoEquals(newAccessTokenDO, dbAccessTokenDO, "createTime", "updateTime", "deleted"); assertPojoEquals(newAccessTokenDO, refreshTokenDO, "id", "expiresTime", "createTime", "updateTime", "deleted", "creator", "updater"); assertFalse(DateUtils.isExpired(newAccessTokenDO.getExpiresTime())); // 断言,新的访问令牌的缓存 OAuth2AccessTokenDO redisAccessTokenDO = oauth2AccessTokenRedisDAO.get(newAccessTokenDO.getAccessToken()); assertPojoEquals(newAccessTokenDO, redisAccessTokenDO, "createTime", "updateTime", "deleted"); } @Test public void testGetAccessToken() { // mock 数据(访问令牌) OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) .setExpiresTime(LocalDateTime.now().plusDays(1)); oauth2AccessTokenMapper.insert(accessTokenDO); // 准备参数 String accessToken = accessTokenDO.getAccessToken(); // 调用 OAuth2AccessTokenDO result = oauth2TokenService.getAccessToken(accessToken); // 断言 assertPojoEquals(accessTokenDO, result, "createTime", "updateTime", "deleted", "creator", "updater"); assertPojoEquals(accessTokenDO, oauth2AccessTokenRedisDAO.get(accessToken), "createTime", "updateTime", "deleted", "creator", "updater"); } @Test public void testCheckAccessToken_null() { // 调研,并断言 assertServiceException(() -> oauth2TokenService.checkAccessToken(randomString()), new ErrorCode(401, "访问令牌不存在")); } @Test public void testCheckAccessToken_expired() { // mock 数据(访问令牌) OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) .setExpiresTime(LocalDateTime.now().minusDays(1)); oauth2AccessTokenMapper.insert(accessTokenDO); // 准备参数 String accessToken = accessTokenDO.getAccessToken(); // 调研,并断言 assertServiceException(() -> oauth2TokenService.checkAccessToken(accessToken), new ErrorCode(401, "访问令牌已过期")); } @Test public void testCheckAccessToken_success() { // mock 数据(访问令牌) OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) .setExpiresTime(LocalDateTime.now().plusDays(1)); oauth2AccessTokenMapper.insert(accessTokenDO); // 准备参数 String accessToken = accessTokenDO.getAccessToken(); // 调研,并断言 OAuth2AccessTokenDO result = oauth2TokenService.getAccessToken(accessToken); // 断言 assertPojoEquals(accessTokenDO, result, "createTime", "updateTime", "deleted", "creator", "updater"); } @Test public void testRemoveAccessToken_null() { // 调用,并断言 assertNull(oauth2TokenService.removeAccessToken(randomString())); } @Test public void testRemoveAccessToken_success() { // mock 数据(访问令牌) OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) .setExpiresTime(LocalDateTime.now().plusDays(1)); oauth2AccessTokenMapper.insert(accessTokenDO); // mock 数据(刷新令牌) OAuth2RefreshTokenDO refreshTokenDO = randomPojo(OAuth2RefreshTokenDO.class) .setRefreshToken(accessTokenDO.getRefreshToken()); oauth2RefreshTokenMapper.insert(refreshTokenDO); // 调用 OAuth2AccessTokenDO result = oauth2TokenService.removeAccessToken(accessTokenDO.getAccessToken()); assertPojoEquals(accessTokenDO, result, "createTime", "updateTime", "deleted", "creator", "updater"); // 断言数据 assertNull(oauth2AccessTokenMapper.selectByAccessToken(accessTokenDO.getAccessToken())); assertNull(oauth2RefreshTokenMapper.selectByRefreshToken(accessTokenDO.getRefreshToken())); assertNull(oauth2AccessTokenRedisDAO.get(accessTokenDO.getAccessToken())); } @Test public void testGetAccessTokenPage() { // mock 数据 OAuth2AccessTokenDO dbAccessToken = randomPojo(OAuth2AccessTokenDO.class, o -> { // 等会查询到 o.setUserId(10L); o.setUserType(1); o.setClientId("test_client"); o.setExpiresTime(LocalDateTime.now().plusDays(1)); }); oauth2AccessTokenMapper.insert(dbAccessToken); // 测试 userId 不匹配 oauth2AccessTokenMapper.insert(cloneIgnoreId(dbAccessToken, o -> o.setUserId(20L))); // 测试 userType 不匹配 oauth2AccessTokenMapper.insert(cloneIgnoreId(dbAccessToken, o -> o.setUserType(2))); // 测试 userType 不匹配 oauth2AccessTokenMapper.insert(cloneIgnoreId(dbAccessToken, o -> o.setClientId("it_client"))); // 测试 expireTime 不匹配 oauth2AccessTokenMapper.insert(cloneIgnoreId(dbAccessToken, o -> o.setExpiresTime(LocalDateTimeUtil.now()))); // 准备参数 OAuth2AccessTokenPageReqVO reqVO = new OAuth2AccessTokenPageReqVO(); reqVO.setUserId(10L); reqVO.setUserType(1); reqVO.setClientId("test"); // 调用 PageResult pageResult = oauth2TokenService.getAccessTokenPage(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbAccessToken, pageResult.getList().get(0)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/permission/MenuServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.permission; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.controller.admin.permission.vo.menu.MenuListReqVO; import co.yixiang.yshop.module.system.controller.admin.permission.vo.menu.MenuSaveVO; import co.yixiang.yshop.module.system.dal.dataobject.permission.MenuDO; import co.yixiang.yshop.module.system.dal.mysql.permission.MenuMapper; import co.yixiang.yshop.module.system.enums.permission.MenuTypeEnum; import co.yixiang.yshop.module.system.service.tenant.TenantService; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; import static co.yixiang.yshop.framework.common.util.collection.SetUtils.asSet; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static co.yixiang.yshop.module.system.dal.dataobject.permission.MenuDO.ID_ROOT; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.verify; @Import(MenuServiceImpl.class) public class MenuServiceImplTest extends BaseDbUnitTest { @Resource private MenuServiceImpl menuService; @Resource private MenuMapper menuMapper; @MockBean private PermissionService permissionService; @MockBean private TenantService tenantService; @Test public void testCreateMenu_success() { // mock 数据(构造父菜单) MenuDO menuDO = buildMenuDO(MenuTypeEnum.MENU, "parent", 0L); menuMapper.insert(menuDO); Long parentId = menuDO.getId(); // 准备参数 MenuSaveVO reqVO = randomPojo(MenuSaveVO.class, o -> { o.setParentId(parentId); o.setName("testSonName"); o.setType(MenuTypeEnum.MENU.getType()); }).setId(null); // 防止 id 被赋值 Long menuId = menuService.createMenu(reqVO); // 校验记录的属性是否正确 MenuDO dbMenu = menuMapper.selectById(menuId); assertPojoEquals(reqVO, dbMenu, "id"); } @Test public void testUpdateMenu_success() { // mock 数据(构造父子菜单) MenuDO sonMenuDO = createParentAndSonMenu(); Long sonId = sonMenuDO.getId(); // 准备参数 MenuSaveVO reqVO = randomPojo(MenuSaveVO.class, o -> { o.setId(sonId); o.setName("testSonName"); // 修改名字 o.setParentId(sonMenuDO.getParentId()); o.setType(MenuTypeEnum.MENU.getType()); }); // 调用 menuService.updateMenu(reqVO); // 校验记录的属性是否正确 MenuDO dbMenu = menuMapper.selectById(sonId); assertPojoEquals(reqVO, dbMenu); } @Test public void testUpdateMenu_sonIdNotExist() { // 准备参数 MenuSaveVO reqVO = randomPojo(MenuSaveVO.class); // 调用,并断言异常 assertServiceException(() -> menuService.updateMenu(reqVO), MENU_NOT_EXISTS); } @Test public void testDeleteMenu_success() { // mock 数据 MenuDO menuDO = randomPojo(MenuDO.class); menuMapper.insert(menuDO); // 准备参数 Long id = menuDO.getId(); // 调用 menuService.deleteMenu(id); // 断言 MenuDO dbMenuDO = menuMapper.selectById(id); assertNull(dbMenuDO); verify(permissionService).processMenuDeleted(id); } @Test public void testDeleteMenu_menuNotExist() { assertServiceException(() -> menuService.deleteMenu(randomLongId()), MENU_NOT_EXISTS); } @Test public void testDeleteMenu_existChildren() { // mock 数据(构造父子菜单) MenuDO sonMenu = createParentAndSonMenu(); // 准备参数 Long parentId = sonMenu.getParentId(); // 调用并断言异常 assertServiceException(() -> menuService.deleteMenu(parentId), MENU_EXISTS_CHILDREN); } @Test public void testGetMenuList_all() { // mock 数据 MenuDO menu100 = randomPojo(MenuDO.class); menuMapper.insert(menu100); MenuDO menu101 = randomPojo(MenuDO.class); menuMapper.insert(menu101); // 准备参数 // 调用 List list = menuService.getMenuList(); // 断言 assertEquals(2, list.size()); assertPojoEquals(menu100, list.get(0)); assertPojoEquals(menu101, list.get(1)); } @Test public void testGetMenuList() { // mock 数据 MenuDO menuDO = randomPojo(MenuDO.class, o -> o.setName("yshop").setStatus(CommonStatusEnum.ENABLE.getStatus())); menuMapper.insert(menuDO); // 测试 status 不匹配 menuMapper.insert(cloneIgnoreId(menuDO, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); // 测试 name 不匹配 menuMapper.insert(cloneIgnoreId(menuDO, o -> o.setName("艿"))); // 准备参数 MenuListReqVO reqVO = new MenuListReqVO().setName("芋").setStatus(CommonStatusEnum.ENABLE.getStatus()); // 调用 List result = menuService.getMenuList(reqVO); // 断言 assertEquals(1, result.size()); assertPojoEquals(menuDO, result.get(0)); } @Test public void testGetMenuListByTenant() { // mock 数据 MenuDO menu100 = randomPojo(MenuDO.class, o -> o.setId(100L).setStatus(CommonStatusEnum.ENABLE.getStatus())); menuMapper.insert(menu100); MenuDO menu101 = randomPojo(MenuDO.class, o -> o.setId(101L).setStatus(CommonStatusEnum.DISABLE.getStatus())); menuMapper.insert(menu101); MenuDO menu102 = randomPojo(MenuDO.class, o -> o.setId(102L).setStatus(CommonStatusEnum.ENABLE.getStatus())); menuMapper.insert(menu102); // mock 过滤菜单 Set menuIds = asSet(100L, 101L); doNothing().when(tenantService).handleTenantMenu(argThat(handler -> { handler.handle(menuIds); return true; })); // 准备参数 MenuListReqVO reqVO = new MenuListReqVO().setStatus(CommonStatusEnum.ENABLE.getStatus()); // 调用 List result = menuService.getMenuListByTenant(reqVO); // 断言 assertEquals(1, result.size()); assertPojoEquals(menu100, result.get(0)); } @Test public void testGetMenuIdListByPermissionFromCache() { // mock 数据 MenuDO menu100 = randomPojo(MenuDO.class); menuMapper.insert(menu100); MenuDO menu101 = randomPojo(MenuDO.class); menuMapper.insert(menu101); // 准备参数 String permission = menu100.getPermission(); // 调用 List ids = menuService.getMenuIdListByPermissionFromCache(permission); // 断言 assertEquals(1, ids.size()); assertEquals(menu100.getId(), ids.get(0)); } @Test public void testGetMenuList_ids() { // mock 数据 MenuDO menu100 = randomPojo(MenuDO.class); menuMapper.insert(menu100); MenuDO menu101 = randomPojo(MenuDO.class); menuMapper.insert(menu101); // 准备参数 Collection ids = Collections.singleton(menu100.getId()); // 调用 List list = menuService.getMenuList(ids); // 断言 assertEquals(1, list.size()); assertPojoEquals(menu100, list.get(0)); } @Test public void testGetMenu() { // mock 数据 MenuDO menu = randomPojo(MenuDO.class); menuMapper.insert(menu); // 准备参数 Long id = menu.getId(); // 调用 MenuDO dbMenu = menuService.getMenu(id); // 断言 assertPojoEquals(menu, dbMenu); } @Test public void testValidateParentMenu_success() { // mock 数据 MenuDO menuDO = buildMenuDO(MenuTypeEnum.MENU, "parent", 0L); menuMapper.insert(menuDO); // 准备参数 Long parentId = menuDO.getId(); // 调用,无需断言 menuService.validateParentMenu(parentId, null); } @Test public void testValidateParentMenu_canNotSetSelfToBeParent() { // 调用,并断言异常 assertServiceException(() -> menuService.validateParentMenu(1L, 1L), MENU_PARENT_ERROR); } @Test public void testValidateParentMenu_parentNotExist() { // 调用,并断言异常 assertServiceException(() -> menuService.validateParentMenu(randomLongId(), null), MENU_PARENT_NOT_EXISTS); } @Test public void testValidateParentMenu_parentTypeError() { // mock 数据 MenuDO menuDO = buildMenuDO(MenuTypeEnum.BUTTON, "parent", 0L); menuMapper.insert(menuDO); // 准备参数 Long parentId = menuDO.getId(); // 调用,并断言异常 assertServiceException(() -> menuService.validateParentMenu(parentId, null), MENU_PARENT_NOT_DIR_OR_MENU); } @Test public void testValidateMenu_success() { // mock 父子菜单 MenuDO sonMenu = createParentAndSonMenu(); // 准备参数 Long parentId = sonMenu.getParentId(); Long otherSonMenuId = randomLongId(); String otherSonMenuName = randomString(); // 调用,无需断言 menuService.validateMenu(parentId, otherSonMenuName, otherSonMenuId); } @Test public void testValidateMenu_sonMenuNameDuplicate() { // mock 父子菜单 MenuDO sonMenu = createParentAndSonMenu(); // 准备参数 Long parentId = sonMenu.getParentId(); Long otherSonMenuId = randomLongId(); String otherSonMenuName = sonMenu.getName(); //相同名称 // 调用,并断言异常 assertServiceException(() -> menuService.validateMenu(parentId, otherSonMenuName, otherSonMenuId), MENU_NAME_DUPLICATE); } // ====================== 初始化方法 ====================== /** * 插入父子菜单,返回子菜单 * * @return 子菜单 */ private MenuDO createParentAndSonMenu() { // 构造父子菜单 MenuDO parentMenuDO = buildMenuDO(MenuTypeEnum.MENU, "parent", ID_ROOT); menuMapper.insert(parentMenuDO); // 构建子菜单 MenuDO sonMenuDO = buildMenuDO(MenuTypeEnum.MENU, "testSonName", parentMenuDO.getParentId()); menuMapper.insert(sonMenuDO); return sonMenuDO; } private MenuDO buildMenuDO(MenuTypeEnum type, String name, Long parentId) { return buildMenuDO(type, name, parentId, randomCommonStatus()); } private MenuDO buildMenuDO(MenuTypeEnum type, String name, Long parentId, Integer status) { return randomPojo(MenuDO.class, o -> o.setId(null).setName(name).setParentId(parentId) .setType(type.getType()).setStatus(status)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/permission/PermissionServiceTest.java ================================================ package co.yixiang.yshop.module.system.service.permission; import cn.hutool.core.collection.CollUtil; import cn.hutool.extra.spring.SpringUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.api.permission.dto.DeptDataPermissionRespDTO; import co.yixiang.yshop.module.system.dal.dataobject.dept.DeptDO; import co.yixiang.yshop.module.system.dal.dataobject.permission.MenuDO; import co.yixiang.yshop.module.system.dal.dataobject.permission.RoleDO; import co.yixiang.yshop.module.system.dal.dataobject.permission.RoleMenuDO; import co.yixiang.yshop.module.system.dal.dataobject.permission.UserRoleDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.module.system.dal.mysql.permission.RoleMenuMapper; import co.yixiang.yshop.module.system.dal.mysql.permission.UserRoleMapper; import co.yixiang.yshop.module.system.enums.permission.DataScopeEnum; import co.yixiang.yshop.module.system.service.dept.DeptService; import co.yixiang.yshop.module.system.service.user.AdminUserService; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.util.Collection; import java.util.List; import java.util.Set; import static cn.hutool.core.collection.ListUtil.toList; import static co.yixiang.yshop.framework.common.util.collection.SetUtils.asSet; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomLongId; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomPojo; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @Import({PermissionServiceImpl.class}) public class PermissionServiceTest extends BaseDbUnitTest { @Resource private PermissionServiceImpl permissionService; @Resource private RoleMenuMapper roleMenuMapper; @Resource private UserRoleMapper userRoleMapper; @MockBean private RoleService roleService; @MockBean private MenuService menuService; @MockBean private DeptService deptService; @MockBean private AdminUserService userService; @Test public void testHasAnyPermissions_superAdmin() { try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PermissionServiceImpl.class))) .thenReturn(permissionService); // 准备参数 Long userId = 1L; String[] roles = new String[]{"system:user:query", "system:user:create"}; // mock 用户登录的角色 userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(100L)); RoleDO role = randomPojo(RoleDO.class, o -> o.setId(100L) .setStatus(CommonStatusEnum.ENABLE.getStatus())); when(roleService.getRoleListFromCache(eq(singleton(100L)))).thenReturn(toList(role)); // mock 其它方法 when(roleService.hasAnySuperAdmin(eq(asSet(100L)))).thenReturn(true); // 调用,并断言 assertTrue(permissionService.hasAnyPermissions(userId, roles)); } } @Test public void testHasAnyPermissions_normal() { try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PermissionServiceImpl.class))) .thenReturn(permissionService); // 准备参数 Long userId = 1L; String[] roles = new String[]{"system:user:query", "system:user:create"}; // mock 用户登录的角色 userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(100L)); RoleDO role = randomPojo(RoleDO.class, o -> o.setId(100L) .setStatus(CommonStatusEnum.ENABLE.getStatus())); when(roleService.getRoleListFromCache(eq(singleton(100L)))).thenReturn(toList(role)); // mock 菜单 Long menuId = 1000L; when(menuService.getMenuIdListByPermissionFromCache( eq("system:user:create"))).thenReturn(singletonList(menuId)); roleMenuMapper.insert(randomPojo(RoleMenuDO.class).setRoleId(100L).setMenuId(1000L)); // 调用,并断言 assertTrue(permissionService.hasAnyPermissions(userId, roles)); } } @Test public void testHasAnyRoles() { try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PermissionServiceImpl.class))) .thenReturn(permissionService); // 准备参数 Long userId = 1L; String[] roles = new String[]{"yshop", "tudou"}; // mock 用户与角色的缓存 userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(100L)); RoleDO role = randomPojo(RoleDO.class, o -> o.setId(100L).setCode("tudou") .setStatus(CommonStatusEnum.ENABLE.getStatus())); when(roleService.getRoleListFromCache(eq(singleton(100L)))).thenReturn(toList(role)); // 调用,并断言 assertTrue(permissionService.hasAnyRoles(userId, roles)); } } // ========== 角色-菜单的相关方法 ========== @Test public void testAssignRoleMenu() { // 准备参数 Long roleId = 1L; Set menuIds = asSet(200L, 300L); // mock 数据 RoleMenuDO roleMenu01 = randomPojo(RoleMenuDO.class).setRoleId(1L).setMenuId(100L); roleMenuMapper.insert(roleMenu01); RoleMenuDO roleMenu02 = randomPojo(RoleMenuDO.class).setRoleId(1L).setMenuId(200L); roleMenuMapper.insert(roleMenu02); // 调用 permissionService.assignRoleMenu(roleId, menuIds); // 断言 List roleMenuList = roleMenuMapper.selectList(); assertEquals(2, roleMenuList.size()); assertEquals(1L, roleMenuList.get(0).getRoleId()); assertEquals(200L, roleMenuList.get(0).getMenuId()); assertEquals(1L, roleMenuList.get(1).getRoleId()); assertEquals(300L, roleMenuList.get(1).getMenuId()); } @Test public void testProcessRoleDeleted() { // 准备参数 Long roleId = randomLongId(); // mock 数据 UserRole UserRoleDO userRoleDO01 = randomPojo(UserRoleDO.class, o -> o.setRoleId(roleId)); // 被删除 userRoleMapper.insert(userRoleDO01); UserRoleDO userRoleDO02 = randomPojo(UserRoleDO.class); // 不被删除 userRoleMapper.insert(userRoleDO02); // mock 数据 RoleMenu RoleMenuDO roleMenuDO01 = randomPojo(RoleMenuDO.class, o -> o.setRoleId(roleId)); // 被删除 roleMenuMapper.insert(roleMenuDO01); RoleMenuDO roleMenuDO02 = randomPojo(RoleMenuDO.class); // 不被删除 roleMenuMapper.insert(roleMenuDO02); // 调用 permissionService.processRoleDeleted(roleId); // 断言数据 RoleMenuDO List dbRoleMenus = roleMenuMapper.selectList(); assertEquals(1, dbRoleMenus.size()); assertPojoEquals(dbRoleMenus.get(0), roleMenuDO02); // 断言数据 UserRoleDO List dbUserRoles = userRoleMapper.selectList(); assertEquals(1, dbUserRoles.size()); assertPojoEquals(dbUserRoles.get(0), userRoleDO02); } @Test public void testProcessMenuDeleted() { // 准备参数 Long menuId = randomLongId(); // mock 数据 RoleMenuDO roleMenuDO01 = randomPojo(RoleMenuDO.class, o -> o.setMenuId(menuId)); // 被删除 roleMenuMapper.insert(roleMenuDO01); RoleMenuDO roleMenuDO02 = randomPojo(RoleMenuDO.class); // 不被删除 roleMenuMapper.insert(roleMenuDO02); // 调用 permissionService.processMenuDeleted(menuId); // 断言数据 List dbRoleMenus = roleMenuMapper.selectList(); assertEquals(1, dbRoleMenus.size()); assertPojoEquals(dbRoleMenus.get(0), roleMenuDO02); } @Test public void testGetRoleMenuIds_superAdmin() { // 准备参数 Long roleId = 100L; // mock 方法 when(roleService.hasAnySuperAdmin(eq(singleton(100L)))).thenReturn(true); List menuList = singletonList(randomPojo(MenuDO.class).setId(1L)); when(menuService.getMenuList()).thenReturn(menuList); // 调用 Set menuIds = permissionService.getRoleMenuListByRoleId(roleId); // 断言 assertEquals(singleton(1L), menuIds); } @Test public void testGetRoleMenuIds_normal() { // 准备参数 Long roleId = 100L; // mock 数据 RoleMenuDO roleMenu01 = randomPojo(RoleMenuDO.class).setRoleId(100L).setMenuId(1L); roleMenuMapper.insert(roleMenu01); RoleMenuDO roleMenu02 = randomPojo(RoleMenuDO.class).setRoleId(100L).setMenuId(2L); roleMenuMapper.insert(roleMenu02); // 调用 Set menuIds = permissionService.getRoleMenuListByRoleId(roleId); // 断言 assertEquals(asSet(1L, 2L), menuIds); } @Test public void testGetMenuRoleIdListByMenuIdFromCache() { // 准备参数 Long menuId = 1L; // mock 数据 RoleMenuDO roleMenu01 = randomPojo(RoleMenuDO.class).setRoleId(100L).setMenuId(1L); roleMenuMapper.insert(roleMenu01); RoleMenuDO roleMenu02 = randomPojo(RoleMenuDO.class).setRoleId(200L).setMenuId(1L); roleMenuMapper.insert(roleMenu02); // 调用 Set roleIds = permissionService.getMenuRoleIdListByMenuIdFromCache(menuId); // 断言 assertEquals(asSet(100L, 200L), roleIds); } // ========== 用户-角色的相关方法 ========== @Test public void testAssignUserRole() { // 准备参数 Long userId = 1L; Set roleIds = asSet(200L, 300L); // mock 数据 UserRoleDO userRole01 = randomPojo(UserRoleDO.class).setUserId(1L).setRoleId(100L); userRoleMapper.insert(userRole01); UserRoleDO userRole02 = randomPojo(UserRoleDO.class).setUserId(1L).setRoleId(200L); userRoleMapper.insert(userRole02); // 调用 permissionService.assignUserRole(userId, roleIds); // 断言 List userRoleDOList = userRoleMapper.selectList(); assertEquals(2, userRoleDOList.size()); assertEquals(1L, userRoleDOList.get(0).getUserId()); assertEquals(200L, userRoleDOList.get(0).getRoleId()); assertEquals(1L, userRoleDOList.get(1).getUserId()); assertEquals(300L, userRoleDOList.get(1).getRoleId()); } @Test public void testProcessUserDeleted() { // 准备参数 Long userId = randomLongId(); // mock 数据 UserRoleDO userRoleDO01 = randomPojo(UserRoleDO.class, o -> o.setUserId(userId)); // 被删除 userRoleMapper.insert(userRoleDO01); UserRoleDO userRoleDO02 = randomPojo(UserRoleDO.class); // 不被删除 userRoleMapper.insert(userRoleDO02); // 调用 permissionService.processUserDeleted(userId); // 断言数据 List dbUserRoles = userRoleMapper.selectList(); assertEquals(1, dbUserRoles.size()); assertPojoEquals(dbUserRoles.get(0), userRoleDO02); } @Test public void testGetUserRoleIdListByUserId() { // 准备参数 Long userId = 1L; // mock 数据 UserRoleDO userRoleDO01 = randomPojo(UserRoleDO.class, o -> o.setUserId(1L).setRoleId(10L)); userRoleMapper.insert(userRoleDO01); UserRoleDO roleMenuDO02 = randomPojo(UserRoleDO.class, o -> o.setUserId(1L).setRoleId(20L)); userRoleMapper.insert(roleMenuDO02); // 调用 Set result = permissionService.getUserRoleIdListByUserId(userId); // 断言 assertEquals(asSet(10L, 20L), result); } @Test public void testGetUserRoleIdListByUserIdFromCache() { // 准备参数 Long userId = 1L; // mock 数据 UserRoleDO userRoleDO01 = randomPojo(UserRoleDO.class, o -> o.setUserId(1L).setRoleId(10L)); userRoleMapper.insert(userRoleDO01); UserRoleDO roleMenuDO02 = randomPojo(UserRoleDO.class, o -> o.setUserId(1L).setRoleId(20L)); userRoleMapper.insert(roleMenuDO02); // 调用 Set result = permissionService.getUserRoleIdListByUserIdFromCache(userId); // 断言 assertEquals(asSet(10L, 20L), result); } @Test public void testGetUserRoleIdsFromCache() { // 准备参数 Long userId = 1L; // mock 数据 UserRoleDO userRoleDO01 = randomPojo(UserRoleDO.class, o -> o.setUserId(1L).setRoleId(10L)); userRoleMapper.insert(userRoleDO01); UserRoleDO roleMenuDO02 = randomPojo(UserRoleDO.class, o -> o.setUserId(1L).setRoleId(20L)); userRoleMapper.insert(roleMenuDO02); // 调用 Set result = permissionService.getUserRoleIdListByUserIdFromCache(userId); // 断言 assertEquals(asSet(10L, 20L), result); } @Test public void testGetUserRoleIdListByRoleId() { // 准备参数 Collection roleIds = asSet(10L, 20L); // mock 数据 UserRoleDO userRoleDO01 = randomPojo(UserRoleDO.class, o -> o.setUserId(1L).setRoleId(10L)); userRoleMapper.insert(userRoleDO01); UserRoleDO roleMenuDO02 = randomPojo(UserRoleDO.class, o -> o.setUserId(2L).setRoleId(20L)); userRoleMapper.insert(roleMenuDO02); // 调用 Set result = permissionService.getUserRoleIdListByRoleId(roleIds); // 断言 assertEquals(asSet(1L, 2L), result); } @Test public void testGetEnableUserRoleListByUserIdFromCache() { try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PermissionServiceImpl.class))) .thenReturn(permissionService); // 准备参数 Long userId = 1L; // mock 用户登录的角色 userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(100L)); userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(200L)); RoleDO role01 = randomPojo(RoleDO.class, o -> o.setId(100L) .setStatus(CommonStatusEnum.ENABLE.getStatus())); RoleDO role02 = randomPojo(RoleDO.class, o -> o.setId(200L) .setStatus(CommonStatusEnum.DISABLE.getStatus())); when(roleService.getRoleListFromCache(eq(asSet(100L, 200L)))) .thenReturn(toList(role01, role02)); // 调用 List result = permissionService.getEnableUserRoleListByUserIdFromCache(userId); // 断言 assertEquals(1, result.size()); assertPojoEquals(role01, result.get(0)); } } // ========== 用户-部门的相关方法 ========== @Test public void testAssignRoleDataScope() { // 准备参数 Long roleId = 1L; Integer dataScope = 2; Set dataScopeDeptIds = asSet(10L, 20L); // 调用 permissionService.assignRoleDataScope(roleId, dataScope, dataScopeDeptIds); // 断言 verify(roleService).updateRoleDataScope(eq(roleId), eq(dataScope), eq(dataScopeDeptIds)); } @Test public void testGetDeptDataPermission_All() { try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PermissionServiceImpl.class))) .thenReturn(permissionService); // 准备参数 Long userId = 1L; // mock 用户的角色编号 userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(2L)); // mock 获得用户的角色 RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.ALL.getScope()) .setStatus(CommonStatusEnum.ENABLE.getStatus())); when(roleService.getRoleListFromCache(eq(singleton(2L)))).thenReturn(toList(roleDO)); // 调用 DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(userId); // 断言 assertTrue(result.getAll()); assertFalse(result.getSelf()); assertTrue(CollUtil.isEmpty(result.getDeptIds())); } } @Test public void testGetDeptDataPermission_DeptCustom() { try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PermissionServiceImpl.class))) .thenReturn(permissionService); // 准备参数 Long userId = 1L; // mock 用户的角色编号 userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(2L)); // mock 获得用户的角色 RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.DEPT_CUSTOM.getScope()) .setStatus(CommonStatusEnum.ENABLE.getStatus())); when(roleService.getRoleListFromCache(eq(singleton(2L)))).thenReturn(toList(roleDO)); // mock 部门的返回 when(userService.getUser(eq(1L))).thenReturn(new AdminUserDO().setDeptId(3L), null, null); // 最后返回 null 的目的,看看会不会重复调用 // 调用 DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(userId); // 断言 assertFalse(result.getAll()); assertFalse(result.getSelf()); assertEquals(roleDO.getDataScopeDeptIds().size() + 1, result.getDeptIds().size()); assertTrue(CollUtil.containsAll(result.getDeptIds(), roleDO.getDataScopeDeptIds())); assertTrue(CollUtil.contains(result.getDeptIds(), 3L)); } } @Test public void testGetDeptDataPermission_DeptOnly() { try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PermissionServiceImpl.class))) .thenReturn(permissionService); // 准备参数 Long userId = 1L; // mock 用户的角色编号 userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(2L)); // mock 获得用户的角色 RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.DEPT_ONLY.getScope()) .setStatus(CommonStatusEnum.ENABLE.getStatus())); when(roleService.getRoleListFromCache(eq(singleton(2L)))).thenReturn(toList(roleDO)); // mock 部门的返回 when(userService.getUser(eq(1L))).thenReturn(new AdminUserDO().setDeptId(3L), null, null); // 最后返回 null 的目的,看看会不会重复调用 // 调用 DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(userId); // 断言 assertFalse(result.getAll()); assertFalse(result.getSelf()); assertEquals(1, result.getDeptIds().size()); assertTrue(CollUtil.contains(result.getDeptIds(), 3L)); } } @Test public void testGetDeptDataPermission_DeptAndChild() { try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PermissionServiceImpl.class))) .thenReturn(permissionService); // 准备参数 Long userId = 1L; // mock 用户的角色编号 userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(2L)); // mock 获得用户的角色 RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.DEPT_AND_CHILD.getScope()) .setStatus(CommonStatusEnum.ENABLE.getStatus())); when(roleService.getRoleListFromCache(eq(singleton(2L)))).thenReturn(toList(roleDO)); // mock 部门的返回 when(userService.getUser(eq(1L))).thenReturn(new AdminUserDO().setDeptId(3L), null, null); // 最后返回 null 的目的,看看会不会重复调用 // mock 方法(部门) DeptDO deptDO = randomPojo(DeptDO.class); when(deptService.getChildDeptIdListFromCache(eq(3L))).thenReturn(singleton(deptDO.getId())); // 调用 DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(userId); // 断言 assertFalse(result.getAll()); assertFalse(result.getSelf()); assertEquals(2, result.getDeptIds().size()); assertTrue(CollUtil.contains(result.getDeptIds(), deptDO.getId())); assertTrue(CollUtil.contains(result.getDeptIds(), 3L)); } } @Test public void testGetDeptDataPermission_Self() { try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PermissionServiceImpl.class))) .thenReturn(permissionService); // 准备参数 Long userId = 1L; // mock 用户的角色编号 userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(2L)); // mock 获得用户的角色 RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.SELF.getScope()) .setStatus(CommonStatusEnum.ENABLE.getStatus())); when(roleService.getRoleListFromCache(eq(singleton(2L)))).thenReturn(toList(roleDO)); // 调用 DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(userId); // 断言 assertFalse(result.getAll()); assertTrue(result.getSelf()); assertTrue(CollUtil.isEmpty(result.getDeptIds())); } } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/permission/RoleServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.permission; import cn.hutool.extra.spring.SpringUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.controller.admin.permission.vo.role.RolePageReqVO; import co.yixiang.yshop.module.system.controller.admin.permission.vo.role.RoleSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.permission.RoleDO; import co.yixiang.yshop.module.system.dal.mysql.permission.RoleMapper; import co.yixiang.yshop.module.system.enums.permission.DataScopeEnum; import co.yixiang.yshop.module.system.enums.permission.RoleTypeEnum; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.util.Collection; import java.util.List; import java.util.Set; import static cn.hutool.core.util.RandomUtil.randomEle; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildTime; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.verify; @Import(RoleServiceImpl.class) public class RoleServiceImplTest extends BaseDbUnitTest { @Resource private RoleServiceImpl roleService; @Resource private RoleMapper roleMapper; @MockBean private PermissionService permissionService; @Test public void testCreateRole() { // 准备参数 RoleSaveReqVO reqVO = randomPojo(RoleSaveReqVO.class) .setId(null); // 防止 id 被赋值 // 调用 Long roleId = roleService.createRole(reqVO, null); // 断言 RoleDO roleDO = roleMapper.selectById(roleId); assertPojoEquals(reqVO, roleDO, "id"); assertEquals(RoleTypeEnum.CUSTOM.getType(), roleDO.getType()); assertEquals(CommonStatusEnum.ENABLE.getStatus(), roleDO.getStatus()); assertEquals(DataScopeEnum.ALL.getScope(), roleDO.getDataScope()); } @Test public void testUpdateRole() { // mock 数据 RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setType(RoleTypeEnum.CUSTOM.getType())); roleMapper.insert(roleDO); // 准备参数 Long id = roleDO.getId(); RoleSaveReqVO reqVO = randomPojo(RoleSaveReqVO.class, o -> o.setId(id)); // 调用 roleService.updateRole(reqVO); // 断言 RoleDO newRoleDO = roleMapper.selectById(id); assertPojoEquals(reqVO, newRoleDO); } @Test public void testUpdateRoleDataScope() { // mock 数据 RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setType(RoleTypeEnum.CUSTOM.getType())); roleMapper.insert(roleDO); // 准备参数 Long id = roleDO.getId(); Integer dataScope = randomEle(DataScopeEnum.values()).getScope(); Set dataScopeRoleIds = randomSet(Long.class); // 调用 roleService.updateRoleDataScope(id, dataScope, dataScopeRoleIds); // 断言 RoleDO dbRoleDO = roleMapper.selectById(id); assertEquals(dataScope, dbRoleDO.getDataScope()); assertEquals(dataScopeRoleIds, dbRoleDO.getDataScopeDeptIds()); } @Test public void testDeleteRole() { // mock 数据 RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setType(RoleTypeEnum.CUSTOM.getType())); roleMapper.insert(roleDO); // 参数准备 Long id = roleDO.getId(); // 调用 roleService.deleteRole(id); // 断言 assertNull(roleMapper.selectById(id)); // verify 删除相关数据 verify(permissionService).processRoleDeleted(id); } @Test public void testValidateRoleDuplicate_success() { // 调用,不会抛异常 roleService.validateRoleDuplicate(randomString(), randomString(), null); } @Test public void testValidateRoleDuplicate_nameDuplicate() { // mock 数据 RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setName("role_name")); roleMapper.insert(roleDO); // 准备参数 String name = "role_name"; // 调用,并断言异常 assertServiceException(() -> roleService.validateRoleDuplicate(name, randomString(), null), ROLE_NAME_DUPLICATE, name); } @Test public void testValidateRoleDuplicate_codeDuplicate() { // mock 数据 RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setCode("code")); roleMapper.insert(roleDO); // 准备参数 String code = "code"; // 调用,并断言异常 assertServiceException(() -> roleService.validateRoleDuplicate(randomString(), code, null), ROLE_CODE_DUPLICATE, code); } @Test public void testValidateUpdateRole_success() { RoleDO roleDO = randomPojo(RoleDO.class); roleMapper.insert(roleDO); // 准备参数 Long id = roleDO.getId(); // 调用,无异常 roleService.validateRoleForUpdate(id); } @Test public void testValidateUpdateRole_roleIdNotExist() { assertServiceException(() -> roleService.validateRoleForUpdate(randomLongId()), ROLE_NOT_EXISTS); } @Test public void testValidateUpdateRole_systemRoleCanNotBeUpdate() { RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setType(RoleTypeEnum.SYSTEM.getType())); roleMapper.insert(roleDO); // 准备参数 Long id = roleDO.getId(); assertServiceException(() -> roleService.validateRoleForUpdate(id), ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE); } @Test public void testGetRole() { // mock 数据 RoleDO roleDO = randomPojo(RoleDO.class); roleMapper.insert(roleDO); // 参数准备 Long id = roleDO.getId(); // 调用 RoleDO dbRoleDO = roleService.getRole(id); // 断言 assertPojoEquals(roleDO, dbRoleDO); } @Test public void testGetRoleFromCache() { // mock 数据(缓存) RoleDO roleDO = randomPojo(RoleDO.class); roleMapper.insert(roleDO); // 参数准备 Long id = roleDO.getId(); // 调用 RoleDO dbRoleDO = roleService.getRoleFromCache(id); // 断言 assertPojoEquals(roleDO, dbRoleDO); } @Test public void testGetRoleListByStatus() { // mock 数据 RoleDO dbRole01 = randomPojo(RoleDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); roleMapper.insert(dbRole01); RoleDO dbRole02 = randomPojo(RoleDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); roleMapper.insert(dbRole02); // 调用 List list = roleService.getRoleListByStatus( singleton(CommonStatusEnum.ENABLE.getStatus())); // 断言 assertEquals(1, list.size()); assertPojoEquals(dbRole01, list.get(0)); } @Test public void testGetRoleList() { // mock 数据 RoleDO dbRole01 = randomPojo(RoleDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); roleMapper.insert(dbRole01); RoleDO dbRole02 = randomPojo(RoleDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); roleMapper.insert(dbRole02); // 调用 List list = roleService.getRoleList(); // 断言 assertEquals(2, list.size()); assertPojoEquals(dbRole01, list.get(0)); assertPojoEquals(dbRole02, list.get(1)); } @Test public void testGetRoleList_ids() { // mock 数据 RoleDO dbRole01 = randomPojo(RoleDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); roleMapper.insert(dbRole01); RoleDO dbRole02 = randomPojo(RoleDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); roleMapper.insert(dbRole02); // 准备参数 Collection ids = singleton(dbRole01.getId()); // 调用 List list = roleService.getRoleList(ids); // 断言 assertEquals(1, list.size()); assertPojoEquals(dbRole01, list.get(0)); } @Test public void testGetRoleListFromCache() { try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(RoleServiceImpl.class))) .thenReturn(roleService); // mock 数据 RoleDO dbRole = randomPojo(RoleDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); roleMapper.insert(dbRole); // 测试 id 不匹配 roleMapper.insert(cloneIgnoreId(dbRole, o -> {})); // 准备参数 Collection ids = singleton(dbRole.getId()); // 调用 List list = roleService.getRoleListFromCache(ids); // 断言 assertEquals(1, list.size()); assertPojoEquals(dbRole, list.get(0)); } } @Test public void testGetRolePage() { // mock 数据 RoleDO dbRole = randomPojo(RoleDO.class, o -> { // 等会查询到 o.setName("土豆"); o.setCode("tudou"); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setCreateTime(buildTime(2022, 2, 8)); }); roleMapper.insert(dbRole); // 测试 name 不匹配 roleMapper.insert(cloneIgnoreId(dbRole, o -> o.setName("红薯"))); // 测试 code 不匹配 roleMapper.insert(cloneIgnoreId(dbRole, o -> o.setCode("hong"))); // 测试 createTime 不匹配 roleMapper.insert(cloneIgnoreId(dbRole, o -> o.setCreateTime(buildTime(2022, 2, 16)))); // 准备参数 RolePageReqVO reqVO = new RolePageReqVO(); reqVO.setName("土豆"); reqVO.setCode("tu"); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); reqVO.setCreateTime(buildBetweenTime(2022, 2, 1, 2022, 2, 12)); // 调用 PageResult pageResult = roleService.getRolePage(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbRole, pageResult.getList().get(0)); } @Test public void testHasAnySuperAdmin_true() { try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(RoleServiceImpl.class))) .thenReturn(roleService); // mock 数据 RoleDO dbRole = randomPojo(RoleDO.class).setCode("super_admin"); roleMapper.insert(dbRole); // 准备参数 Long id = dbRole.getId(); // 调用,并调用 assertTrue(roleService.hasAnySuperAdmin(singletonList(id))); } } @Test public void testHasAnySuperAdmin_false() { try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(RoleServiceImpl.class))) .thenReturn(roleService); // mock 数据 RoleDO dbRole = randomPojo(RoleDO.class).setCode("tenant_admin"); roleMapper.insert(dbRole); // 准备参数 Long id = dbRole.getId(); // 调用,并调用 assertFalse(roleService.hasAnySuperAdmin(singletonList(id))); } } @Test public void testValidateRoleList_success() { // mock 数据 RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); roleMapper.insert(roleDO); // 准备参数 List ids = singletonList(roleDO.getId()); // 调用,无需断言 roleService.validateRoleList(ids); } @Test public void testValidateRoleList_notFound() { // 准备参数 List ids = singletonList(randomLongId()); // 调用, 并断言异常 assertServiceException(() -> roleService.validateRoleList(ids), ROLE_NOT_EXISTS); } @Test public void testValidateRoleList_notEnable() { // mock 数据 RoleDO RoleDO = randomPojo(RoleDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); roleMapper.insert(RoleDO); // 准备参数 List ids = singletonList(RoleDO.getId()); // 调用, 并断言异常 assertServiceException(() -> roleService.validateRoleList(ids), ROLE_IS_DISABLE, RoleDO.getName()); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/sms/SmsChannelServiceTest.java ================================================ package co.yixiang.yshop.module.system.service.sms; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.object.BeanUtils; import co.yixiang.yshop.module.system.framework.sms.core.client.SmsClient; import co.yixiang.yshop.module.system.framework.sms.core.client.SmsClientFactory; import co.yixiang.yshop.module.system.framework.sms.core.property.SmsChannelProperties; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; import co.yixiang.yshop.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsChannelDO; import co.yixiang.yshop.module.system.dal.mysql.sms.SmsChannelMapper; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.util.List; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildTime; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @Import(SmsChannelServiceImpl.class) public class SmsChannelServiceTest extends BaseDbUnitTest { @Resource private SmsChannelServiceImpl smsChannelService; @Resource private SmsChannelMapper smsChannelMapper; @MockBean private SmsClientFactory smsClientFactory; @MockBean private SmsTemplateService smsTemplateService; @Test public void testCreateSmsChannel_success() { // 准备参数 SmsChannelSaveReqVO reqVO = randomPojo(SmsChannelSaveReqVO.class, o -> o.setStatus(randomCommonStatus())) .setId(null); // 防止 id 被赋值 // 调用 Long smsChannelId = smsChannelService.createSmsChannel(reqVO); // 断言 assertNotNull(smsChannelId); // 校验记录的属性是否正确 SmsChannelDO smsChannel = smsChannelMapper.selectById(smsChannelId); assertPojoEquals(reqVO, smsChannel, "id"); // 断言 cache assertNull(smsChannelService.getIdClientCache().getIfPresent(smsChannel.getId())); assertNull(smsChannelService.getCodeClientCache().getIfPresent(smsChannel.getCode())); } @Test public void testUpdateSmsChannel_success() { // mock 数据 SmsChannelDO dbSmsChannel = randomPojo(SmsChannelDO.class); smsChannelMapper.insert(dbSmsChannel);// @Sql: 先插入出一条存在的数据 // 准备参数 SmsChannelSaveReqVO reqVO = randomPojo(SmsChannelSaveReqVO.class, o -> { o.setId(dbSmsChannel.getId()); // 设置更新的 ID o.setStatus(randomCommonStatus()); o.setCallbackUrl(randomString()); }); // 调用 smsChannelService.updateSmsChannel(reqVO); // 校验是否更新正确 SmsChannelDO smsChannel = smsChannelMapper.selectById(reqVO.getId()); // 获取最新的 assertPojoEquals(reqVO, smsChannel); // 断言 cache assertNull(smsChannelService.getIdClientCache().getIfPresent(smsChannel.getId())); assertNull(smsChannelService.getCodeClientCache().getIfPresent(smsChannel.getCode())); } @Test public void testUpdateSmsChannel_notExists() { // 准备参数 SmsChannelSaveReqVO reqVO = randomPojo(SmsChannelSaveReqVO.class); // 调用, 并断言异常 assertServiceException(() -> smsChannelService.updateSmsChannel(reqVO), SMS_CHANNEL_NOT_EXISTS); } @Test public void testDeleteSmsChannel_success() { // mock 数据 SmsChannelDO dbSmsChannel = randomPojo(SmsChannelDO.class); smsChannelMapper.insert(dbSmsChannel);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbSmsChannel.getId(); // 调用 smsChannelService.deleteSmsChannel(id); // 校验数据不存在了 assertNull(smsChannelMapper.selectById(id)); // 断言 cache assertNull(smsChannelService.getIdClientCache().getIfPresent(dbSmsChannel.getId())); assertNull(smsChannelService.getCodeClientCache().getIfPresent(dbSmsChannel.getCode())); } @Test public void testDeleteSmsChannel_notExists() { // 准备参数 Long id = randomLongId(); // 调用, 并断言异常 assertServiceException(() -> smsChannelService.deleteSmsChannel(id), SMS_CHANNEL_NOT_EXISTS); } @Test public void testDeleteSmsChannel_hasChildren() { // mock 数据 SmsChannelDO dbSmsChannel = randomPojo(SmsChannelDO.class); smsChannelMapper.insert(dbSmsChannel);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbSmsChannel.getId(); // mock 方法 when(smsTemplateService.getSmsTemplateCountByChannelId(eq(id))).thenReturn(10L); // 调用, 并断言异常 assertServiceException(() -> smsChannelService.deleteSmsChannel(id), SMS_CHANNEL_HAS_CHILDREN); } @Test public void testGetSmsChannel() { // mock 数据 SmsChannelDO dbSmsChannel = randomPojo(SmsChannelDO.class); smsChannelMapper.insert(dbSmsChannel); // @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbSmsChannel.getId(); // 调用,并断言 assertPojoEquals(dbSmsChannel, smsChannelService.getSmsChannel(id)); } @Test public void testGetSmsChannelList() { // mock 数据 SmsChannelDO dbSmsChannel01 = randomPojo(SmsChannelDO.class); smsChannelMapper.insert(dbSmsChannel01); SmsChannelDO dbSmsChannel02 = randomPojo(SmsChannelDO.class); smsChannelMapper.insert(dbSmsChannel02); // 准备参数 // 调用 List list = smsChannelService.getSmsChannelList(); // 断言 assertEquals(2, list.size()); assertPojoEquals(dbSmsChannel01, list.get(0)); assertPojoEquals(dbSmsChannel02, list.get(1)); } @Test public void testGetSmsChannelPage() { // mock 数据 SmsChannelDO dbSmsChannel = randomPojo(SmsChannelDO.class, o -> { // 等会查询到 o.setSignature("yshop"); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setCreateTime(buildTime(2020, 12, 12)); }); smsChannelMapper.insert(dbSmsChannel); // 测试 signature 不匹配 smsChannelMapper.insert(cloneIgnoreId(dbSmsChannel, o -> o.setSignature("源码"))); // 测试 status 不匹配 smsChannelMapper.insert(cloneIgnoreId(dbSmsChannel, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); // 测试 createTime 不匹配 smsChannelMapper.insert(cloneIgnoreId(dbSmsChannel, o -> o.setCreateTime(buildTime(2020, 11, 11)))); // 准备参数 SmsChannelPageReqVO reqVO = new SmsChannelPageReqVO(); reqVO.setSignature("yshop"); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); reqVO.setCreateTime(buildBetweenTime(2020, 12, 1, 2020, 12, 24)); // 调用 PageResult pageResult = smsChannelService.getSmsChannelPage(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbSmsChannel, pageResult.getList().get(0)); } @Test public void testGetSmsClient_id() { // mock 数据 SmsChannelDO channel = randomPojo(SmsChannelDO.class); smsChannelMapper.insert(channel); // mock 参数 Long id = channel.getId(); // mock 方法 SmsClient mockClient = mock(SmsClient.class); when(smsClientFactory.getSmsClient(eq(id))).thenReturn(mockClient); // 调用 SmsClient client = smsChannelService.getSmsClient(id); // 断言 assertSame(client, mockClient); verify(smsClientFactory).createOrUpdateSmsClient(argThat(arg -> { SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class); return properties.equals(arg); })); } @Test public void testGetSmsClient_code() { // mock 数据 SmsChannelDO channel = randomPojo(SmsChannelDO.class); smsChannelMapper.insert(channel); // mock 参数 String code = channel.getCode(); // mock 方法 SmsClient mockClient = mock(SmsClient.class); when(smsClientFactory.getSmsClient(eq(code))).thenReturn(mockClient); // 调用 SmsClient client = smsChannelService.getSmsClient(code); // 断言 assertSame(client, mockClient); verify(smsClientFactory).createOrUpdateSmsClient(argThat(arg -> { SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class); return properties.equals(arg); })); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/sms/SmsCodeServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.sms; import cn.hutool.core.map.MapUtil; import co.yixiang.yshop.framework.mybatis.core.enums.SqlConstants; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeSendReqDTO; import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeUseReqDTO; import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeValidateReqDTO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsCodeDO; import co.yixiang.yshop.module.system.dal.mysql.sms.SmsCodeMapper; import co.yixiang.yshop.module.system.enums.sms.SmsSceneEnum; import co.yixiang.yshop.module.system.framework.sms.config.SmsCodeProperties; import com.baomidou.mybatisplus.annotation.DbType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.time.Duration; import java.time.LocalDateTime; import static cn.hutool.core.util.RandomUtil.randomEle; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomPojo; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @Import(SmsCodeServiceImpl.class) public class SmsCodeServiceImplTest extends BaseDbUnitTest { @Resource private SmsCodeServiceImpl smsCodeService; @Resource private SmsCodeMapper smsCodeMapper; @MockBean private SmsCodeProperties smsCodeProperties; @MockBean private SmsSendService smsSendService; @BeforeEach public void setUp() { when(smsCodeProperties.getExpireTimes()).thenReturn(Duration.ofMinutes(5)); when(smsCodeProperties.getSendFrequency()).thenReturn(Duration.ofMinutes(1)); when(smsCodeProperties.getSendMaximumQuantityPerDay()).thenReturn(10); when(smsCodeProperties.getBeginCode()).thenReturn(9999); when(smsCodeProperties.getEndCode()).thenReturn(9999); } @Test public void sendSmsCode_success() { // 准备参数 SmsCodeSendReqDTO reqDTO = randomPojo(SmsCodeSendReqDTO.class, o -> { o.setMobile("15601691300"); o.setScene(SmsSceneEnum.MEMBER_LOGIN.getScene()); }); // mock 方法 SqlConstants.init(DbType.MYSQL); // 调用 smsCodeService.sendSmsCode(reqDTO); // 断言 code 验证码 SmsCodeDO smsCodeDO = smsCodeMapper.selectOne(null); assertPojoEquals(reqDTO, smsCodeDO); assertEquals("9999", smsCodeDO.getCode()); assertEquals(1, smsCodeDO.getTodayIndex()); assertFalse(smsCodeDO.getUsed()); // 断言调用 verify(smsSendService).sendSingleSms(eq(reqDTO.getMobile()), isNull(), isNull(), eq("user-sms-login"), eq(MapUtil.of("code", "9999"))); } @Test public void sendSmsCode_tooFast() { // mock 数据 SmsCodeDO smsCodeDO = randomPojo(SmsCodeDO.class, o -> o.setMobile("15601691300").setTodayIndex(1)); smsCodeMapper.insert(smsCodeDO); // 准备参数 SmsCodeSendReqDTO reqDTO = randomPojo(SmsCodeSendReqDTO.class, o -> { o.setMobile("15601691300"); o.setScene(SmsSceneEnum.MEMBER_LOGIN.getScene()); }); // mock 方法 SqlConstants.init(DbType.MYSQL); // 调用,并断言异常 assertServiceException(() -> smsCodeService.sendSmsCode(reqDTO), SMS_CODE_SEND_TOO_FAST); } @Test public void sendSmsCode_exceedDay() { // mock 数据 SmsCodeDO smsCodeDO = randomPojo(SmsCodeDO.class, o -> o.setMobile("15601691300").setTodayIndex(10).setCreateTime(LocalDateTime.now())); smsCodeMapper.insert(smsCodeDO); // 准备参数 SmsCodeSendReqDTO reqDTO = randomPojo(SmsCodeSendReqDTO.class, o -> { o.setMobile("15601691300"); o.setScene(SmsSceneEnum.MEMBER_LOGIN.getScene()); }); // mock 方法 SqlConstants.init(DbType.MYSQL); when(smsCodeProperties.getSendFrequency()).thenReturn(Duration.ofMillis(0)); // 调用,并断言异常 assertServiceException(() -> smsCodeService.sendSmsCode(reqDTO), SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY); } @Test public void testUseSmsCode_success() { // 准备参数 SmsCodeUseReqDTO reqDTO = randomPojo(SmsCodeUseReqDTO.class, o -> { o.setMobile("15601691300"); o.setScene(randomEle(SmsSceneEnum.values()).getScene()); }); // mock 数据 SqlConstants.init(DbType.MYSQL); smsCodeMapper.insert(randomPojo(SmsCodeDO.class, o -> { o.setMobile(reqDTO.getMobile()).setScene(reqDTO.getScene()) .setCode(reqDTO.getCode()).setUsed(false); })); // 调用 smsCodeService.useSmsCode(reqDTO); // 断言 SmsCodeDO smsCodeDO = smsCodeMapper.selectOne(null); assertTrue(smsCodeDO.getUsed()); assertNotNull(smsCodeDO.getUsedTime()); assertEquals(reqDTO.getUsedIp(), smsCodeDO.getUsedIp()); } @Test public void validateSmsCode_success() { // 准备参数 SmsCodeValidateReqDTO reqDTO = randomPojo(SmsCodeValidateReqDTO.class, o -> { o.setMobile("15601691300"); o.setScene(randomEle(SmsSceneEnum.values()).getScene()); }); // mock 数据 SqlConstants.init(DbType.MYSQL); smsCodeMapper.insert(randomPojo(SmsCodeDO.class, o -> o.setMobile(reqDTO.getMobile()) .setScene(reqDTO.getScene()).setCode(reqDTO.getCode()).setUsed(false))); // 调用 smsCodeService.validateSmsCode(reqDTO); } @Test public void validateSmsCode_notFound() { // 准备参数 SmsCodeValidateReqDTO reqDTO = randomPojo(SmsCodeValidateReqDTO.class, o -> { o.setMobile("15601691300"); o.setScene(randomEle(SmsSceneEnum.values()).getScene()); }); // mock 数据 SqlConstants.init(DbType.MYSQL); // 调用,并断言异常 assertServiceException(() -> smsCodeService.validateSmsCode(reqDTO), SMS_CODE_NOT_FOUND); } @Test public void validateSmsCode_expired() { // 准备参数 SmsCodeValidateReqDTO reqDTO = randomPojo(SmsCodeValidateReqDTO.class, o -> { o.setMobile("15601691300"); o.setScene(randomEle(SmsSceneEnum.values()).getScene()); }); // mock 数据 SqlConstants.init(DbType.MYSQL); smsCodeMapper.insert(randomPojo(SmsCodeDO.class, o -> o.setMobile(reqDTO.getMobile()) .setScene(reqDTO.getScene()).setCode(reqDTO.getCode()).setUsed(false) .setCreateTime(LocalDateTime.now().minusMinutes(6)))); // 调用,并断言异常 assertServiceException(() -> smsCodeService.validateSmsCode(reqDTO), SMS_CODE_EXPIRED); } @Test public void validateSmsCode_used() { // 准备参数 SmsCodeValidateReqDTO reqDTO = randomPojo(SmsCodeValidateReqDTO.class, o -> { o.setMobile("15601691300"); o.setScene(randomEle(SmsSceneEnum.values()).getScene()); }); // mock 数据 SqlConstants.init(DbType.MYSQL); smsCodeMapper.insert(randomPojo(SmsCodeDO.class, o -> o.setMobile(reqDTO.getMobile()) .setScene(reqDTO.getScene()).setCode(reqDTO.getCode()).setUsed(true) .setCreateTime(LocalDateTime.now()))); // 调用,并断言异常 assertServiceException(() -> smsCodeService.validateSmsCode(reqDTO), SMS_CODE_USED); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/sms/SmsLogServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.sms; import cn.hutool.core.map.MapUtil; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.collection.ArrayUtils; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsLogDO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsTemplateDO; import co.yixiang.yshop.module.system.dal.mysql.sms.SmsLogMapper; import co.yixiang.yshop.module.system.enums.sms.SmsReceiveStatusEnum; import co.yixiang.yshop.module.system.enums.sms.SmsSendStatusEnum; import co.yixiang.yshop.module.system.enums.sms.SmsTemplateTypeEnum; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.time.LocalDateTime; import java.util.Map; import java.util.function.Consumer; import static cn.hutool.core.util.RandomUtil.randomBoolean; import static cn.hutool.core.util.RandomUtil.randomEle; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildTime; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @Import(SmsLogServiceImpl.class) public class SmsLogServiceImplTest extends BaseDbUnitTest { @Resource private SmsLogServiceImpl smsLogService; @Resource private SmsLogMapper smsLogMapper; @Test public void testGetSmsLogPage() { // mock 数据 SmsLogDO dbSmsLog = randomSmsLogDO(o -> { // 等会查询到 o.setChannelId(1L); o.setTemplateId(10L); o.setMobile("15601691300"); o.setSendStatus(SmsSendStatusEnum.INIT.getStatus()); o.setSendTime(buildTime(2020, 11, 11)); o.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus()); o.setReceiveTime(buildTime(2021, 11, 11)); }); smsLogMapper.insert(dbSmsLog); // 测试 channelId 不匹配 smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setChannelId(2L))); // 测试 templateId 不匹配 smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setTemplateId(20L))); // 测试 mobile 不匹配 smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setMobile("18818260999"))); // 测试 sendStatus 不匹配 smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setSendStatus(SmsSendStatusEnum.IGNORE.getStatus()))); // 测试 sendTime 不匹配 smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setSendTime(buildTime(2020, 12, 12)))); // 测试 receiveStatus 不匹配 smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setReceiveStatus(SmsReceiveStatusEnum.SUCCESS.getStatus()))); // 测试 receiveTime 不匹配 smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setReceiveTime(buildTime(2021, 12, 12)))); // 准备参数 SmsLogPageReqVO reqVO = new SmsLogPageReqVO(); reqVO.setChannelId(1L); reqVO.setTemplateId(10L); reqVO.setMobile("156"); reqVO.setSendStatus(SmsSendStatusEnum.INIT.getStatus()); reqVO.setSendTime(buildBetweenTime(2020, 11, 1, 2020, 11, 30)); reqVO.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus()); reqVO.setReceiveTime(buildBetweenTime(2021, 11, 1, 2021, 11, 30)); // 调用 PageResult pageResult = smsLogService.getSmsLogPage(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbSmsLog, pageResult.getList().get(0)); } @Test public void testCreateSmsLog() { // 准备参数 String mobile = randomString(); Long userId = randomLongId(); Integer userType = randomEle(UserTypeEnum.values()).getValue(); Boolean isSend = randomBoolean(); SmsTemplateDO templateDO = randomPojo(SmsTemplateDO.class, o -> o.setType(randomEle(SmsTemplateTypeEnum.values()).getType())); String templateContent = randomString(); Map templateParams = randomTemplateParams(); // mock 方法 // 调用 Long logId = smsLogService.createSmsLog(mobile, userId, userType, isSend, templateDO, templateContent, templateParams); // 断言 SmsLogDO logDO = smsLogMapper.selectById(logId); assertEquals(isSend ? SmsSendStatusEnum.INIT.getStatus() : SmsSendStatusEnum.IGNORE.getStatus(), logDO.getSendStatus()); assertEquals(mobile, logDO.getMobile()); assertEquals(userType, logDO.getUserType()); assertEquals(userId, logDO.getUserId()); assertEquals(templateDO.getId(), logDO.getTemplateId()); assertEquals(templateDO.getCode(), logDO.getTemplateCode()); assertEquals(templateDO.getType(), logDO.getTemplateType()); assertEquals(templateDO.getChannelId(), logDO.getChannelId()); assertEquals(templateDO.getChannelCode(), logDO.getChannelCode()); assertEquals(templateContent, logDO.getTemplateContent()); assertEquals(templateParams, logDO.getTemplateParams()); assertEquals(SmsReceiveStatusEnum.INIT.getStatus(), logDO.getReceiveStatus()); } @Test public void testUpdateSmsSendResult() { // mock 数据 SmsLogDO dbSmsLog = randomSmsLogDO( o -> o.setSendStatus(SmsSendStatusEnum.IGNORE.getStatus())); smsLogMapper.insert(dbSmsLog); // 准备参数 Long id = dbSmsLog.getId(); Boolean success = randomBoolean(); String apiSendCode = randomString(); String apiSendMsg = randomString(); String apiRequestId = randomString(); String apiSerialNo = randomString(); // 调用 smsLogService.updateSmsSendResult(id, success, apiSendCode, apiSendMsg, apiRequestId, apiSerialNo); // 断言 dbSmsLog = smsLogMapper.selectById(id); assertEquals(success ? SmsSendStatusEnum.SUCCESS.getStatus() : SmsSendStatusEnum.FAILURE.getStatus(), dbSmsLog.getSendStatus()); assertNotNull(dbSmsLog.getSendTime()); assertEquals(apiSendCode, dbSmsLog.getApiSendCode()); assertEquals(apiSendMsg, dbSmsLog.getApiSendMsg()); assertEquals(apiRequestId, dbSmsLog.getApiRequestId()); assertEquals(apiSerialNo, dbSmsLog.getApiSerialNo()); } @Test public void testUpdateSmsReceiveResult() { // mock 数据 SmsLogDO dbSmsLog = randomSmsLogDO( o -> o.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus())); smsLogMapper.insert(dbSmsLog); // 准备参数 Long id = dbSmsLog.getId(); Boolean success = randomBoolean(); LocalDateTime receiveTime = randomLocalDateTime(); String apiReceiveCode = randomString(); String apiReceiveMsg = randomString(); // 调用 smsLogService.updateSmsReceiveResult(id, success, receiveTime, apiReceiveCode, apiReceiveMsg); // 断言 dbSmsLog = smsLogMapper.selectById(id); assertEquals(success ? SmsReceiveStatusEnum.SUCCESS.getStatus() : SmsReceiveStatusEnum.FAILURE.getStatus(), dbSmsLog.getReceiveStatus()); assertEquals(receiveTime, dbSmsLog.getReceiveTime()); assertEquals(apiReceiveCode, dbSmsLog.getApiReceiveCode()); assertEquals(apiReceiveMsg, dbSmsLog.getApiReceiveMsg()); } // ========== 随机对象 ========== @SafeVarargs private static SmsLogDO randomSmsLogDO(Consumer... consumers) { Consumer consumer = (o) -> { o.setTemplateParams(randomTemplateParams()); o.setTemplateType(randomEle(SmsTemplateTypeEnum.values()).getType()); // 保证 templateType 的范围 o.setUserType(randomEle(UserTypeEnum.values()).getValue()); // 保证 userType 的范围 o.setSendStatus(randomEle(SmsSendStatusEnum.values()).getStatus()); // 保证 sendStatus 的范围 o.setReceiveStatus(randomEle(SmsReceiveStatusEnum.values()).getStatus()); // 保证 receiveStatus 的范围 }; return randomPojo(SmsLogDO.class, ArrayUtils.append(consumer, consumers)); } private static Map randomTemplateParams() { return MapUtil.builder().put(randomString(), randomString()) .put(randomString(), randomString()).build(); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/sms/SmsSendServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.sms; import cn.hutool.core.map.MapUtil; import co.yixiang.yshop.framework.common.core.KeyValue; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.module.system.framework.sms.core.client.SmsClient; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsSendRespDTO; import co.yixiang.yshop.framework.test.core.ut.BaseMockitoUnitTest; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsChannelDO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsTemplateDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.module.system.mq.message.sms.SmsSendMessage; import co.yixiang.yshop.module.system.mq.producer.sms.SmsProducer; import co.yixiang.yshop.module.system.service.member.MemberService; import co.yixiang.yshop.module.system.service.user.AdminUserService; import org.assertj.core.util.Lists; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import java.util.HashMap; import java.util.List; import java.util.Map; import static cn.hutool.core.util.RandomUtil.randomEle; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; public class SmsSendServiceImplTest extends BaseMockitoUnitTest { @InjectMocks private SmsSendServiceImpl smsSendService; @Mock private AdminUserService adminUserService; @Mock private MemberService memberService; @Mock private SmsChannelService smsChannelService; @Mock private SmsTemplateService smsTemplateService; @Mock private SmsLogService smsLogService; @Mock private SmsProducer smsProducer; @Test public void testSendSingleSmsToAdmin() { // 准备参数 Long userId = randomLongId(); String templateCode = randomString(); Map templateParams = MapUtil.builder().put("code", "1234") .put("op", "login").build(); // mock adminUserService 的方法 AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setMobile("15601691300")); when(adminUserService.getUser(eq(userId))).thenReturn(user); // mock SmsTemplateService 的方法 SmsTemplateDO template = randomPojo(SmsTemplateDO.class, o -> { o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setContent("验证码为{code}, 操作为{op}"); o.setParams(Lists.newArrayList("code", "op")); }); when(smsTemplateService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); String content = randomString(); when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams))) .thenReturn(content); // mock SmsChannelService 的方法 SmsChannelDO smsChannel = randomPojo(SmsChannelDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); when(smsChannelService.getSmsChannel(eq(template.getChannelId()))).thenReturn(smsChannel); // mock SmsLogService 的方法 Long smsLogId = randomLongId(); when(smsLogService.createSmsLog(eq(user.getMobile()), eq(userId), eq(UserTypeEnum.ADMIN.getValue()), eq(Boolean.TRUE), eq(template), eq(content), eq(templateParams))).thenReturn(smsLogId); // 调用 Long resultSmsLogId = smsSendService.sendSingleSmsToAdmin(null, userId, templateCode, templateParams); // 断言 assertEquals(smsLogId, resultSmsLogId); // 断言调用 verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(user.getMobile()), eq(template.getChannelId()), eq(template.getApiTemplateId()), eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login")))); } @Test public void testSendSingleSmsToUser() { // 准备参数 Long userId = randomLongId(); String templateCode = randomString(); Map templateParams = MapUtil.builder().put("code", "1234") .put("op", "login").build(); // mock memberService 的方法 String mobile = "15601691300"; when(memberService.getMemberUserMobile(eq(userId))).thenReturn(mobile); // mock SmsTemplateService 的方法 SmsTemplateDO template = randomPojo(SmsTemplateDO.class, o -> { o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setContent("验证码为{code}, 操作为{op}"); o.setParams(Lists.newArrayList("code", "op")); }); when(smsTemplateService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); String content = randomString(); when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams))) .thenReturn(content); // mock SmsChannelService 的方法 SmsChannelDO smsChannel = randomPojo(SmsChannelDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); when(smsChannelService.getSmsChannel(eq(template.getChannelId()))).thenReturn(smsChannel); // mock SmsLogService 的方法 Long smsLogId = randomLongId(); when(smsLogService.createSmsLog(eq(mobile), eq(userId), eq(UserTypeEnum.MEMBER.getValue()), eq(Boolean.TRUE), eq(template), eq(content), eq(templateParams))).thenReturn(smsLogId); // 调用 Long resultSmsLogId = smsSendService.sendSingleSmsToMember(null, userId, templateCode, templateParams); // 断言 assertEquals(smsLogId, resultSmsLogId); // 断言调用 verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile), eq(template.getChannelId()), eq(template.getApiTemplateId()), eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login")))); } /** * 发送成功,当短信模板开启时 */ @Test public void testSendSingleSms_successWhenSmsTemplateEnable() { // 准备参数 String mobile = randomString(); Long userId = randomLongId(); Integer userType = randomEle(UserTypeEnum.values()).getValue(); String templateCode = randomString(); Map templateParams = MapUtil.builder().put("code", "1234") .put("op", "login").build(); // mock SmsTemplateService 的方法 SmsTemplateDO template = randomPojo(SmsTemplateDO.class, o -> { o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setContent("验证码为{code}, 操作为{op}"); o.setParams(Lists.newArrayList("code", "op")); }); when(smsTemplateService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); String content = randomString(); when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams))) .thenReturn(content); // mock SmsChannelService 的方法 SmsChannelDO smsChannel = randomPojo(SmsChannelDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); when(smsChannelService.getSmsChannel(eq(template.getChannelId()))).thenReturn(smsChannel); // mock SmsLogService 的方法 Long smsLogId = randomLongId(); when(smsLogService.createSmsLog(eq(mobile), eq(userId), eq(userType), eq(Boolean.TRUE), eq(template), eq(content), eq(templateParams))).thenReturn(smsLogId); // 调用 Long resultSmsLogId = smsSendService.sendSingleSms(mobile, userId, userType, templateCode, templateParams); // 断言 assertEquals(smsLogId, resultSmsLogId); // 断言调用 verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile), eq(template.getChannelId()), eq(template.getApiTemplateId()), eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login")))); } /** * 发送成功,当短信模板关闭时 */ @Test public void testSendSingleSms_successWhenSmsTemplateDisable() { // 准备参数 String mobile = randomString(); Long userId = randomLongId(); Integer userType = randomEle(UserTypeEnum.values()).getValue(); String templateCode = randomString(); Map templateParams = MapUtil.builder().put("code", "1234") .put("op", "login").build(); // mock SmsTemplateService 的方法 SmsTemplateDO template = randomPojo(SmsTemplateDO.class, o -> { o.setStatus(CommonStatusEnum.DISABLE.getStatus()); o.setContent("验证码为{code}, 操作为{op}"); o.setParams(Lists.newArrayList("code", "op")); }); when(smsTemplateService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); String content = randomString(); when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams))) .thenReturn(content); // mock SmsChannelService 的方法 SmsChannelDO smsChannel = randomPojo(SmsChannelDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); when(smsChannelService.getSmsChannel(eq(template.getChannelId()))).thenReturn(smsChannel); // mock SmsLogService 的方法 Long smsLogId = randomLongId(); when(smsLogService.createSmsLog(eq(mobile), eq(userId), eq(userType), eq(Boolean.FALSE), eq(template), eq(content), eq(templateParams))).thenReturn(smsLogId); // 调用 Long resultSmsLogId = smsSendService.sendSingleSms(mobile, userId, userType, templateCode, templateParams); // 断言 assertEquals(smsLogId, resultSmsLogId); // 断言调用 verify(smsProducer, times(0)).sendSmsSendMessage(anyLong(), anyString(), anyLong(), any(), anyList()); } @Test public void testCheckSmsTemplateValid_notExists() { // 准备参数 String templateCode = randomString(); // mock 方法 // 调用,并断言异常 assertServiceException(() -> smsSendService.validateSmsTemplate(templateCode), SMS_SEND_TEMPLATE_NOT_EXISTS); } @Test public void testBuildTemplateParams_paramMiss() { // 准备参数 SmsTemplateDO template = randomPojo(SmsTemplateDO.class, o -> o.setParams(Lists.newArrayList("code"))); Map templateParams = new HashMap<>(); // mock 方法 // 调用,并断言异常 assertServiceException(() -> smsSendService.buildTemplateParams(template, templateParams), SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS, "code"); } @Test public void testCheckMobile_notExists() { // 准备参数 // mock 方法 // 调用,并断言异常 assertServiceException(() -> smsSendService.validateMobile(null), SMS_SEND_MOBILE_NOT_EXISTS); } @Test public void testSendBatchNotify() { // 准备参数 // mock 方法 // 调用 UnsupportedOperationException exception = Assertions.assertThrows( UnsupportedOperationException.class, () -> smsSendService.sendBatchSms(null, null, null, null, null) ); // 断言 assertEquals("暂时不支持该操作,感兴趣可以实现该功能哟!", exception.getMessage()); } @Test @SuppressWarnings("unchecked") public void testDoSendSms() throws Throwable { // 准备参数 SmsSendMessage message = randomPojo(SmsSendMessage.class); // mock SmsClientFactory 的方法 SmsClient smsClient = spy(SmsClient.class); when(smsChannelService.getSmsClient(eq(message.getChannelId()))).thenReturn(smsClient); // mock SmsClient 的方法 SmsSendRespDTO sendResult = randomPojo(SmsSendRespDTO.class); when(smsClient.sendSms(eq(message.getLogId()), eq(message.getMobile()), eq(message.getApiTemplateId()), eq(message.getTemplateParams()))).thenReturn(sendResult); // 调用 smsSendService.doSendSms(message); // 断言 verify(smsLogService).updateSmsSendResult(eq(message.getLogId()), eq(sendResult.getSuccess()), eq(sendResult.getApiCode()), eq(sendResult.getApiMsg()), eq(sendResult.getApiRequestId()), eq(sendResult.getSerialNo())); } @Test public void testReceiveSmsStatus() throws Throwable { // 准备参数 String channelCode = randomString(); String text = randomString(); // mock SmsClientFactory 的方法 SmsClient smsClient = spy(SmsClient.class); when(smsChannelService.getSmsClient(eq(channelCode))).thenReturn(smsClient); // mock SmsClient 的方法 List receiveResults = randomPojoList(SmsReceiveRespDTO.class); // 调用 smsSendService.receiveSmsStatus(channelCode, text); // 断言 receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(eq(result.getLogId()), eq(result.getSuccess()), eq(result.getReceiveTime()), eq(result.getErrorCode()), eq(result.getErrorCode()))); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/sms/SmsTemplateServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.sms; import cn.hutool.core.map.MapUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.collection.ArrayUtils; import co.yixiang.yshop.framework.common.util.object.ObjectUtils; import co.yixiang.yshop.module.system.framework.sms.core.client.SmsClient; import co.yixiang.yshop.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; import co.yixiang.yshop.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.controller.admin.sms.vo.template.SmsTemplatePageReqVO; import co.yixiang.yshop.module.system.controller.admin.sms.vo.template.SmsTemplateSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsChannelDO; import co.yixiang.yshop.module.system.dal.dataobject.sms.SmsTemplateDO; import co.yixiang.yshop.module.system.dal.mysql.sms.SmsTemplateMapper; import co.yixiang.yshop.module.system.enums.sms.SmsTemplateTypeEnum; import com.google.common.collect.Lists; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.util.List; import java.util.Map; import java.util.function.Consumer; import static cn.hutool.core.util.RandomUtil.randomEle; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildTime; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; @Import(SmsTemplateServiceImpl.class) public class SmsTemplateServiceImplTest extends BaseDbUnitTest { @Resource private SmsTemplateServiceImpl smsTemplateService; @Resource private SmsTemplateMapper smsTemplateMapper; @MockBean private SmsChannelService smsChannelService; @MockBean private SmsClient smsClient; @Test public void testFormatSmsTemplateContent() { // 准备参数 String content = "正在进行登录操作{operation},您的验证码是{code}"; Map params = MapUtil.builder("operation", "登录") .put("code", "1234").build(); // 调用 String result = smsTemplateService.formatSmsTemplateContent(content, params); // 断言 assertEquals("正在进行登录操作登录,您的验证码是1234", result); } @Test public void testParseTemplateContentParams() { // 准备参数 String content = "正在进行登录操作{operation},您的验证码是{code}"; // mock 方法 // 调用 List params = smsTemplateService.parseTemplateContentParams(content); // 断言 assertEquals(Lists.newArrayList("operation", "code"), params); } @Test @SuppressWarnings("unchecked") public void testCreateSmsTemplate_success() throws Throwable { // 准备参数 SmsTemplateSaveReqVO reqVO = randomPojo(SmsTemplateSaveReqVO.class, o -> { o.setContent("正在进行登录操作{operation},您的验证码是{code}"); o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 o.setType(randomEle(SmsTemplateTypeEnum.values()).getType()); // 保证 type 的 范围 }).setId(null); // 防止 id 被赋值 // mock Channel 的方法 SmsChannelDO channelDO = randomPojo(SmsChannelDO.class, o -> { o.setId(reqVO.getChannelId()); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 保证 status 开启,创建必须处于这个状态 }); when(smsChannelService.getSmsChannel(eq(channelDO.getId()))).thenReturn(channelDO); // mock 获得 API 短信模板成功 when(smsChannelService.getSmsClient(eq(reqVO.getChannelId()))).thenReturn(smsClient); when(smsClient.getSmsTemplate(eq(reqVO.getApiTemplateId()))).thenReturn( randomPojo(SmsTemplateRespDTO.class, o -> o.setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()))); // 调用 Long smsTemplateId = smsTemplateService.createSmsTemplate(reqVO); // 断言 assertNotNull(smsTemplateId); // 校验记录的属性是否正确 SmsTemplateDO smsTemplate = smsTemplateMapper.selectById(smsTemplateId); assertPojoEquals(reqVO, smsTemplate, "id"); assertEquals(Lists.newArrayList("operation", "code"), smsTemplate.getParams()); assertEquals(channelDO.getCode(), smsTemplate.getChannelCode()); } @Test @SuppressWarnings("unchecked") public void testUpdateSmsTemplate_success() throws Throwable { // mock 数据 SmsTemplateDO dbSmsTemplate = randomSmsTemplateDO(); smsTemplateMapper.insert(dbSmsTemplate);// @Sql: 先插入出一条存在的数据 // 准备参数 SmsTemplateSaveReqVO reqVO = randomPojo(SmsTemplateSaveReqVO.class, o -> { o.setId(dbSmsTemplate.getId()); // 设置更新的 ID o.setContent("正在进行登录操作{operation},您的验证码是{code}"); o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 o.setType(randomEle(SmsTemplateTypeEnum.values()).getType()); // 保证 type 的 范围 }); // mock 方法 SmsChannelDO channelDO = randomPojo(SmsChannelDO.class, o -> { o.setId(reqVO.getChannelId()); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 保证 status 开启,创建必须处于这个状态 }); when(smsChannelService.getSmsChannel(eq(channelDO.getId()))).thenReturn(channelDO); // mock 获得 API 短信模板成功 when(smsChannelService.getSmsClient(eq(reqVO.getChannelId()))).thenReturn(smsClient); when(smsClient.getSmsTemplate(eq(reqVO.getApiTemplateId()))).thenReturn( randomPojo(SmsTemplateRespDTO.class, o -> o.setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()))); // 调用 smsTemplateService.updateSmsTemplate(reqVO); // 校验是否更新正确 SmsTemplateDO smsTemplate = smsTemplateMapper.selectById(reqVO.getId()); // 获取最新的 assertPojoEquals(reqVO, smsTemplate); assertEquals(Lists.newArrayList("operation", "code"), smsTemplate.getParams()); assertEquals(channelDO.getCode(), smsTemplate.getChannelCode()); } @Test public void testUpdateSmsTemplate_notExists() { // 准备参数 SmsTemplateSaveReqVO reqVO = randomPojo(SmsTemplateSaveReqVO.class); // 调用, 并断言异常 assertServiceException(() -> smsTemplateService.updateSmsTemplate(reqVO), SMS_TEMPLATE_NOT_EXISTS); } @Test public void testDeleteSmsTemplate_success() { // mock 数据 SmsTemplateDO dbSmsTemplate = randomSmsTemplateDO(); smsTemplateMapper.insert(dbSmsTemplate);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbSmsTemplate.getId(); // 调用 smsTemplateService.deleteSmsTemplate(id); // 校验数据不存在了 assertNull(smsTemplateMapper.selectById(id)); } @Test public void testDeleteSmsTemplate_notExists() { // 准备参数 Long id = randomLongId(); // 调用, 并断言异常 assertServiceException(() -> smsTemplateService.deleteSmsTemplate(id), SMS_TEMPLATE_NOT_EXISTS); } @Test public void testGetSmsTemplate() { // mock 数据 SmsTemplateDO dbSmsTemplate = randomSmsTemplateDO(); smsTemplateMapper.insert(dbSmsTemplate);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbSmsTemplate.getId(); // 调用 SmsTemplateDO smsTemplate = smsTemplateService.getSmsTemplate(id); // 校验 assertPojoEquals(dbSmsTemplate, smsTemplate); } @Test public void testGetSmsTemplateByCodeFromCache() { // mock 数据 SmsTemplateDO dbSmsTemplate = randomSmsTemplateDO(); smsTemplateMapper.insert(dbSmsTemplate);// @Sql: 先插入出一条存在的数据 // 准备参数 String code = dbSmsTemplate.getCode(); // 调用 SmsTemplateDO smsTemplate = smsTemplateService.getSmsTemplateByCodeFromCache(code); // 校验 assertPojoEquals(dbSmsTemplate, smsTemplate); } @Test public void testGetSmsTemplatePage() { // mock 数据 SmsTemplateDO dbSmsTemplate = randomPojo(SmsTemplateDO.class, o -> { // 等会查询到 o.setType(SmsTemplateTypeEnum.PROMOTION.getType()); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setCode("tudou"); o.setContent("yshop"); o.setApiTemplateId("yshop"); o.setChannelId(1L); o.setCreateTime(buildTime(2021, 11, 11)); }); smsTemplateMapper.insert(dbSmsTemplate); // 测试 type 不匹配 smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setType(SmsTemplateTypeEnum.VERIFICATION_CODE.getType()))); // 测试 status 不匹配 smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); // 测试 code 不匹配 smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setCode("yuanma"))); // 测试 content 不匹配 smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setContent("源码"))); // 测试 apiTemplateId 不匹配 smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setApiTemplateId("nai"))); // 测试 channelId 不匹配 smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setChannelId(2L))); // 测试 createTime 不匹配 smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setCreateTime(buildTime(2021, 12, 12)))); // 准备参数 SmsTemplatePageReqVO reqVO = new SmsTemplatePageReqVO(); reqVO.setType(SmsTemplateTypeEnum.PROMOTION.getType()); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); reqVO.setCode("tu"); reqVO.setContent("yshop"); reqVO.setApiTemplateId("yu"); reqVO.setChannelId(1L); reqVO.setCreateTime(buildBetweenTime(2021, 11, 1, 2021, 12, 1)); // 调用 PageResult pageResult = smsTemplateService.getSmsTemplatePage(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbSmsTemplate, pageResult.getList().get(0)); } @Test public void testGetSmsTemplateCountByChannelId() { // mock 数据 SmsTemplateDO dbSmsTemplate = randomPojo(SmsTemplateDO.class, o -> o.setChannelId(1L)); smsTemplateMapper.insert(dbSmsTemplate); // 测试 channelId 不匹配 smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setChannelId(2L))); // 准备参数 Long channelId = 1L; // 调用 Long count = smsTemplateService.getSmsTemplateCountByChannelId(channelId); // 断言 assertEquals(1, count); } @Test public void testValidateSmsChannel_success() { // 准备参数 Long channelId = randomLongId(); // mock 方法 SmsChannelDO channelDO = randomPojo(SmsChannelDO.class, o -> { o.setId(channelId); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 保证 status 开启,创建必须处于这个状态 }); when(smsChannelService.getSmsChannel(eq(channelId))).thenReturn(channelDO); // 调用 SmsChannelDO returnChannelDO = smsTemplateService.validateSmsChannel(channelId); // 断言 assertPojoEquals(returnChannelDO, channelDO); } @Test public void testValidateSmsChannel_notExists() { // 准备参数 Long channelId = randomLongId(); // 调用,校验异常 assertServiceException(() -> smsTemplateService.validateSmsChannel(channelId), SMS_CHANNEL_NOT_EXISTS); } @Test public void testValidateSmsChannel_disable() { // 准备参数 Long channelId = randomLongId(); // mock 方法 SmsChannelDO channelDO = randomPojo(SmsChannelDO.class, o -> { o.setId(channelId); o.setStatus(CommonStatusEnum.DISABLE.getStatus()); // 保证 status 禁用,触发失败 }); when(smsChannelService.getSmsChannel(eq(channelId))).thenReturn(channelDO); // 调用,校验异常 assertServiceException(() -> smsTemplateService.validateSmsChannel(channelId), SMS_CHANNEL_DISABLE); } @Test public void testValidateDictDataValueUnique_success() { // 调用,成功 smsTemplateService.validateSmsTemplateCodeDuplicate(randomLongId(), randomString()); } @Test public void testValidateSmsTemplateCodeDuplicate_valueDuplicateForCreate() { // 准备参数 String code = randomString(); // mock 数据 smsTemplateMapper.insert(randomSmsTemplateDO(o -> o.setCode(code))); // 调用,校验异常 assertServiceException(() -> smsTemplateService.validateSmsTemplateCodeDuplicate(null, code), SMS_TEMPLATE_CODE_DUPLICATE, code); } @Test public void testValidateDictDataValueUnique_valueDuplicateForUpdate() { // 准备参数 Long id = randomLongId(); String code = randomString(); // mock 数据 smsTemplateMapper.insert(randomSmsTemplateDO(o -> o.setCode(code))); // 调用,校验异常 assertServiceException(() -> smsTemplateService.validateSmsTemplateCodeDuplicate(id, code), SMS_TEMPLATE_CODE_DUPLICATE, code); } // ========== 随机对象 ========== @SafeVarargs private static SmsTemplateDO randomSmsTemplateDO(Consumer... consumers) { Consumer consumer = (o) -> { o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 o.setType(randomEle(SmsTemplateTypeEnum.values()).getType()); // 保证 type 的 范围 }; return randomPojo(SmsTemplateDO.class, ArrayUtils.append(consumer, consumers)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/social/SocialClientServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.social; import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.api.WxMaUserService; import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; import cn.hutool.core.util.ReflectUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO; import co.yixiang.yshop.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.social.SocialClientDO; import co.yixiang.yshop.module.system.dal.mysql.social.SocialClientMapper; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties; import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; import com.xingyuv.jushauth.config.AuthConfig; import com.xingyuv.jushauth.model.AuthResponse; import com.xingyuv.jushauth.model.AuthUser; import com.xingyuv.jushauth.request.AuthDefaultRequest; import com.xingyuv.jushauth.request.AuthRequest; import com.xingyuv.jushauth.utils.AuthStateUtils; import com.xingyuv.justauth.AuthRequestFactory; import me.chanjar.weixin.common.bean.WxJsapiSignature; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.mp.api.WxMpService; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import org.springframework.data.redis.core.StringRedisTemplate; import jakarta.annotation.Resource; import static cn.hutool.core.util.RandomUtil.randomEle; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; /** * {@link SocialClientServiceImpl} 的单元测试类 * * @author yshop */ @Import(SocialClientServiceImpl.class) public class SocialClientServiceImplTest extends BaseDbUnitTest { @Resource private SocialClientServiceImpl socialClientService; @Resource private SocialClientMapper socialClientMapper; @MockBean private AuthRequestFactory authRequestFactory; @MockBean private WxMpService wxMpService; @MockBean private WxMpProperties wxMpProperties; @MockBean private StringRedisTemplate stringRedisTemplate; @MockBean private WxMaService wxMaService; @MockBean private WxMaProperties wxMaProperties; @Test public void testGetAuthorizeUrl() { try (MockedStatic authStateUtilsMock = mockStatic(AuthStateUtils.class)) { // 准备参数 Integer socialType = SocialTypeEnum.WECHAT_MP.getType(); Integer userType = randomPojo(UserTypeEnum.class).getValue(); String redirectUri = "sss"; // mock 获得对应的 AuthRequest 实现 AuthRequest authRequest = mock(AuthRequest.class); when(authRequestFactory.get(eq("WECHAT_MP"))).thenReturn(authRequest); // mock 方法 authStateUtilsMock.when(AuthStateUtils::createState).thenReturn("aoteman"); when(authRequest.authorize(eq("aoteman"))).thenReturn("https://www.yixiang.co?redirect_uri=yyy"); // 调用 String url = socialClientService.getAuthorizeUrl(socialType, userType, redirectUri); // 断言 assertEquals("https://www.yixiang.co?redirect_uri=sss", url); } } @Test public void testAuthSocialUser_success() { // 准备参数 Integer socialType = SocialTypeEnum.WECHAT_MP.getType(); Integer userType = randomPojo(UserTypeEnum.class).getValue(); String code = randomString(); String state = randomString(); // mock 方法(AuthRequest) AuthRequest authRequest = mock(AuthRequest.class); when(authRequestFactory.get(eq("WECHAT_MP"))).thenReturn(authRequest); // mock 方法(AuthResponse) AuthUser authUser = randomPojo(AuthUser.class); AuthResponse authResponse = new AuthResponse<>(2000, null, authUser); when(authRequest.login(argThat(authCallback -> { assertEquals(code, authCallback.getCode()); assertEquals(state, authCallback.getState()); return true; }))).thenReturn(authResponse); // 调用 AuthUser result = socialClientService.getAuthUser(socialType, userType, code, state); // 断言 assertSame(authUser, result); } @Test public void testAuthSocialUser_fail() { // 准备参数 Integer socialType = SocialTypeEnum.WECHAT_MP.getType(); Integer userType = randomPojo(UserTypeEnum.class).getValue(); String code = randomString(); String state = randomString(); // mock 方法(AuthRequest) AuthRequest authRequest = mock(AuthRequest.class); when(authRequestFactory.get(eq("WECHAT_MP"))).thenReturn(authRequest); // mock 方法(AuthResponse) AuthResponse authResponse = new AuthResponse<>(0, "模拟失败", null); when(authRequest.login(argThat(authCallback -> { assertEquals(code, authCallback.getCode()); assertEquals(state, authCallback.getState()); return true; }))).thenReturn(authResponse); // 调用并断言 assertServiceException( () -> socialClientService.getAuthUser(socialType, userType, code, state), SOCIAL_USER_AUTH_FAILURE, "模拟失败"); } @Test public void testBuildAuthRequest_clientNull() { // 准备参数 Integer socialType = SocialTypeEnum.WECHAT_MP.getType(); Integer userType = randomPojo(SocialTypeEnum.class).getType(); // mock 获得对应的 AuthRequest 实现 AuthRequest authRequest = mock(AuthDefaultRequest.class); AuthConfig authConfig = (AuthConfig) ReflectUtil.getFieldValue(authRequest, "config"); when(authRequestFactory.get(eq("WECHAT_MP"))).thenReturn(authRequest); // 调用 AuthRequest result = socialClientService.buildAuthRequest(socialType, userType); // 断言 assertSame(authRequest, result); assertSame(authConfig, ReflectUtil.getFieldValue(authConfig, "config")); } @Test public void testBuildAuthRequest_clientDisable() { // 准备参数 Integer socialType = SocialTypeEnum.WECHAT_MP.getType(); Integer userType = randomPojo(SocialTypeEnum.class).getType(); // mock 获得对应的 AuthRequest 实现 AuthRequest authRequest = mock(AuthDefaultRequest.class); AuthConfig authConfig = (AuthConfig) ReflectUtil.getFieldValue(authRequest, "config"); when(authRequestFactory.get(eq("WECHAT_MP"))).thenReturn(authRequest); // mock 数据 SocialClientDO client = randomPojo(SocialClientDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()) .setUserType(userType).setSocialType(socialType)); socialClientMapper.insert(client); // 调用 AuthRequest result = socialClientService.buildAuthRequest(socialType, userType); // 断言 assertSame(authRequest, result); assertSame(authConfig, ReflectUtil.getFieldValue(authConfig, "config")); } @Test public void testBuildAuthRequest_clientEnable() { // 准备参数 Integer socialType = SocialTypeEnum.WECHAT_MP.getType(); Integer userType = randomPojo(SocialTypeEnum.class).getType(); // mock 获得对应的 AuthRequest 实现 AuthConfig authConfig = mock(AuthConfig.class); AuthRequest authRequest = mock(AuthDefaultRequest.class); ReflectUtil.setFieldValue(authRequest, "config", authConfig); when(authRequestFactory.get(eq("WECHAT_MP"))).thenReturn(authRequest); // mock 数据 SocialClientDO client = randomPojo(SocialClientDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()) .setUserType(userType).setSocialType(socialType)); socialClientMapper.insert(client); // 调用 AuthRequest result = socialClientService.buildAuthRequest(socialType, userType); // 断言 assertSame(authRequest, result); assertNotSame(authConfig, ReflectUtil.getFieldValue(authRequest, "config")); } // =================== 微信公众号独有 =================== @Test public void testCreateWxMpJsapiSignature() throws WxErrorException { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); String url = randomString(); // mock 方法 WxJsapiSignature signature = randomPojo(WxJsapiSignature.class); when(wxMpService.createJsapiSignature(eq(url))).thenReturn(signature); // 调用 WxJsapiSignature result = socialClientService.createWxMpJsapiSignature(userType, url); // 断言 assertSame(signature, result); } @Test public void testGetWxMpService_clientNull() { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); // mock 方法 // 调用 WxMpService result = socialClientService.getWxMpService(userType); // 断言 assertSame(wxMpService, result); } @Test public void testGetWxMpService_clientDisable() { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); // mock 数据 SocialClientDO client = randomPojo(SocialClientDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()) .setUserType(userType).setSocialType(SocialTypeEnum.WECHAT_MP.getType())); socialClientMapper.insert(client); // 调用 WxMpService result = socialClientService.getWxMpService(userType); // 断言 assertSame(wxMpService, result); } @Test public void testGetWxMpService_clientEnable() { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); // mock 数据 SocialClientDO client = randomPojo(SocialClientDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()) .setUserType(userType).setSocialType(SocialTypeEnum.WECHAT_MP.getType())); socialClientMapper.insert(client); // mock 方法 WxMpProperties.ConfigStorage configStorage = mock(WxMpProperties.ConfigStorage.class); when(wxMpProperties.getConfigStorage()).thenReturn(configStorage); // 调用 WxMpService result = socialClientService.getWxMpService(userType); // 断言 assertNotSame(wxMpService, result); assertEquals(client.getClientId(), result.getWxMpConfigStorage().getAppId()); assertEquals(client.getClientSecret(), result.getWxMpConfigStorage().getSecret()); } // =================== 微信小程序独有 =================== @Test public void testGetWxMaPhoneNumberInfo_success() throws WxErrorException { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); String phoneCode = randomString(); // mock 方法 WxMaUserService userService = mock(WxMaUserService.class); when(wxMaService.getUserService()).thenReturn(userService); WxMaPhoneNumberInfo phoneNumber = randomPojo(WxMaPhoneNumberInfo.class); when(userService.getPhoneNoInfo(eq(phoneCode))).thenReturn(phoneNumber); // 调用 WxMaPhoneNumberInfo result = socialClientService.getWxMaPhoneNumberInfo(userType, phoneCode); // 断言 assertSame(phoneNumber, result); } @Test public void testGetWxMaPhoneNumberInfo_exception() throws WxErrorException { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); String phoneCode = randomString(); // mock 方法 WxMaUserService userService = mock(WxMaUserService.class); when(wxMaService.getUserService()).thenReturn(userService); WxErrorException wxErrorException = new WxErrorException(new NullPointerException()); when(userService.getPhoneNoInfo(eq(phoneCode))).thenThrow(wxErrorException); // 调用并断言异常 assertServiceException(() -> socialClientService.getWxMaPhoneNumberInfo(userType, phoneCode), SOCIAL_CLIENT_WEIXIN_MINI_APP_PHONE_CODE_ERROR); } @Test public void testGetWxMaService_clientNull() { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); // mock 方法 // 调用 WxMaService result = socialClientService.getWxMaService(userType); // 断言 assertSame(wxMaService, result); } @Test public void testGetWxMaService_clientDisable() { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); // mock 数据 SocialClientDO client = randomPojo(SocialClientDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()) .setUserType(userType).setSocialType(SocialTypeEnum.WECHAT_MINI_APP.getType())); socialClientMapper.insert(client); // 调用 WxMaService result = socialClientService.getWxMaService(userType); // 断言 assertSame(wxMaService, result); } @Test public void testGetWxMaService_clientEnable() { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); // mock 数据 SocialClientDO client = randomPojo(SocialClientDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()) .setUserType(userType).setSocialType(SocialTypeEnum.WECHAT_MINI_APP.getType())); socialClientMapper.insert(client); // mock 方法 WxMaProperties.ConfigStorage configStorage = mock(WxMaProperties.ConfigStorage.class); when(wxMaProperties.getConfigStorage()).thenReturn(configStorage); // 调用 WxMaService result = socialClientService.getWxMaService(userType); // 断言 assertNotSame(wxMaService, result); assertEquals(client.getClientId(), result.getWxMaConfig().getAppid()); assertEquals(client.getClientSecret(), result.getWxMaConfig().getSecret()); } // =================== 客户端管理 =================== @Test public void testCreateSocialClient_success() { // 准备参数 SocialClientSaveReqVO reqVO = randomPojo(SocialClientSaveReqVO.class, o -> o.setSocialType(randomEle(SocialTypeEnum.values()).getType()) .setUserType(randomEle(UserTypeEnum.values()).getValue()) .setStatus(randomCommonStatus())) .setId(null); // 防止 id 被赋值 // 调用 Long socialClientId = socialClientService.createSocialClient(reqVO); // 断言 assertNotNull(socialClientId); // 校验记录的属性是否正确 SocialClientDO socialClient = socialClientMapper.selectById(socialClientId); assertPojoEquals(reqVO, socialClient, "id"); } @Test public void testUpdateSocialClient_success() { // mock 数据 SocialClientDO dbSocialClient = randomPojo(SocialClientDO.class); socialClientMapper.insert(dbSocialClient);// @Sql: 先插入出一条存在的数据 // 准备参数 SocialClientSaveReqVO reqVO = randomPojo(SocialClientSaveReqVO.class, o -> { o.setId(dbSocialClient.getId()); // 设置更新的 ID o.setSocialType(randomEle(SocialTypeEnum.values()).getType()) .setUserType(randomEle(UserTypeEnum.values()).getValue()) .setStatus(randomCommonStatus()); }); // 调用 socialClientService.updateSocialClient(reqVO); // 校验是否更新正确 SocialClientDO socialClient = socialClientMapper.selectById(reqVO.getId()); // 获取最新的 assertPojoEquals(reqVO, socialClient); } @Test public void testUpdateSocialClient_notExists() { // 准备参数 SocialClientSaveReqVO reqVO = randomPojo(SocialClientSaveReqVO.class); // 调用, 并断言异常 assertServiceException(() -> socialClientService.updateSocialClient(reqVO), SOCIAL_CLIENT_NOT_EXISTS); } @Test public void testDeleteSocialClient_success() { // mock 数据 SocialClientDO dbSocialClient = randomPojo(SocialClientDO.class); socialClientMapper.insert(dbSocialClient);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbSocialClient.getId(); // 调用 socialClientService.deleteSocialClient(id); // 校验数据不存在了 assertNull(socialClientMapper.selectById(id)); } @Test public void testDeleteSocialClient_notExists() { // 准备参数 Long id = randomLongId(); // 调用, 并断言异常 assertServiceException(() -> socialClientService.deleteSocialClient(id), SOCIAL_CLIENT_NOT_EXISTS); } @Test public void testGetSocialClient() { // mock 数据 SocialClientDO dbSocialClient = randomPojo(SocialClientDO.class); socialClientMapper.insert(dbSocialClient);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbSocialClient.getId(); // 调用 SocialClientDO socialClient = socialClientService.getSocialClient(id); // 校验数据正确 assertPojoEquals(dbSocialClient, socialClient); } @Test public void testGetSocialClientPage() { // mock 数据 SocialClientDO dbSocialClient = randomPojo(SocialClientDO.class, o -> { // 等会查询到 o.setName("芋头"); o.setSocialType(SocialTypeEnum.GITEE.getType()); o.setUserType(UserTypeEnum.ADMIN.getValue()); o.setClientId("yshop"); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); }); socialClientMapper.insert(dbSocialClient); // 测试 name 不匹配 socialClientMapper.insert(cloneIgnoreId(dbSocialClient, o -> o.setName(randomString()))); // 测试 socialType 不匹配 socialClientMapper.insert(cloneIgnoreId(dbSocialClient, o -> o.setSocialType(SocialTypeEnum.DINGTALK.getType()))); // 测试 userType 不匹配 socialClientMapper.insert(cloneIgnoreId(dbSocialClient, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); // 测试 clientId 不匹配 socialClientMapper.insert(cloneIgnoreId(dbSocialClient, o -> o.setClientId("dao"))); // 测试 status 不匹配 socialClientMapper.insert(cloneIgnoreId(dbSocialClient, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); // 准备参数 SocialClientPageReqVO reqVO = new SocialClientPageReqVO(); reqVO.setName("芋"); reqVO.setSocialType(SocialTypeEnum.GITEE.getType()); reqVO.setUserType(UserTypeEnum.ADMIN.getValue()); reqVO.setClientId("yu"); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 调用 PageResult pageResult = socialClientService.getSocialClientPage(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbSocialClient, pageResult.getList().get(0)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/social/SocialUserServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.social; import co.yixiang.yshop.framework.common.enums.UserTypeEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.api.social.dto.SocialUserBindReqDTO; import co.yixiang.yshop.module.system.api.social.dto.SocialUserRespDTO; import co.yixiang.yshop.module.system.controller.admin.socail.vo.user.SocialUserPageReqVO; import co.yixiang.yshop.module.system.dal.dataobject.social.SocialUserBindDO; import co.yixiang.yshop.module.system.dal.dataobject.social.SocialUserDO; import co.yixiang.yshop.module.system.dal.mysql.social.SocialUserBindMapper; import co.yixiang.yshop.module.system.dal.mysql.social.SocialUserMapper; import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum; import com.xingyuv.jushauth.model.AuthUser; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.util.List; import static cn.hutool.core.util.RandomUtil.randomEle; import static cn.hutool.core.util.RandomUtil.randomLong; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildTime; import static co.yixiang.yshop.framework.common.util.json.JsonUtils.toJsonString; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomPojo; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomString; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.SOCIAL_USER_NOT_FOUND; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.when; /** * {@link SocialUserServiceImpl} 的单元测试类 * * @author yshop */ @Import(SocialUserServiceImpl.class) public class SocialUserServiceImplTest extends BaseDbUnitTest { @Resource private SocialUserServiceImpl socialUserService; @Resource private SocialUserMapper socialUserMapper; @Resource private SocialUserBindMapper socialUserBindMapper; @MockBean private SocialClientService socialClientService; @Test public void testGetSocialUserList() { Long userId = 1L; Integer userType = UserTypeEnum.ADMIN.getValue(); // mock 获得社交用户 SocialUserDO socialUser = randomPojo(SocialUserDO.class).setType(SocialTypeEnum.GITEE.getType()); socialUserMapper.insert(socialUser); // 可被查到 socialUserMapper.insert(randomPojo(SocialUserDO.class)); // 不可被查到 // mock 获得绑定 socialUserBindMapper.insert(randomPojo(SocialUserBindDO.class) // 可被查询到 .setUserId(userId).setUserType(userType).setSocialType(SocialTypeEnum.GITEE.getType()) .setSocialUserId(socialUser.getId())); socialUserBindMapper.insert(randomPojo(SocialUserBindDO.class) // 不可被查询到 .setUserId(2L).setUserType(userType).setSocialType(SocialTypeEnum.DINGTALK.getType())); // 调用 List result = socialUserService.getSocialUserList(userId, userType); // 断言 assertEquals(1, result.size()); assertPojoEquals(socialUser, result.get(0)); } @Test public void testBindSocialUser() { // 准备参数 SocialUserBindReqDTO reqDTO = new SocialUserBindReqDTO() .setUserId(1L).setUserType(UserTypeEnum.ADMIN.getValue()) .setSocialType(SocialTypeEnum.GITEE.getType()).setCode("test_code").setState("test_state"); // mock 数据:获得社交用户 SocialUserDO socialUser = randomPojo(SocialUserDO.class).setType(reqDTO.getSocialType()) .setCode(reqDTO.getCode()).setState(reqDTO.getState()); socialUserMapper.insert(socialUser); // mock 数据:用户可能之前已经绑定过该社交类型 socialUserBindMapper.insert(randomPojo(SocialUserBindDO.class).setUserId(1L).setUserType(UserTypeEnum.ADMIN.getValue()) .setSocialType(SocialTypeEnum.GITEE.getType()).setSocialUserId(-1L)); // mock 数据:社交用户可能之前绑定过别的用户 socialUserBindMapper.insert(randomPojo(SocialUserBindDO.class).setUserType(UserTypeEnum.ADMIN.getValue()) .setSocialType(SocialTypeEnum.GITEE.getType()).setSocialUserId(socialUser.getId())); // 调用 String openid = socialUserService.bindSocialUser(reqDTO); // 断言 List socialUserBinds = socialUserBindMapper.selectList(); assertEquals(1, socialUserBinds.size()); assertEquals(socialUser.getOpenid(), openid); } @Test public void testUnbindSocialUser_success() { // 准备参数 Long userId = 1L; Integer userType = UserTypeEnum.ADMIN.getValue(); Integer type = SocialTypeEnum.GITEE.getType(); String openid = "test_openid"; // mock 数据:社交用户 SocialUserDO socialUser = randomPojo(SocialUserDO.class).setType(type).setOpenid(openid); socialUserMapper.insert(socialUser); // mock 数据:社交绑定关系 SocialUserBindDO socialUserBind = randomPojo(SocialUserBindDO.class).setUserType(userType) .setUserId(userId).setSocialType(type); socialUserBindMapper.insert(socialUserBind); // 调用 socialUserService.unbindSocialUser(userId, userType, type, openid); // 断言 assertEquals(0, socialUserBindMapper.selectCount(null).intValue()); } @Test public void testUnbindSocialUser_notFound() { // 调用,并断言 assertServiceException( () -> socialUserService.unbindSocialUser(randomLong(), UserTypeEnum.ADMIN.getValue(), SocialTypeEnum.GITEE.getType(), "test_openid"), SOCIAL_USER_NOT_FOUND); } @Test public void testGetSocialUser() { // 准备参数 Integer userType = UserTypeEnum.ADMIN.getValue(); Integer type = SocialTypeEnum.GITEE.getType(); String code = "tudou"; String state = "yuanma"; // mock 社交用户 SocialUserDO socialUserDO = randomPojo(SocialUserDO.class).setType(type).setCode(code).setState(state); socialUserMapper.insert(socialUserDO); // mock 社交用户的绑定 Long userId = randomLong(); SocialUserBindDO socialUserBind = randomPojo(SocialUserBindDO.class).setUserType(userType).setUserId(userId) .setSocialType(type).setSocialUserId(socialUserDO.getId()); socialUserBindMapper.insert(socialUserBind); // 调用 SocialUserRespDTO socialUser = socialUserService.getSocialUserByCode(userType, type, code, state); // 断言 assertEquals(userId, socialUser.getUserId()); assertEquals(socialUserDO.getOpenid(), socialUser.getOpenid()); } @Test public void testAuthSocialUser_exists() { // 准备参数 Integer socialType = SocialTypeEnum.GITEE.getType(); Integer userType = randomEle(SocialTypeEnum.values()).getType(); String code = "tudou"; String state = "yuanma"; // mock 方法 SocialUserDO socialUser = randomPojo(SocialUserDO.class).setType(socialType).setCode(code).setState(state); socialUserMapper.insert(socialUser); // 调用 SocialUserDO result = socialUserService.authSocialUser(socialType, userType, code, state); // 断言 assertPojoEquals(socialUser, result); } @Test public void testAuthSocialUser_notNull() { // mock 数据 SocialUserDO socialUser = randomPojo(SocialUserDO.class, o -> o.setType(SocialTypeEnum.GITEE.getType()).setCode("tudou").setState("yuanma")); socialUserMapper.insert(socialUser); // 准备参数 Integer socialType = SocialTypeEnum.GITEE.getType(); Integer userType = randomEle(SocialTypeEnum.values()).getType(); String code = "tudou"; String state = "yuanma"; // 调用 SocialUserDO result = socialUserService.authSocialUser(socialType, userType, code, state); // 断言 assertPojoEquals(socialUser, result); } @Test public void testAuthSocialUser_insert() { // 准备参数 Integer socialType = SocialTypeEnum.GITEE.getType(); Integer userType = randomEle(SocialTypeEnum.values()).getType(); String code = "tudou"; String state = "yuanma"; // mock 方法 AuthUser authUser = randomPojo(AuthUser.class); when(socialClientService.getAuthUser(eq(socialType), eq(userType), eq(code), eq(state))).thenReturn(authUser); // 调用 SocialUserDO result = socialUserService.authSocialUser(socialType, userType, code, state); // 断言 assertBindSocialUser(socialType, result, authUser); assertEquals(code, result.getCode()); assertEquals(state, result.getState()); } @Test public void testAuthSocialUser_update() { // 准备参数 Integer socialType = SocialTypeEnum.GITEE.getType(); Integer userType = randomEle(SocialTypeEnum.values()).getType(); String code = "tudou"; String state = "yuanma"; // mock 数据 socialUserMapper.insert(randomPojo(SocialUserDO.class).setType(socialType).setOpenid("test_openid")); // mock 方法 AuthUser authUser = randomPojo(AuthUser.class); when(socialClientService.getAuthUser(eq(socialType), eq(userType), eq(code), eq(state))).thenReturn(authUser); // 调用 SocialUserDO result = socialUserService.authSocialUser(socialType, userType, code, state); // 断言 assertBindSocialUser(socialType, result, authUser); assertEquals(code, result.getCode()); assertEquals(state, result.getState()); } private void assertBindSocialUser(Integer type, SocialUserDO socialUser, AuthUser authUser) { assertEquals(authUser.getToken().getAccessToken(), socialUser.getToken()); assertEquals(toJsonString(authUser.getToken()), socialUser.getRawTokenInfo()); assertEquals(authUser.getNickname(), socialUser.getNickname()); assertEquals(authUser.getAvatar(), socialUser.getAvatar()); assertEquals(toJsonString(authUser.getRawUserInfo()), socialUser.getRawUserInfo()); assertEquals(type, socialUser.getType()); assertEquals(authUser.getUuid(), socialUser.getOpenid()); } @Test public void testGetSocialUser_id() { // mock 数据 SocialUserDO socialUserDO = randomPojo(SocialUserDO.class); socialUserMapper.insert(socialUserDO); // 参数准备 Long id = socialUserDO.getId(); // 调用 SocialUserDO dbSocialUserDO = socialUserService.getSocialUser(id); // 断言 assertPojoEquals(socialUserDO, dbSocialUserDO); } @Test public void testGetSocialUserPage() { // mock 数据 SocialUserDO dbSocialUser = randomPojo(SocialUserDO.class, o -> { // 等会查询到 o.setType(SocialTypeEnum.GITEE.getType()); o.setNickname("yshop"); o.setOpenid("yshopyuanma"); o.setCreateTime(buildTime(2020, 1, 15)); }); socialUserMapper.insert(dbSocialUser); // 测试 type 不匹配 socialUserMapper.insert(cloneIgnoreId(dbSocialUser, o -> o.setType(SocialTypeEnum.DINGTALK.getType()))); // 测试 nickname 不匹配 socialUserMapper.insert(cloneIgnoreId(dbSocialUser, o -> o.setNickname(randomString()))); // 测试 openid 不匹配 socialUserMapper.insert(cloneIgnoreId(dbSocialUser, o -> o.setOpenid("java"))); // 测试 createTime 不匹配 socialUserMapper.insert(cloneIgnoreId(dbSocialUser, o -> o.setCreateTime(buildTime(2020, 1, 21)))); // 准备参数 SocialUserPageReqVO reqVO = new SocialUserPageReqVO(); reqVO.setType(SocialTypeEnum.GITEE.getType()); reqVO.setNickname("芋"); reqVO.setOpenid("yshop"); reqVO.setCreateTime(buildBetweenTime(2020, 1, 10, 2020, 1, 20)); // 调用 PageResult pageResult = socialUserService.getSocialUserPage(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbSocialUser, pageResult.getList().get(0)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/tenant/TenantPackageServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.tenant; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.controller.admin.tenant.vo.packages.TenantPackagePageReqVO; import co.yixiang.yshop.module.system.controller.admin.tenant.vo.packages.TenantPackageSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.tenant.TenantDO; import co.yixiang.yshop.module.system.dal.dataobject.tenant.TenantPackageDO; import co.yixiang.yshop.module.system.dal.mysql.tenant.TenantPackageMapper; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.util.List; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildTime; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * {@link TenantPackageServiceImpl} 的单元测试类 * * @author yshop */ @Import(TenantPackageServiceImpl.class) public class TenantPackageServiceImplTest extends BaseDbUnitTest { @Resource private TenantPackageServiceImpl tenantPackageService; @Resource private TenantPackageMapper tenantPackageMapper; @MockBean private TenantService tenantService; @Test public void testCreateTenantPackage_success() { // 准备参数 TenantPackageSaveReqVO reqVO = randomPojo(TenantPackageSaveReqVO.class, o -> o.setStatus(randomCommonStatus())) .setId(null); // 防止 id 被赋值 // 调用 Long tenantPackageId = tenantPackageService.createTenantPackage(reqVO); // 断言 assertNotNull(tenantPackageId); // 校验记录的属性是否正确 TenantPackageDO tenantPackage = tenantPackageMapper.selectById(tenantPackageId); assertPojoEquals(reqVO, tenantPackage, "id"); } @Test public void testUpdateTenantPackage_success() { // mock 数据 TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class, o -> o.setStatus(randomCommonStatus())); tenantPackageMapper.insert(dbTenantPackage);// @Sql: 先插入出一条存在的数据 // 准备参数 TenantPackageSaveReqVO reqVO = randomPojo(TenantPackageSaveReqVO.class, o -> { o.setId(dbTenantPackage.getId()); // 设置更新的 ID o.setStatus(randomCommonStatus()); }); // mock 方法 Long tenantId01 = randomLongId(); Long tenantId02 = randomLongId(); when(tenantService.getTenantListByPackageId(eq(reqVO.getId()))).thenReturn( asList(randomPojo(TenantDO.class, o -> o.setId(tenantId01)), randomPojo(TenantDO.class, o -> o.setId(tenantId02)))); // 调用 tenantPackageService.updateTenantPackage(reqVO); // 校验是否更新正确 TenantPackageDO tenantPackage = tenantPackageMapper.selectById(reqVO.getId()); // 获取最新的 assertPojoEquals(reqVO, tenantPackage); // 校验调用租户的菜单 verify(tenantService).updateTenantRoleMenu(eq(tenantId01), eq(reqVO.getMenuIds())); verify(tenantService).updateTenantRoleMenu(eq(tenantId02), eq(reqVO.getMenuIds())); } @Test public void testUpdateTenantPackage_notExists() { // 准备参数 TenantPackageSaveReqVO reqVO = randomPojo(TenantPackageSaveReqVO.class); // 调用, 并断言异常 assertServiceException(() -> tenantPackageService.updateTenantPackage(reqVO), TENANT_PACKAGE_NOT_EXISTS); } @Test public void testDeleteTenantPackage_success() { // mock 数据 TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class); tenantPackageMapper.insert(dbTenantPackage);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbTenantPackage.getId(); // mock 租户未使用该套餐 when(tenantService.getTenantCountByPackageId(eq(id))).thenReturn(0L); // 调用 tenantPackageService.deleteTenantPackage(id); // 校验数据不存在了 assertNull(tenantPackageMapper.selectById(id)); } @Test public void testDeleteTenantPackage_notExists() { // 准备参数 Long id = randomLongId(); // 调用, 并断言异常 assertServiceException(() -> tenantPackageService.deleteTenantPackage(id), TENANT_PACKAGE_NOT_EXISTS); } @Test public void testDeleteTenantPackage_used() { // mock 数据 TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class); tenantPackageMapper.insert(dbTenantPackage);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbTenantPackage.getId(); // mock 租户在使用该套餐 when(tenantService.getTenantCountByPackageId(eq(id))).thenReturn(1L); // 调用, 并断言异常 assertServiceException(() -> tenantPackageService.deleteTenantPackage(id), TENANT_PACKAGE_USED); } @Test public void testGetTenantPackagePage() { // mock 数据 TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class, o -> { // 等会查询到 o.setName("yshop"); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setRemark("源码解析"); o.setCreateTime(buildTime(2022, 10, 10)); }); tenantPackageMapper.insert(dbTenantPackage); // 测试 name 不匹配 tenantPackageMapper.insert(cloneIgnoreId(dbTenantPackage, o -> o.setName("源码"))); // 测试 status 不匹配 tenantPackageMapper.insert(cloneIgnoreId(dbTenantPackage, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); // 测试 remark 不匹配 tenantPackageMapper.insert(cloneIgnoreId(dbTenantPackage, o -> o.setRemark("解析"))); // 测试 createTime 不匹配 tenantPackageMapper.insert(cloneIgnoreId(dbTenantPackage, o -> o.setCreateTime(buildTime(2022, 11, 11)))); // 准备参数 TenantPackagePageReqVO reqVO = new TenantPackagePageReqVO(); reqVO.setName("yshop"); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); reqVO.setRemark("源码"); reqVO.setCreateTime(buildBetweenTime(2022, 10, 9, 2022, 10, 11)); // 调用 PageResult pageResult = tenantPackageService.getTenantPackagePage(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbTenantPackage, pageResult.getList().get(0)); } @Test public void testValidTenantPackage_success() { // mock 数据 TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); tenantPackageMapper.insert(dbTenantPackage);// @Sql: 先插入出一条存在的数据 // 调用 TenantPackageDO result = tenantPackageService.validTenantPackage(dbTenantPackage.getId()); // 断言 assertPojoEquals(dbTenantPackage, result); } @Test public void testValidTenantPackage_notExists() { // 准备参数 Long id = randomLongId(); // 调用, 并断言异常 assertServiceException(() -> tenantPackageService.validTenantPackage(id), TENANT_PACKAGE_NOT_EXISTS); } @Test public void testValidTenantPackage_disable() { // mock 数据 TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); tenantPackageMapper.insert(dbTenantPackage);// @Sql: 先插入出一条存在的数据 // 调用, 并断言异常 assertServiceException(() -> tenantPackageService.validTenantPackage(dbTenantPackage.getId()), TENANT_PACKAGE_DISABLE, dbTenantPackage.getName()); } @Test public void testGetTenantPackage() { // mock 数据 TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class); tenantPackageMapper.insert(dbTenantPackage);// @Sql: 先插入出一条存在的数据 // 调用 TenantPackageDO result = tenantPackageService.getTenantPackage(dbTenantPackage.getId()); // 断言 assertPojoEquals(result, dbTenantPackage); } @Test public void testGetTenantPackageListByStatus() { // mock 数据 TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); tenantPackageMapper.insert(dbTenantPackage); // 测试 status 不匹配 tenantPackageMapper.insert(cloneIgnoreId(dbTenantPackage, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); // 调用 List list = tenantPackageService.getTenantPackageListByStatus( CommonStatusEnum.ENABLE.getStatus()); assertEquals(1, list.size()); assertPojoEquals(dbTenantPackage, list.get(0)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/tenant/TenantServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.tenant; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.tenant.config.TenantProperties; import co.yixiang.yshop.framework.tenant.core.context.TenantContextHolder; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; import co.yixiang.yshop.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO; import co.yixiang.yshop.module.system.dal.dataobject.permission.MenuDO; import co.yixiang.yshop.module.system.dal.dataobject.permission.RoleDO; import co.yixiang.yshop.module.system.dal.dataobject.tenant.TenantDO; import co.yixiang.yshop.module.system.dal.dataobject.tenant.TenantPackageDO; import co.yixiang.yshop.module.system.dal.mysql.tenant.TenantMapper; import co.yixiang.yshop.module.system.enums.permission.RoleCodeEnum; import co.yixiang.yshop.module.system.enums.permission.RoleTypeEnum; import co.yixiang.yshop.module.system.service.permission.MenuService; import co.yixiang.yshop.module.system.service.permission.PermissionService; import co.yixiang.yshop.module.system.service.permission.RoleService; import co.yixiang.yshop.module.system.service.tenant.handler.TenantInfoHandler; import co.yixiang.yshop.module.system.service.tenant.handler.TenantMenuHandler; import co.yixiang.yshop.module.system.service.user.AdminUserService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.time.LocalDateTime; import java.util.Arrays; import java.util.Collections; import java.util.List; import static co.yixiang.yshop.framework.common.util.collection.SetUtils.asSet; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildTime; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static co.yixiang.yshop.module.system.dal.dataobject.tenant.TenantDO.PACKAGE_ID_SYSTEM; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; import static java.util.Arrays.asList; import static java.util.Collections.singleton; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; /** * {@link TenantServiceImpl} 的单元测试类 * * @author yshop */ @Import(TenantServiceImpl.class) public class TenantServiceImplTest extends BaseDbUnitTest { @Resource private TenantServiceImpl tenantService; @Resource private TenantMapper tenantMapper; @MockBean private TenantProperties tenantProperties; @MockBean private TenantPackageService tenantPackageService; @MockBean private AdminUserService userService; @MockBean private RoleService roleService; @MockBean private MenuService menuService; @MockBean private PermissionService permissionService; @BeforeEach public void setUp() { // 清理租户上下文 TenantContextHolder.clear(); } @Test public void testGetTenantIdList() { // mock 数据 TenantDO tenant = randomPojo(TenantDO.class, o -> o.setId(1L)); tenantMapper.insert(tenant); // 调用,并断言业务异常 List result = tenantService.getTenantIdList(); assertEquals(Collections.singletonList(1L), result); } @Test public void testValidTenant_notExists() { assertServiceException(() -> tenantService.validTenant(randomLongId()), TENANT_NOT_EXISTS); } @Test public void testValidTenant_disable() { // mock 数据 TenantDO tenant = randomPojo(TenantDO.class, o -> o.setId(1L).setStatus(CommonStatusEnum.DISABLE.getStatus())); tenantMapper.insert(tenant); // 调用,并断言业务异常 assertServiceException(() -> tenantService.validTenant(1L), TENANT_DISABLE, tenant.getName()); } @Test public void testValidTenant_expired() { // mock 数据 TenantDO tenant = randomPojo(TenantDO.class, o -> o.setId(1L).setStatus(CommonStatusEnum.ENABLE.getStatus()) .setExpireTime(buildTime(2020, 2, 2))); tenantMapper.insert(tenant); // 调用,并断言业务异常 assertServiceException(() -> tenantService.validTenant(1L), TENANT_EXPIRE, tenant.getName()); } @Test public void testValidTenant_success() { // mock 数据 TenantDO tenant = randomPojo(TenantDO.class, o -> o.setId(1L).setStatus(CommonStatusEnum.ENABLE.getStatus()) .setExpireTime(LocalDateTime.now().plusDays(1))); tenantMapper.insert(tenant); // 调用,并断言业务异常 tenantService.validTenant(1L); } @Test public void testCreateTenant() { // mock 套餐 100L TenantPackageDO tenantPackage = randomPojo(TenantPackageDO.class, o -> o.setId(100L)); when(tenantPackageService.validTenantPackage(eq(100L))).thenReturn(tenantPackage); // mock 角色 200L when(roleService.createRole(argThat(role -> { assertEquals(RoleCodeEnum.TENANT_ADMIN.getName(), role.getName()); assertEquals(RoleCodeEnum.TENANT_ADMIN.getCode(), role.getCode()); assertEquals(0, role.getSort()); assertEquals("系统自动生成", role.getRemark()); return true; }), eq(RoleTypeEnum.SYSTEM.getType()))).thenReturn(200L); // mock 用户 300L when(userService.createUser(argThat(user -> { assertEquals("yshop", user.getUsername()); assertEquals("yuanma", user.getPassword()); assertEquals("yshop", user.getNickname()); assertEquals("15601691300", user.getMobile()); return true; }))).thenReturn(300L); // 准备参数 TenantSaveReqVO reqVO = randomPojo(TenantSaveReqVO.class, o -> { o.setContactName("yshop"); o.setContactMobile("15601691300"); o.setPackageId(100L); o.setStatus(randomCommonStatus()); o.setWebsite("https://www.yixiang.co"); o.setUsername("yshop"); o.setPassword("yuanma"); }).setId(null); // 设置为 null,方便后面校验 // 调用 Long tenantId = tenantService.createTenant(reqVO); // 断言 assertNotNull(tenantId); // 校验记录的属性是否正确 TenantDO tenant = tenantMapper.selectById(tenantId); assertPojoEquals(reqVO, tenant, "id"); assertEquals(300L, tenant.getContactUserId()); // verify 分配权限 verify(permissionService).assignRoleMenu(eq(200L), same(tenantPackage.getMenuIds())); // verify 分配角色 verify(permissionService).assignUserRole(eq(300L), eq(singleton(200L))); } @Test public void testUpdateTenant_success() { // mock 数据 TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setStatus(randomCommonStatus())); tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 // 准备参数 TenantSaveReqVO reqVO = randomPojo(TenantSaveReqVO.class, o -> { o.setId(dbTenant.getId()); // 设置更新的 ID o.setStatus(randomCommonStatus()); o.setWebsite(randomString()); }); // mock 套餐 TenantPackageDO tenantPackage = randomPojo(TenantPackageDO.class, o -> o.setMenuIds(asSet(200L, 201L))); when(tenantPackageService.validTenantPackage(eq(reqVO.getPackageId()))).thenReturn(tenantPackage); // mock 所有角色 RoleDO role100 = randomPojo(RoleDO.class, o -> o.setId(100L).setCode(RoleCodeEnum.TENANT_ADMIN.getCode())); role100.setTenantId(dbTenant.getId()); RoleDO role101 = randomPojo(RoleDO.class, o -> o.setId(101L)); role101.setTenantId(dbTenant.getId()); when(roleService.getRoleList()).thenReturn(asList(role100, role101)); // mock 每个角色的权限 when(permissionService.getRoleMenuListByRoleId(eq(101L))).thenReturn(asSet(201L, 202L)); // 调用 tenantService.updateTenant(reqVO); // 校验是否更新正确 TenantDO tenant = tenantMapper.selectById(reqVO.getId()); // 获取最新的 assertPojoEquals(reqVO, tenant); // verify 设置角色权限 verify(permissionService).assignRoleMenu(eq(100L), eq(asSet(200L, 201L))); verify(permissionService).assignRoleMenu(eq(101L), eq(asSet(201L))); } @Test public void testUpdateTenant_notExists() { // 准备参数 TenantSaveReqVO reqVO = randomPojo(TenantSaveReqVO.class); // 调用, 并断言异常 assertServiceException(() -> tenantService.updateTenant(reqVO), TENANT_NOT_EXISTS); } @Test public void testUpdateTenant_system() { // mock 数据 TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setPackageId(PACKAGE_ID_SYSTEM)); tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 // 准备参数 TenantSaveReqVO reqVO = randomPojo(TenantSaveReqVO.class, o -> { o.setId(dbTenant.getId()); // 设置更新的 ID }); // 调用,校验业务异常 assertServiceException(() -> tenantService.updateTenant(reqVO), TENANT_CAN_NOT_UPDATE_SYSTEM); } @Test public void testDeleteTenant_success() { // mock 数据 TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setStatus(randomCommonStatus())); tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbTenant.getId(); // 调用 tenantService.deleteTenant(id); // 校验数据不存在了 assertNull(tenantMapper.selectById(id)); } @Test public void testDeleteTenant_notExists() { // 准备参数 Long id = randomLongId(); // 调用, 并断言异常 assertServiceException(() -> tenantService.deleteTenant(id), TENANT_NOT_EXISTS); } @Test public void testDeleteTenant_system() { // mock 数据 TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setPackageId(PACKAGE_ID_SYSTEM)); tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbTenant.getId(); // 调用, 并断言异常 assertServiceException(() -> tenantService.deleteTenant(id), TENANT_CAN_NOT_UPDATE_SYSTEM); } @Test public void testGetTenant() { // mock 数据 TenantDO dbTenant = randomPojo(TenantDO.class); tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbTenant.getId(); // 调用 TenantDO result = tenantService.getTenant(id); // 校验存在 assertPojoEquals(result, dbTenant); } @Test public void testGetTenantPage() { // mock 数据 TenantDO dbTenant = randomPojo(TenantDO.class, o -> { // 等会查询到 o.setName("yshop"); o.setContactName("yshop"); o.setContactMobile("15601691300"); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setCreateTime(buildTime(2020, 12, 12)); }); tenantMapper.insert(dbTenant); // 测试 name 不匹配 tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setName(randomString()))); // 测试 contactName 不匹配 tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setContactName(randomString()))); // 测试 contactMobile 不匹配 tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setContactMobile(randomString()))); // 测试 status 不匹配 tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); // 测试 createTime 不匹配 tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setCreateTime(buildTime(2021, 12, 12)))); // 准备参数 TenantPageReqVO reqVO = new TenantPageReqVO(); reqVO.setName("yshop"); reqVO.setContactName("艿"); reqVO.setContactMobile("1560"); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); reqVO.setCreateTime(buildBetweenTime(2020, 12, 1, 2020, 12, 24)); // 调用 PageResult pageResult = tenantService.getTenantPage(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbTenant, pageResult.getList().get(0)); } @Test public void testGetTenantByName() { // mock 数据 TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setName("yshop")); tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 // 调用 TenantDO result = tenantService.getTenantByName("yshop"); // 校验存在 assertPojoEquals(result, dbTenant); } @Test public void testGetTenantByWebsite() { // mock 数据 TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setWebsite("https://www.yixiang.co")); tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 // 调用 TenantDO result = tenantService.getTenantByWebsite("https://www.yixiang.co"); // 校验存在 assertPojoEquals(result, dbTenant); } @Test public void testGetTenantListByPackageId() { // mock 数据 TenantDO dbTenant1 = randomPojo(TenantDO.class, o -> o.setPackageId(1L)); tenantMapper.insert(dbTenant1);// @Sql: 先插入出一条存在的数据 TenantDO dbTenant2 = randomPojo(TenantDO.class, o -> o.setPackageId(2L)); tenantMapper.insert(dbTenant2);// @Sql: 先插入出一条存在的数据 // 调用 List result = tenantService.getTenantListByPackageId(1L); assertEquals(1, result.size()); assertPojoEquals(dbTenant1, result.get(0)); } @Test public void testGetTenantCountByPackageId() { // mock 数据 TenantDO dbTenant1 = randomPojo(TenantDO.class, o -> o.setPackageId(1L)); tenantMapper.insert(dbTenant1);// @Sql: 先插入出一条存在的数据 TenantDO dbTenant2 = randomPojo(TenantDO.class, o -> o.setPackageId(2L)); tenantMapper.insert(dbTenant2);// @Sql: 先插入出一条存在的数据 // 调用 Long count = tenantService.getTenantCountByPackageId(1L); assertEquals(1, count); } @Test public void testHandleTenantInfo_disable() { // 准备参数 TenantInfoHandler handler = mock(TenantInfoHandler.class); // mock 禁用 when(tenantProperties.getEnable()).thenReturn(false); // 调用 tenantService.handleTenantInfo(handler); // 断言 verify(handler, never()).handle(any()); } @Test public void testHandleTenantInfo_success() { // 准备参数 TenantInfoHandler handler = mock(TenantInfoHandler.class); // mock 未禁用 when(tenantProperties.getEnable()).thenReturn(true); // mock 租户 TenantDO dbTenant = randomPojo(TenantDO.class); tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 TenantContextHolder.setTenantId(dbTenant.getId()); // 调用 tenantService.handleTenantInfo(handler); // 断言 verify(handler).handle(argThat(argument -> { assertPojoEquals(dbTenant, argument); return true; })); } @Test public void testHandleTenantMenu_disable() { // 准备参数 TenantMenuHandler handler = mock(TenantMenuHandler.class); // mock 禁用 when(tenantProperties.getEnable()).thenReturn(false); // 调用 tenantService.handleTenantMenu(handler); // 断言 verify(handler, never()).handle(any()); } @Test // 系统租户的情况 public void testHandleTenantMenu_system() { // 准备参数 TenantMenuHandler handler = mock(TenantMenuHandler.class); // mock 未禁用 when(tenantProperties.getEnable()).thenReturn(true); // mock 租户 TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setPackageId(PACKAGE_ID_SYSTEM)); tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 TenantContextHolder.setTenantId(dbTenant.getId()); // mock 菜单 when(menuService.getMenuList()).thenReturn(Arrays.asList(randomPojo(MenuDO.class, o -> o.setId(100L)), randomPojo(MenuDO.class, o -> o.setId(101L)))); // 调用 tenantService.handleTenantMenu(handler); // 断言 verify(handler).handle(asSet(100L, 101L)); } @Test // 普通租户的情况 public void testHandleTenantMenu_normal() { // 准备参数 TenantMenuHandler handler = mock(TenantMenuHandler.class); // mock 未禁用 when(tenantProperties.getEnable()).thenReturn(true); // mock 租户 TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setPackageId(200L)); tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 TenantContextHolder.setTenantId(dbTenant.getId()); // mock 菜单 when(tenantPackageService.getTenantPackage(eq(200L))).thenReturn(randomPojo(TenantPackageDO.class, o -> o.setMenuIds(asSet(100L, 101L)))); // 调用 tenantService.handleTenantMenu(handler); // 断言 verify(handler).handle(asSet(100L, 101L)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/java/co/yixiang/yshop/module/system/service/user/AdminUserServiceImplTest.java ================================================ package co.yixiang.yshop.module.system.service.user; import cn.hutool.core.util.RandomUtil; import co.yixiang.yshop.framework.common.enums.CommonStatusEnum; import co.yixiang.yshop.framework.common.exception.ServiceException; import co.yixiang.yshop.framework.common.pojo.PageResult; import co.yixiang.yshop.framework.common.util.collection.ArrayUtils; import co.yixiang.yshop.framework.common.util.collection.CollectionUtils; import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest; import co.yixiang.yshop.module.infra.api.file.FileApi; import co.yixiang.yshop.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; import co.yixiang.yshop.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; import co.yixiang.yshop.module.system.controller.admin.user.vo.user.*; import co.yixiang.yshop.module.system.dal.dataobject.dept.DeptDO; import co.yixiang.yshop.module.system.dal.dataobject.dept.PostDO; import co.yixiang.yshop.module.system.dal.dataobject.dept.UserPostDO; import co.yixiang.yshop.module.system.dal.dataobject.tenant.TenantDO; import co.yixiang.yshop.module.system.dal.dataobject.user.AdminUserDO; import co.yixiang.yshop.module.system.dal.mysql.dept.UserPostMapper; import co.yixiang.yshop.module.system.dal.mysql.user.AdminUserMapper; import co.yixiang.yshop.module.system.enums.common.SexEnum; import co.yixiang.yshop.module.system.service.dept.DeptService; import co.yixiang.yshop.module.system.service.dept.PostService; import co.yixiang.yshop.module.system.service.permission.PermissionService; import co.yixiang.yshop.module.system.service.tenant.TenantService; import org.junit.jupiter.api.Test; import org.mockito.stubbing.Answer; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import org.springframework.security.crypto.password.PasswordEncoder; import jakarta.annotation.Resource; import java.io.ByteArrayInputStream; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.function.Consumer; import static cn.hutool.core.util.RandomUtil.randomBytes; import static cn.hutool.core.util.RandomUtil.randomEle; import static co.yixiang.yshop.framework.common.util.collection.SetUtils.asSet; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; import static co.yixiang.yshop.framework.common.util.date.LocalDateTimeUtils.buildTime; import static co.yixiang.yshop.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals; import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException; import static co.yixiang.yshop.framework.test.core.util.RandomUtils.*; import static co.yixiang.yshop.module.system.enums.ErrorCodeConstants.*; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static org.assertj.core.util.Lists.newArrayList; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @Import(AdminUserServiceImpl.class) public class AdminUserServiceImplTest extends BaseDbUnitTest { @Resource private AdminUserServiceImpl userService; @Resource private AdminUserMapper userMapper; @Resource private UserPostMapper userPostMapper; @MockBean private DeptService deptService; @MockBean private PostService postService; @MockBean private PermissionService permissionService; @MockBean private PasswordEncoder passwordEncoder; @MockBean private TenantService tenantService; @MockBean private FileApi fileApi; @Test public void testCreatUser_success() { // 准备参数 UserSaveReqVO reqVO = randomPojo(UserSaveReqVO.class, o -> { o.setSex(RandomUtil.randomEle(SexEnum.values()).getSex()); o.setMobile(randomString()); o.setPostIds(asSet(1L, 2L)); }).setId(null); // 避免 id 被赋值 // mock 账户额度充足 TenantDO tenant = randomPojo(TenantDO.class, o -> o.setAccountCount(1)); doNothing().when(tenantService).handleTenantInfo(argThat(handler -> { handler.handle(tenant); return true; })); // mock deptService 的方法 DeptDO dept = randomPojo(DeptDO.class, o -> { o.setId(reqVO.getDeptId()); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); }); when(deptService.getDept(eq(dept.getId()))).thenReturn(dept); // mock postService 的方法 List posts = CollectionUtils.convertList(reqVO.getPostIds(), postId -> randomPojo(PostDO.class, o -> { o.setId(postId); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); })); when(postService.getPostList(eq(reqVO.getPostIds()), isNull())).thenReturn(posts); // mock passwordEncoder 的方法 when(passwordEncoder.encode(eq(reqVO.getPassword()))).thenReturn("yshopyuanma"); // 调用 Long userId = userService.createUser(reqVO); // 断言 AdminUserDO user = userMapper.selectById(userId); assertPojoEquals(reqVO, user, "password", "id"); assertEquals("yshopyuanma", user.getPassword()); assertEquals(CommonStatusEnum.ENABLE.getStatus(), user.getStatus()); // 断言关联岗位 List userPosts = userPostMapper.selectListByUserId(user.getId()); assertEquals(1L, userPosts.get(0).getPostId()); assertEquals(2L, userPosts.get(1).getPostId()); } @Test public void testCreatUser_max() { // 准备参数 UserSaveReqVO reqVO = randomPojo(UserSaveReqVO.class); // mock 账户额度不足 TenantDO tenant = randomPojo(TenantDO.class, o -> o.setAccountCount(-1)); doNothing().when(tenantService).handleTenantInfo(argThat(handler -> { handler.handle(tenant); return true; })); // 调用,并断言异常 assertServiceException(() -> userService.createUser(reqVO), USER_COUNT_MAX, -1); } @Test public void testUpdateUser_success() { // mock 数据 AdminUserDO dbUser = randomAdminUserDO(o -> o.setPostIds(asSet(1L, 2L))); userMapper.insert(dbUser); userPostMapper.insert(new UserPostDO().setUserId(dbUser.getId()).setPostId(1L)); userPostMapper.insert(new UserPostDO().setUserId(dbUser.getId()).setPostId(2L)); // 准备参数 UserSaveReqVO reqVO = randomPojo(UserSaveReqVO.class, o -> { o.setId(dbUser.getId()); o.setSex(RandomUtil.randomEle(SexEnum.values()).getSex()); o.setMobile(randomString()); o.setPostIds(asSet(2L, 3L)); }); // mock deptService 的方法 DeptDO dept = randomPojo(DeptDO.class, o -> { o.setId(reqVO.getDeptId()); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); }); when(deptService.getDept(eq(dept.getId()))).thenReturn(dept); // mock postService 的方法 List posts = CollectionUtils.convertList(reqVO.getPostIds(), postId -> randomPojo(PostDO.class, o -> { o.setId(postId); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); })); when(postService.getPostList(eq(reqVO.getPostIds()), isNull())).thenReturn(posts); // 调用 userService.updateUser(reqVO); // 断言 AdminUserDO user = userMapper.selectById(reqVO.getId()); assertPojoEquals(reqVO, user, "password"); // 断言关联岗位 List userPosts = userPostMapper.selectListByUserId(user.getId()); assertEquals(2L, userPosts.get(0).getPostId()); assertEquals(3L, userPosts.get(1).getPostId()); } @Test public void testUpdateUserLogin() { // mock 数据 AdminUserDO user = randomAdminUserDO(o -> o.setLoginDate(null)); userMapper.insert(user); // 准备参数 Long id = user.getId(); String loginIp = randomString(); // 调用 userService.updateUserLogin(id, loginIp); // 断言 AdminUserDO dbUser = userMapper.selectById(id); assertEquals(loginIp, dbUser.getLoginIp()); assertNotNull(dbUser.getLoginDate()); } @Test public void testUpdateUserProfile_success() { // mock 数据 AdminUserDO dbUser = randomAdminUserDO(); userMapper.insert(dbUser); // 准备参数 Long userId = dbUser.getId(); UserProfileUpdateReqVO reqVO = randomPojo(UserProfileUpdateReqVO.class, o -> { o.setMobile(randomString()); o.setSex(RandomUtil.randomEle(SexEnum.values()).getSex()); }); // 调用 userService.updateUserProfile(userId, reqVO); // 断言 AdminUserDO user = userMapper.selectById(userId); assertPojoEquals(reqVO, user); } @Test public void testUpdateUserPassword_success() { // mock 数据 AdminUserDO dbUser = randomAdminUserDO(o -> o.setPassword("encode:tudou")); userMapper.insert(dbUser); // 准备参数 Long userId = dbUser.getId(); UserProfileUpdatePasswordReqVO reqVO = randomPojo(UserProfileUpdatePasswordReqVO.class, o -> { o.setOldPassword("tudou"); o.setNewPassword("yuanma"); }); // mock 方法 when(passwordEncoder.encode(anyString())).then( (Answer) invocationOnMock -> "encode:" + invocationOnMock.getArgument(0)); when(passwordEncoder.matches(eq(reqVO.getOldPassword()), eq(dbUser.getPassword()))).thenReturn(true); // 调用 userService.updateUserPassword(userId, reqVO); // 断言 AdminUserDO user = userMapper.selectById(userId); assertEquals("encode:yuanma", user.getPassword()); } @Test public void testUpdateUserAvatar_success() throws Exception { // mock 数据 AdminUserDO dbUser = randomAdminUserDO(); userMapper.insert(dbUser); // 准备参数 Long userId = dbUser.getId(); byte[] avatarFileBytes = randomBytes(10); ByteArrayInputStream avatarFile = new ByteArrayInputStream(avatarFileBytes); // mock 方法 String avatar = randomString(); when(fileApi.createFile(eq( avatarFileBytes))).thenReturn(avatar); // 调用 userService.updateUserAvatar(userId, avatarFile); // 断言 AdminUserDO user = userMapper.selectById(userId); assertEquals(avatar, user.getAvatar()); } @Test public void testUpdateUserPassword02_success() { // mock 数据 AdminUserDO dbUser = randomAdminUserDO(); userMapper.insert(dbUser); // 准备参数 Long userId = dbUser.getId(); String password = "yshop"; // mock 方法 when(passwordEncoder.encode(anyString())).then( (Answer) invocationOnMock -> "encode:" + invocationOnMock.getArgument(0)); // 调用 userService.updateUserPassword(userId, password); // 断言 AdminUserDO user = userMapper.selectById(userId); assertEquals("encode:" + password, user.getPassword()); } @Test public void testUpdateUserStatus() { // mock 数据 AdminUserDO dbUser = randomAdminUserDO(); userMapper.insert(dbUser); // 准备参数 Long userId = dbUser.getId(); Integer status = randomCommonStatus(); // 调用 userService.updateUserStatus(userId, status); // 断言 AdminUserDO user = userMapper.selectById(userId); assertEquals(status, user.getStatus()); } @Test public void testDeleteUser_success(){ // mock 数据 AdminUserDO dbUser = randomAdminUserDO(); userMapper.insert(dbUser); // 准备参数 Long userId = dbUser.getId(); // 调用数据 userService.deleteUser(userId); // 校验结果 assertNull(userMapper.selectById(userId)); // 校验调用次数 verify(permissionService, times(1)).processUserDeleted(eq(userId)); } @Test public void testGetUserByUsername() { // mock 数据 AdminUserDO dbUser = randomAdminUserDO(); userMapper.insert(dbUser); // 准备参数 String username = dbUser.getUsername(); // 调用 AdminUserDO user = userService.getUserByUsername(username); // 断言 assertPojoEquals(dbUser, user); } @Test public void testGetUserByMobile() { // mock 数据 AdminUserDO dbUser = randomAdminUserDO(); userMapper.insert(dbUser); // 准备参数 String mobile = dbUser.getMobile(); // 调用 AdminUserDO user = userService.getUserByMobile(mobile); // 断言 assertPojoEquals(dbUser, user); } @Test public void testGetUserPage() { // mock 数据 AdminUserDO dbUser = initGetUserPageData(); // 准备参数 UserPageReqVO reqVO = new UserPageReqVO(); reqVO.setUsername("tu"); reqVO.setMobile("1560"); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); reqVO.setCreateTime(buildBetweenTime(2020, 12, 1, 2020, 12, 24)); reqVO.setDeptId(1L); // 其中,1L 是 2L 的父部门 // mock 方法 List deptList = newArrayList(randomPojo(DeptDO.class, o -> o.setId(2L))); when(deptService.getChildDeptList(eq(reqVO.getDeptId()))).thenReturn(deptList); // 调用 PageResult pageResult = userService.getUserPage(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbUser, pageResult.getList().get(0)); } /** * 初始化 getUserPage 方法的测试数据 */ private AdminUserDO initGetUserPageData() { // mock 数据 AdminUserDO dbUser = randomAdminUserDO(o -> { // 等会查询到 o.setUsername("tudou"); o.setMobile("15601691300"); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setCreateTime(buildTime(2020, 12, 12)); o.setDeptId(2L); }); userMapper.insert(dbUser); // 测试 username 不匹配 userMapper.insert(cloneIgnoreId(dbUser, o -> o.setUsername("dou"))); // 测试 mobile 不匹配 userMapper.insert(cloneIgnoreId(dbUser, o -> o.setMobile("18818260888"))); // 测试 status 不匹配 userMapper.insert(cloneIgnoreId(dbUser, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); // 测试 createTime 不匹配 userMapper.insert(cloneIgnoreId(dbUser, o -> o.setCreateTime(buildTime(2020, 11, 11)))); // 测试 dept 不匹配 userMapper.insert(cloneIgnoreId(dbUser, o -> o.setDeptId(0L))); return dbUser; } @Test public void testGetUser() { // mock 数据 AdminUserDO dbUser = randomAdminUserDO(); userMapper.insert(dbUser); // 准备参数 Long userId = dbUser.getId(); // 调用 AdminUserDO user = userService.getUser(userId); // 断言 assertPojoEquals(dbUser, user); } @Test public void testGetUserListByDeptIds() { // mock 数据 AdminUserDO dbUser = randomAdminUserDO(o -> o.setDeptId(1L)); userMapper.insert(dbUser); // 测试 deptId 不匹配 userMapper.insert(cloneIgnoreId(dbUser, o -> o.setDeptId(2L))); // 准备参数 Collection deptIds = singleton(1L); // 调用 List list = userService.getUserListByDeptIds(deptIds); // 断言 assertEquals(1, list.size()); assertEquals(dbUser, list.get(0)); } /** * 情况一,校验不通过,导致插入失败 */ @Test public void testImportUserList_01() { // 准备参数 UserImportExcelVO importUser = randomPojo(UserImportExcelVO.class, o -> { }); // mock 方法,模拟失败 doThrow(new ServiceException(DEPT_NOT_FOUND)).when(deptService).validateDeptList(any()); // 调用 UserImportRespVO respVO = userService.importUserList(newArrayList(importUser), true); // 断言 assertEquals(0, respVO.getCreateUsernames().size()); assertEquals(0, respVO.getUpdateUsernames().size()); assertEquals(1, respVO.getFailureUsernames().size()); assertEquals(DEPT_NOT_FOUND.getMsg(), respVO.getFailureUsernames().get(importUser.getUsername())); } /** * 情况二,不存在,进行插入 */ @Test public void testImportUserList_02() { // 准备参数 UserImportExcelVO importUser = randomPojo(UserImportExcelVO.class, o -> { o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 o.setSex(randomEle(SexEnum.values()).getSex()); // 保证 sex 的范围 }); // mock deptService 的方法 DeptDO dept = randomPojo(DeptDO.class, o -> { o.setId(importUser.getDeptId()); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); }); when(deptService.getDept(eq(dept.getId()))).thenReturn(dept); // mock passwordEncoder 的方法 when(passwordEncoder.encode(eq("yshopyuanma"))).thenReturn("java"); // 调用 UserImportRespVO respVO = userService.importUserList(newArrayList(importUser), true); // 断言 assertEquals(1, respVO.getCreateUsernames().size()); AdminUserDO user = userMapper.selectByUsername(respVO.getCreateUsernames().get(0)); assertPojoEquals(importUser, user); assertEquals("java", user.getPassword()); assertEquals(0, respVO.getUpdateUsernames().size()); assertEquals(0, respVO.getFailureUsernames().size()); } /** * 情况三,存在,但是不强制更新 */ @Test public void testImportUserList_03() { // mock 数据 AdminUserDO dbUser = randomAdminUserDO(); userMapper.insert(dbUser); // 准备参数 UserImportExcelVO importUser = randomPojo(UserImportExcelVO.class, o -> { o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 o.setSex(randomEle(SexEnum.values()).getSex()); // 保证 sex 的范围 o.setUsername(dbUser.getUsername()); }); // mock deptService 的方法 DeptDO dept = randomPojo(DeptDO.class, o -> { o.setId(importUser.getDeptId()); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); }); when(deptService.getDept(eq(dept.getId()))).thenReturn(dept); // 调用 UserImportRespVO respVO = userService.importUserList(newArrayList(importUser), false); // 断言 assertEquals(0, respVO.getCreateUsernames().size()); assertEquals(0, respVO.getUpdateUsernames().size()); assertEquals(1, respVO.getFailureUsernames().size()); assertEquals(USER_USERNAME_EXISTS.getMsg(), respVO.getFailureUsernames().get(importUser.getUsername())); } /** * 情况四,存在,强制更新 */ @Test public void testImportUserList_04() { // mock 数据 AdminUserDO dbUser = randomAdminUserDO(); userMapper.insert(dbUser); // 准备参数 UserImportExcelVO importUser = randomPojo(UserImportExcelVO.class, o -> { o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 o.setSex(randomEle(SexEnum.values()).getSex()); // 保证 sex 的范围 o.setUsername(dbUser.getUsername()); }); // mock deptService 的方法 DeptDO dept = randomPojo(DeptDO.class, o -> { o.setId(importUser.getDeptId()); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); }); when(deptService.getDept(eq(dept.getId()))).thenReturn(dept); // 调用 UserImportRespVO respVO = userService.importUserList(newArrayList(importUser), true); // 断言 assertEquals(0, respVO.getCreateUsernames().size()); assertEquals(1, respVO.getUpdateUsernames().size()); AdminUserDO user = userMapper.selectByUsername(respVO.getUpdateUsernames().get(0)); assertPojoEquals(importUser, user); assertEquals(0, respVO.getFailureUsernames().size()); } @Test public void testValidateUserExists_notExists() { assertServiceException(() -> userService.validateUserExists(randomLongId()), USER_NOT_EXISTS); } @Test public void testValidateUsernameUnique_usernameExistsForCreate() { // 准备参数 String username = randomString(); // mock 数据 userMapper.insert(randomAdminUserDO(o -> o.setUsername(username))); // 调用,校验异常 assertServiceException(() -> userService.validateUsernameUnique(null, username), USER_USERNAME_EXISTS); } @Test public void testValidateUsernameUnique_usernameExistsForUpdate() { // 准备参数 Long id = randomLongId(); String username = randomString(); // mock 数据 userMapper.insert(randomAdminUserDO(o -> o.setUsername(username))); // 调用,校验异常 assertServiceException(() -> userService.validateUsernameUnique(id, username), USER_USERNAME_EXISTS); } @Test public void testValidateEmailUnique_emailExistsForCreate() { // 准备参数 String email = randomString(); // mock 数据 userMapper.insert(randomAdminUserDO(o -> o.setEmail(email))); // 调用,校验异常 assertServiceException(() -> userService.validateEmailUnique(null, email), USER_EMAIL_EXISTS); } @Test public void testValidateEmailUnique_emailExistsForUpdate() { // 准备参数 Long id = randomLongId(); String email = randomString(); // mock 数据 userMapper.insert(randomAdminUserDO(o -> o.setEmail(email))); // 调用,校验异常 assertServiceException(() -> userService.validateEmailUnique(id, email), USER_EMAIL_EXISTS); } @Test public void testValidateMobileUnique_mobileExistsForCreate() { // 准备参数 String mobile = randomString(); // mock 数据 userMapper.insert(randomAdminUserDO(o -> o.setMobile(mobile))); // 调用,校验异常 assertServiceException(() -> userService.validateMobileUnique(null, mobile), USER_MOBILE_EXISTS); } @Test public void testValidateMobileUnique_mobileExistsForUpdate() { // 准备参数 Long id = randomLongId(); String mobile = randomString(); // mock 数据 userMapper.insert(randomAdminUserDO(o -> o.setMobile(mobile))); // 调用,校验异常 assertServiceException(() -> userService.validateMobileUnique(id, mobile), USER_MOBILE_EXISTS); } @Test public void testValidateOldPassword_notExists() { assertServiceException(() -> userService.validateOldPassword(randomLongId(), randomString()), USER_NOT_EXISTS); } @Test public void testValidateOldPassword_passwordFailed() { // mock 数据 AdminUserDO user = randomAdminUserDO(); userMapper.insert(user); // 准备参数 Long id = user.getId(); String oldPassword = user.getPassword(); // 调用,校验异常 assertServiceException(() -> userService.validateOldPassword(id, oldPassword), USER_PASSWORD_FAILED); // 校验调用 verify(passwordEncoder, times(1)).matches(eq(oldPassword), eq(user.getPassword())); } @Test public void testUserListByPostIds() { // 准备参数 Collection postIds = asSet(10L, 20L); // mock user1 数据 AdminUserDO user1 = randomAdminUserDO(o -> o.setPostIds(asSet(10L, 30L))); userMapper.insert(user1); userPostMapper.insert(new UserPostDO().setUserId(user1.getId()).setPostId(10L)); userPostMapper.insert(new UserPostDO().setUserId(user1.getId()).setPostId(30L)); // mock user2 数据 AdminUserDO user2 = randomAdminUserDO(o -> o.setPostIds(singleton(100L))); userMapper.insert(user2); userPostMapper.insert(new UserPostDO().setUserId(user2.getId()).setPostId(100L)); // 调用 List result = userService.getUserListByPostIds(postIds); // 断言 assertEquals(1, result.size()); assertEquals(user1, result.get(0)); } @Test public void testGetUserList() { // mock 数据 AdminUserDO user = randomAdminUserDO(); userMapper.insert(user); // 测试 id 不匹配 userMapper.insert(randomAdminUserDO()); // 准备参数 Collection ids = singleton(user.getId()); // 调用 List result = userService.getUserList(ids); // 断言 assertEquals(1, result.size()); assertEquals(user, result.get(0)); } @Test public void testGetUserMap() { // mock 数据 AdminUserDO user = randomAdminUserDO(); userMapper.insert(user); // 测试 id 不匹配 userMapper.insert(randomAdminUserDO()); // 准备参数 Collection ids = singleton(user.getId()); // 调用 Map result = userService.getUserMap(ids); // 断言 assertEquals(1, result.size()); assertEquals(user, result.get(user.getId())); } @Test public void testGetUserListByNickname() { // mock 数据 AdminUserDO user = randomAdminUserDO(o -> o.setNickname("芋头")); userMapper.insert(user); // 测试 nickname 不匹配 userMapper.insert(randomAdminUserDO(o -> o.setNickname("源码"))); // 准备参数 String nickname = "芋"; // 调用 List result = userService.getUserListByNickname(nickname); // 断言 assertEquals(1, result.size()); assertEquals(user, result.get(0)); } @Test public void testGetUserListByStatus() { // mock 数据 AdminUserDO user = randomAdminUserDO(o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); userMapper.insert(user); // 测试 status 不匹配 userMapper.insert(randomAdminUserDO(o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()))); // 准备参数 Integer status = CommonStatusEnum.DISABLE.getStatus(); // 调用 List result = userService.getUserListByStatus(status); // 断言 assertEquals(1, result.size()); assertEquals(user, result.get(0)); } @Test public void testValidateUserList_success() { // mock 数据 AdminUserDO userDO = randomAdminUserDO().setStatus(CommonStatusEnum.ENABLE.getStatus()); userMapper.insert(userDO); // 准备参数 List ids = singletonList(userDO.getId()); // 调用,无需断言 userService.validateUserList(ids); } @Test public void testValidateUserList_notFound() { // 准备参数 List ids = singletonList(randomLongId()); // 调用, 并断言异常 assertServiceException(() -> userService.validateUserList(ids), USER_NOT_EXISTS); } @Test public void testValidateUserList_notEnable() { // mock 数据 AdminUserDO userDO = randomAdminUserDO().setStatus(CommonStatusEnum.DISABLE.getStatus()); userMapper.insert(userDO); // 准备参数 List ids = singletonList(userDO.getId()); // 调用, 并断言异常 assertServiceException(() -> userService.validateUserList(ids), USER_IS_DISABLE, userDO.getNickname()); } // ========== 随机对象 ========== @SafeVarargs private static AdminUserDO randomAdminUserDO(Consumer... consumers) { Consumer consumer = (o) -> { o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 o.setSex(randomEle(SexEnum.values()).getSex()); // 保证 sex 的范围 }; return randomPojo(AdminUserDO.class, ArrayUtils.append(consumer, consumers)); } } ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/resources/application-unit-test.yaml ================================================ spring: main: lazy-initialization: true # 开启懒加载,加快速度 banner-mode: off # 单元测试,禁用 Banner --- #################### 数据库相关配置 #################### spring: # 数据源配置项 datasource: name: yixiang-drink url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 driver-class-name: org.h2.Driver username: sa password: druid: async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度 initial-size: 1 # 单元测试,配置为 1,提升启动速度 sql: init: schema-locations: classpath:/sql/create_tables.sql # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 data: redis: host: 127.0.0.1 # 地址 port: 16379 # 端口(单元测试,使用 16379 端口) database: 0 # 数据库索引 mybatis: lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 --- #################### 定时任务相关配置 #################### --- #################### 配置中心相关配置 #################### --- #################### 服务保障相关配置 #################### # Lock4j 配置项(单元测试,禁用 Lock4j) --- #################### 监控相关配置 #################### --- #################### yshop相关配置 #################### # yshop配置项,设置当前项目所有自定义的配置 yshop: info: base-package: co.yixiang.yshop.module captcha: timeout: 5m width: 160 height: 60 enable: true ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/resources/logback.xml ================================================ ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/resources/sql/clean.sql ================================================ DELETE FROM "system_dept"; DELETE FROM "system_dict_data"; DELETE FROM "system_role"; DELETE FROM "system_role_menu"; DELETE FROM "system_menu"; DELETE FROM "system_user_role"; DELETE FROM "system_dict_type"; DELETE FROM "system_user_session"; DELETE FROM "system_post"; DELETE FROM "system_user_post"; DELETE FROM "system_notice"; DELETE FROM "system_login_log"; DELETE FROM "system_operate_log"; DELETE FROM "system_users"; DELETE FROM "system_sms_channel"; DELETE FROM "system_sms_template"; DELETE FROM "system_sms_log"; DELETE FROM "system_sms_code"; DELETE FROM "system_social_client"; DELETE FROM "system_social_user"; DELETE FROM "system_social_user_bind"; DELETE FROM "system_tenant"; DELETE FROM "system_tenant_package"; DELETE FROM "system_oauth2_client"; DELETE FROM "system_oauth2_approve"; DELETE FROM "system_oauth2_access_token"; DELETE FROM "system_oauth2_refresh_token"; DELETE FROM "system_oauth2_code"; DELETE FROM "system_mail_account"; DELETE FROM "system_mail_template"; DELETE FROM "system_mail_log"; DELETE FROM "system_notify_template"; DELETE FROM "system_notify_message"; ================================================ FILE: yshop-drink-boot3/yshop-module-system/yshop-module-system-biz/src/test/resources/sql/create_tables.sql ================================================ CREATE TABLE IF NOT EXISTS "system_dept" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "name" varchar(30) NOT NULL DEFAULT '', "parent_id" bigint NOT NULL DEFAULT '0', "sort" int NOT NULL DEFAULT '0', "leader_user_id" bigint DEFAULT NULL, "phone" varchar(11) DEFAULT NULL, "email" varchar(50) DEFAULT NULL, "status" tinyint NOT NULL, "creator" varchar(64) DEFAULT '', "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar(64) DEFAULT '', "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, "tenant_id" bigint not null default '0', PRIMARY KEY ("id") ) COMMENT '部门表'; CREATE TABLE IF NOT EXISTS "system_dict_data" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "sort" int NOT NULL DEFAULT '0', "label" varchar(100) NOT NULL DEFAULT '', "value" varchar(100) NOT NULL DEFAULT '', "dict_type" varchar(100) NOT NULL DEFAULT '', "status" tinyint NOT NULL DEFAULT '0', "color_type" varchar(100) NOT NULL DEFAULT '', "css_class" varchar(100) NOT NULL DEFAULT '', "remark" varchar(500) DEFAULT NULL, "creator" varchar(64) DEFAULT '', "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar(64) DEFAULT '', "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT '字典数据表'; CREATE TABLE IF NOT EXISTS "system_role" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "name" varchar(30) NOT NULL, "code" varchar(100) NOT NULL, "sort" int NOT NULL, "data_scope" tinyint NOT NULL DEFAULT '1', "data_scope_dept_ids" varchar(500) NOT NULL DEFAULT '', "status" tinyint NOT NULL, "type" tinyint NOT NULL, "remark" varchar(500) DEFAULT NULL, "creator" varchar(64) DEFAULT '', "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar(64) DEFAULT '', "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, "tenant_id" bigint not null default '0', PRIMARY KEY ("id") ) COMMENT '角色信息表'; CREATE TABLE IF NOT EXISTS "system_role_menu" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "role_id" bigint NOT NULL, "menu_id" bigint NOT NULL, "creator" varchar(64) DEFAULT '', "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar(64) DEFAULT '', "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, "tenant_id" bigint not null default '0', PRIMARY KEY ("id") ) COMMENT '角色和菜单关联表'; CREATE TABLE IF NOT EXISTS "system_menu" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "name" varchar(50) NOT NULL, "permission" varchar(100) NOT NULL DEFAULT '', "type" tinyint NOT NULL, "sort" int NOT NULL DEFAULT '0', "parent_id" bigint NOT NULL DEFAULT '0', "path" varchar(200) DEFAULT '', "icon" varchar(100) DEFAULT '#', "component" varchar(255) DEFAULT NULL, "component_name" varchar(255) DEFAULT NULL, "status" tinyint NOT NULL DEFAULT '0', "visible" bit NOT NULL DEFAULT TRUE, "keep_alive" bit NOT NULL DEFAULT TRUE, "always_show" bit NOT NULL DEFAULT TRUE, "creator" varchar(64) DEFAULT '', "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar(64) DEFAULT '', "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT '菜单权限表'; CREATE TABLE IF NOT EXISTS "system_user_role" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "user_id" bigint NOT NULL, "role_id" bigint NOT NULL, "creator" varchar(64) DEFAULT '', "create_time" timestamp DEFAULT NULL, "updater" varchar(64) DEFAULT '', "update_time" timestamp DEFAULT NULL, "deleted" bit DEFAULT FALSE, "tenant_id" bigint not null default '0', PRIMARY KEY ("id") ) COMMENT '用户和角色关联表'; CREATE TABLE IF NOT EXISTS "system_dict_type" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "name" varchar(100) NOT NULL DEFAULT '', "type" varchar(100) NOT NULL DEFAULT '', "status" tinyint NOT NULL DEFAULT '0', "remark" varchar(500) DEFAULT NULL, "creator" varchar(64) DEFAULT '', "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar(64) DEFAULT '', "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, "deleted_time" timestamp NOT NULL, PRIMARY KEY ("id") ) COMMENT '字典类型表'; CREATE TABLE IF NOT EXISTS `system_user_session` ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, `token` varchar(32) NOT NULL, `user_id` bigint DEFAULT NULL, "user_type" tinyint NOT NULL, `username` varchar(50) NOT NULL DEFAULT '', `user_ip` varchar(50) DEFAULT NULL, `user_agent` varchar(512) DEFAULT NULL, `session_timeout` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "creator" varchar(64) DEFAULT '', "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `updater` varchar(64) DEFAULT '' , "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, "tenant_id" bigint not null default '0', PRIMARY KEY (`id`) ) COMMENT '用户在线 Session'; CREATE TABLE IF NOT EXISTS "system_post" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "code" varchar(64) NOT NULL, "name" varchar(50) NOT NULL, "sort" integer NOT NULL, "status" tinyint NOT NULL, "remark" varchar(500) DEFAULT NULL, "creator" varchar(64) DEFAULT '', "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar(64) DEFAULT '', "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, "tenant_id" bigint not null default '0', PRIMARY KEY ("id") ) COMMENT '岗位信息表'; CREATE TABLE IF NOT EXISTS `system_user_post`( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "user_id" bigint DEFAULT NULL, "post_id" bigint DEFAULT NULL, "creator" varchar(64) DEFAULT '', "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar(64) DEFAULT '', "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, "tenant_id" bigint not null default '0', PRIMARY KEY (`id`) ) COMMENT ='用户岗位表'; CREATE TABLE IF NOT EXISTS "system_notice" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "title" varchar(50) NOT NULL COMMENT '公告标题', "content" text NOT NULL COMMENT '公告内容', "type" tinyint NOT NULL COMMENT '公告类型(1通知 2公告)', "status" tinyint NOT NULL DEFAULT '0' COMMENT '公告状态(0正常 1关闭)', "creator" varchar(64) DEFAULT '' COMMENT '创建者', "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', "updater" varchar(64) DEFAULT '' COMMENT '更新者', "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', "deleted" bit NOT NULL DEFAULT 0 COMMENT '是否删除', "tenant_id" bigint not null default '0', PRIMARY KEY("id") ) COMMENT '通知公告表'; CREATE TABLE IF NOT EXISTS `system_login_log` ( `id` bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY, `log_type` bigint(4) NOT NULL, "user_id" bigint not null default '0', "user_type" tinyint NOT NULL, `trace_id` varchar(64) NOT NULL DEFAULT '', `username` varchar(50) NOT NULL DEFAULT '', `result` tinyint(4) NOT NULL, `user_ip` varchar(50) NOT NULL, `user_agent` varchar(512) NOT NULL, `creator` varchar(64) DEFAULT '', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updater` varchar(64) DEFAULT '', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `deleted` bit(1) NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) COMMENT ='系统访问记录'; CREATE TABLE IF NOT EXISTS `system_operate_log` ( `id` bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY, `trace_id` varchar(64) NOT NULL DEFAULT '', `user_id` bigint(20) NOT NULL, "user_type" tinyint not null default '0', `type` varchar(50) NOT NULL, `sub_type` varchar(50) NOT NULL, `biz_id` bigint(20) NOT NULL, `action` varchar(2000) NOT NULL DEFAULT '', `extra` varchar(512) NOT NULL DEFAULT '', `request_method` varchar(16) DEFAULT '', `request_url` varchar(255) DEFAULT '', `user_ip` varchar(50) DEFAULT NULL, `user_agent` varchar(200) DEFAULT NULL, `creator` varchar(64) DEFAULT '', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updater` varchar(64) DEFAULT '', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `deleted` bit(1) NOT NULL DEFAULT '0', "tenant_id" bigint not null default '0', PRIMARY KEY (`id`) ) COMMENT ='操作日志记录'; CREATE TABLE IF NOT EXISTS "system_users" ( "id" bigint not null GENERATED BY DEFAULT AS IDENTITY, "username" varchar(30) not null, "password" varchar(100) not null default '', "nickname" varchar(30) not null, "remark" varchar(500) default null, "dept_id" bigint default null, "post_ids" varchar(255) default null, "email" varchar(50) default '', "mobile" varchar(11) default '', "sex" tinyint default '0', "avatar" varchar(100) default '', "status" tinyint not null default '0', "login_ip" varchar(50) default '', "login_date" timestamp default null, "creator" varchar(64) default '', "create_time" timestamp not null default current_timestamp, "updater" varchar(64) default '', "update_time" timestamp not null default current_timestamp, "deleted" bit not null default false, "tenant_id" bigint not null default '0', primary key ("id") ) comment '用户信息表'; CREATE TABLE IF NOT EXISTS "system_sms_channel" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "signature" varchar(10) NOT NULL, "code" varchar(63) NOT NULL, "status" tinyint NOT NULL, "remark" varchar(255) DEFAULT NULL, "api_key" varchar(63) NOT NULL, "api_secret" varchar(63) DEFAULT NULL, "callback_url" varchar(255) DEFAULT NULL, "creator" varchar(64) DEFAULT '', "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar(64) DEFAULT '', "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT '短信渠道'; CREATE TABLE IF NOT EXISTS "system_sms_template" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "type" tinyint NOT NULL, "status" tinyint NOT NULL, "code" varchar(63) NOT NULL, "name" varchar(63) NOT NULL, "content" varchar(255) NOT NULL, "params" varchar(255) NOT NULL, "remark" varchar(255) DEFAULT NULL, "api_template_id" varchar(63) NOT NULL, "channel_id" bigint NOT NULL, "channel_code" varchar(63) NOT NULL, "creator" varchar(64) DEFAULT '', "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar(64) DEFAULT '', "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT '短信模板'; CREATE TABLE IF NOT EXISTS "system_sms_log" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "channel_id" bigint NOT NULL, "channel_code" varchar(63) NOT NULL, "template_id" bigint NOT NULL, "template_code" varchar(63) NOT NULL, "template_type" tinyint NOT NULL, "template_content" varchar(255) NOT NULL, "template_params" varchar(255) NOT NULL, "api_template_id" varchar(63) NOT NULL, "mobile" varchar(11) NOT NULL, "user_id" bigint DEFAULT '0', "user_type" tinyint DEFAULT '0', "send_status" tinyint NOT NULL DEFAULT '0', "send_time" timestamp DEFAULT NULL, "send_code" int DEFAULT NULL, "send_msg" varchar(255) DEFAULT NULL, "api_send_code" varchar(63) DEFAULT NULL, "api_send_msg" varchar(255) DEFAULT NULL, "api_request_id" varchar(255) DEFAULT NULL, "api_serial_no" varchar(255) DEFAULT NULL, "receive_status" tinyint NOT NULL DEFAULT '0', "receive_time" timestamp DEFAULT NULL, "api_receive_code" varchar(63) DEFAULT NULL, "api_receive_msg" varchar(255) DEFAULT NULL, "creator" varchar(64) DEFAULT '', "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar(64) DEFAULT '', "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT '短信日志'; CREATE TABLE IF NOT EXISTS "system_sms_code" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "mobile" varchar(11) NOT NULL, "code" varchar(11) NOT NULL, "scene" bigint NOT NULL, "create_ip" varchar NOT NULL, "today_index" int NOT NULL, "used" bit NOT NULL DEFAULT FALSE, "used_time" timestamp DEFAULT NULL, "used_ip" varchar NULL, "creator" varchar(64) DEFAULT '', "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar(64) DEFAULT '', "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT '短信日志'; CREATE TABLE IF NOT EXISTS "system_social_client" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "name" varchar(255) NOT NULL, "social_type" int NOT NULL, "user_type" int NOT NULL, "client_id" varchar(255) NOT NULL, "client_secret" varchar(255) NOT NULL, "agent_id" varchar(255) NOT NULL, "status" int NOT NULL, "creator" varchar(64) DEFAULT '', "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar(64) DEFAULT '', "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, "tenant_id" bigint not null default '0', PRIMARY KEY ("id") ) COMMENT '社交客户端表'; CREATE TABLE IF NOT EXISTS "system_social_user" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "type" tinyint NOT NULL, "openid" varchar(64) NOT NULL, "token" varchar(256) DEFAULT NULL, "raw_token_info" varchar(1024) NOT NULL, "nickname" varchar(32) NOT NULL, "avatar" varchar(255) DEFAULT NULL, "raw_user_info" varchar(1024) NOT NULL, "code" varchar(64) NOT NULL, "state" varchar(64), "creator" varchar(64) DEFAULT '', "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar(64) DEFAULT '', "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT '社交用户'; CREATE TABLE IF NOT EXISTS "system_social_user_bind" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "user_id" bigint NOT NULL, "user_type" tinyint NOT NULL, "social_type" tinyint NOT NULL, "social_user_id" number NOT NULL, "creator" varchar(64) DEFAULT '', "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar(64) DEFAULT '', "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT '社交用户的绑定'; CREATE TABLE IF NOT EXISTS "system_tenant" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "name" varchar(63) NOT NULL, "contact_user_id" bigint NOT NULL DEFAULT '0', "contact_name" varchar(255) NOT NULL, "contact_mobile" varchar(255), "status" tinyint NOT NULL, "website" varchar(63) DEFAULT '', "package_id" bigint NOT NULL, "expire_time" timestamp NOT NULL, "account_count" int NOT NULL, "creator" varchar(64) DEFAULT '', "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar(64) DEFAULT '', "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT '租户'; CREATE TABLE IF NOT EXISTS "system_tenant_package" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "name" varchar(30) NOT NULL, "status" tinyint NOT NULL, "remark" varchar(256), "menu_ids" varchar(2048) NOT NULL, "creator" varchar(64) DEFAULT '', "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar(64) DEFAULT '', "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT '租户套餐表'; CREATE TABLE IF NOT EXISTS "system_oauth2_client" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "client_id" varchar NOT NULL, "secret" varchar NOT NULL, "name" varchar NOT NULL, "logo" varchar NOT NULL, "description" varchar, "status" int NOT NULL, "access_token_validity_seconds" int NOT NULL, "refresh_token_validity_seconds" int NOT NULL, "redirect_uris" varchar NOT NULL, "authorized_grant_types" varchar NOT NULL, "scopes" varchar NOT NULL DEFAULT '', "auto_approve_scopes" varchar NOT NULL DEFAULT '', "authorities" varchar NOT NULL DEFAULT '', "resource_ids" varchar NOT NULL DEFAULT '', "additional_information" varchar NOT NULL DEFAULT '', "creator" varchar DEFAULT '', "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar DEFAULT '', "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT 'OAuth2 客户端表'; CREATE TABLE IF NOT EXISTS "system_oauth2_approve" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "user_id" bigint NOT NULL, "user_type" tinyint NOT NULL, "client_id" varchar NOT NULL, "scope" varchar NOT NULL, "approved" bit NOT NULL DEFAULT FALSE, "expires_time" datetime NOT NULL, "creator" varchar DEFAULT '', "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar DEFAULT '', "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT 'OAuth2 批准表'; CREATE TABLE IF NOT EXISTS "system_oauth2_access_token" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "user_id" bigint NOT NULL, "user_type" tinyint NOT NULL, "user_info" varchar NOT NULL, "access_token" varchar NOT NULL, "refresh_token" varchar NOT NULL, "client_id" varchar NOT NULL, "scopes" varchar NOT NULL, "approved" bit NOT NULL DEFAULT FALSE, "expires_time" datetime NOT NULL, "creator" varchar DEFAULT '', "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar DEFAULT '', "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, "tenant_id" bigint NOT NULL, PRIMARY KEY ("id") ) COMMENT 'OAuth2 访问令牌'; CREATE TABLE IF NOT EXISTS "system_oauth2_refresh_token" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "user_id" bigint NOT NULL, "user_type" tinyint NOT NULL, "refresh_token" varchar NOT NULL, "client_id" varchar NOT NULL, "scopes" varchar NOT NULL, "approved" bit NOT NULL DEFAULT FALSE, "expires_time" datetime NOT NULL, "creator" varchar DEFAULT '', "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar DEFAULT '', "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT 'OAuth2 刷新令牌'; CREATE TABLE IF NOT EXISTS "system_oauth2_code" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "user_id" bigint NOT NULL, "user_type" tinyint NOT NULL, "code" varchar NOT NULL, "client_id" varchar NOT NULL, "scopes" varchar NOT NULL, "expires_time" datetime NOT NULL, "redirect_uri" varchar NOT NULL, "state" varchar NOT NULL, "creator" varchar DEFAULT '', "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar DEFAULT '', "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT 'OAuth2 刷新令牌'; CREATE TABLE IF NOT EXISTS "system_mail_account" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "mail" varchar NOT NULL, "username" varchar NOT NULL, "password" varchar NOT NULL, "host" varchar NOT NULL, "port" int NOT NULL, "ssl_enable" bit NOT NULL, "starttls_enable" bit NOT NULL, "creator" varchar DEFAULT '', "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar DEFAULT '', "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT '邮箱账号表'; CREATE TABLE IF NOT EXISTS "system_mail_template" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "name" varchar NOT NULL, "code" varchar NOT NULL, "account_id" bigint NOT NULL, "nickname" varchar, "title" varchar NOT NULL, "content" varchar NOT NULL, "params" varchar NOT NULL, "status" varchar NOT NULL, "remark" varchar, "creator" varchar DEFAULT '', "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar DEFAULT '', "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT '邮件模版表'; CREATE TABLE IF NOT EXISTS "system_mail_log" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "user_id" bigint, "user_type" varchar, "to_mail" varchar NOT NULL, "account_id" bigint NOT NULL, "from_mail" varchar NOT NULL, "template_id" bigint NOT NULL, "template_code" varchar NOT NULL, "template_nickname" varchar, "template_title" varchar NOT NULL, "template_content" varchar NOT NULL, "template_params" varchar NOT NULL, "send_status" varchar NOT NULL, "send_time" datetime, "send_message_id" varchar, "send_exception" varchar, "creator" varchar DEFAULT '', "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar DEFAULT '', "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT '邮件日志表'; -- 将该建表 SQL 语句,添加到 yshop-module-system-biz 模块的 test/resources/sql/create_tables.sql 文件里 CREATE TABLE IF NOT EXISTS "system_notify_template" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "name" varchar NOT NULL, "code" varchar NOT NULL, "nickname" varchar NOT NULL, "content" varchar NOT NULL, "type" varchar NOT NULL, "params" varchar, "status" varchar NOT NULL, "remark" varchar, "creator" varchar DEFAULT '', "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar DEFAULT '', "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT '站内信模板表'; CREATE TABLE IF NOT EXISTS "system_notify_message" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "user_id" bigint NOT NULL, "user_type" varchar NOT NULL, "template_id" bigint NOT NULL, "template_code" varchar NOT NULL, "template_nickname" varchar NOT NULL, "template_content" varchar NOT NULL, "template_type" int NOT NULL, "template_params" varchar NOT NULL, "read_status" bit NOT NULL, "read_time" varchar, "creator" varchar DEFAULT '', "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar DEFAULT '', "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, "tenant_id" bigint not null default '0', PRIMARY KEY ("id") ) COMMENT '站内信消息表'; ================================================ FILE: yshop-drink-boot3/yshop-server/Dockerfile ================================================ ## AdoptOpenJDK 停止发布 OpenJDK 二进制,而 Eclipse Temurin 是它的延伸,提供更好的稳定性 ## 感谢复旦核博士的建议!灰子哥,牛皮! FROM eclipse-temurin:21-jre ## 创建目录,并使用它作为工作目录 RUN mkdir -p /yshop-server WORKDIR /yshop-server ## 将后端项目的 Jar 文件,复制到镜像中 COPY ./target/yshop-server.jar app.jar ## 设置 TZ 时区 ENV TZ=Asia/Shanghai ## 设置 JAVA_OPTS 环境变量,可通过 docker run -e "JAVA_OPTS=" 进行覆盖 ENV JAVA_OPTS="-Xms512m -Xmx512m -Djava.security.egd=file:/dev/./urandom" ## 应用参数 ENV ARGS="" ## 暴露后端项目的 48080 端口 EXPOSE 48080 ## 启动后端项目 CMD java ${JAVA_OPTS} -jar app.jar $ARGS ================================================ FILE: yshop-drink-boot3/yshop-server/pom.xml ================================================ co.yixiang.boot yshop ${revision} 4.0.0 yshop-server jar ${project.artifactId} 后端 Server 的主项目,通过引入需要 yshop-module-xxx 的依赖, 从而实现提供 RESTful API 给前端项目。 本质上来说,它就是个空壳(容器)!! https://gitee.com/guchengwuyue/yshop-drink co.yixiang.boot yshop-module-system-biz ${revision} co.yixiang.boot yshop-module-infra-biz ${revision} org.springframework.boot spring-boot-configuration-processor true co.yixiang.boot yshop-module-pay-biz ${revision} co.yixiang.boot yshop-module-express-biz ${revision} co.yixiang.boot yshop-module-message-biz ${revision} co.yixiang.boot yshop-module-mp-biz ${revision} co.yixiang.boot yshop-module-shop-biz ${revision} co.yixiang.boot yshop-module-order-biz ${revision} co.yixiang.boot yshop-module-coupon-biz ${revision} co.yixiang.boot yshop-module-score-biz ${revision} co.yixiang.boot yshop-spring-boot-starter-protection ${project.artifactId} org.springframework.boot spring-boot-maven-plugin ${spring.boot.version} repackage ================================================ FILE: yshop-drink-boot3/yshop-server/src/main/java/co/yixiang/yshop/server/YshopServerApplication.java ================================================ package co.yixiang.yshop.server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 项目的启动类 * @author yshop */ @SuppressWarnings("SpringComponentScan") // 忽略 IDEA 无法识别 ${yshop.info.base-package} @SpringBootApplication(scanBasePackages = {"${yshop.info.base-package}.server", "${yshop.info.base-package}.module"}) public class YshopServerApplication { public static void main(String[] args){ SpringApplication.run(YshopServerApplication.class, args); } } ================================================ FILE: yshop-drink-boot3/yshop-server/src/main/java/co/yixiang/yshop/server/controller/DefaultController.java ================================================ package co.yixiang.yshop.server.controller; import org.springframework.web.bind.annotation.RestController; /** * 默认 Controller,解决部分 module 未开启时的 404 提示。 * 例如说,/bpm/** 路径,工作流 * * @author yshop */ @RestController public class DefaultController { } ================================================ FILE: yshop-drink-boot3/yshop-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ com.egzosn.pay.spring.boot.autoconfigue.PayAutoConfiguration ================================================ FILE: yshop-drink-boot3/yshop-server/src/main/resources/application-dev.yaml ================================================ server: port: 48080 --- #################### 数据库相关配置 #################### spring: # 数据源配置项 datasource: druid: # Druid 【监控】相关的全局配置 web-stat-filter: enabled: true stat-view-servlet: enabled: true allow: # 设置白名单,不填则允许所有访问 url-pattern: /druid/* login-username: # 控制台管理用户名和密码 login-password: filter: stat: enabled: true log-slow-sql: true # 慢 SQL 记录 slow-sql-millis: 100 merge-sql: true wall: config: multi-statement-allow: true dynamic: # 多数据源配置 druid: # Druid 【连接池】相关的全局配置 initial-size: 5 # 初始连接数 min-idle: 10 # 最小连接池数量 max-active: 20 # 最大连接池数量 max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒 time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒 max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒 validation-query: SELECT 1 # 配置检测连接是否有效 test-while-idle: true test-on-borrow: false test-on-return: false primary: master datasource: master: url: jdbc:mysql://127.0.0.1:3306/yixiang-drink-open?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 username: root password: root slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 lazy: true # 开启懒加载,保证启动速度 url: jdbc:mysql://127.0.0.1:3306/yixiang-drink-open?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 username: root password: root # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 data: redis: host: 400-infra.server.yixiang.co # 地址 port: 6379 # 端口 database: 1 # 数据库索引 # password: 123456 # 密码,建议生产环境开启 --- #################### 定时任务相关配置 #################### # Quartz 配置项,对应 QuartzProperties 配置类 spring: quartz: auto-startup: true # 测试环境,需要开启 Job scheduler-name: schedulerName # Scheduler 名字。默认为 schedulerName job-store-type: jdbc # Job 存储器类型。默认为 memory 表示内存,可选 jdbc 使用数据库。 wait-for-jobs-to-complete-on-shutdown: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true properties: # 添加 Quartz Scheduler 附加属性,更多可以看 http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html 文档 org: quartz: # Scheduler 相关配置 scheduler: instanceName: schedulerName instanceId: AUTO # 自动生成 instance ID # JobStore 相关配置 jobStore: # JobStore 实现类。可见博客:https://blog.csdn.net/weixin_42458219/article/details/122247162 class: org.springframework.scheduling.quartz.LocalDataSourceJobStore isClustered: true # 是集群模式 clusterCheckinInterval: 15000 # 集群检查频率,单位:毫秒。默认为 15000,即 15 秒 misfireThreshold: 60000 # misfire 阀值,单位:毫秒。 # 线程池相关配置 threadPool: threadCount: 25 # 线程池大小。默认为 10 。 threadPriority: 5 # 线程优先级 class: org.quartz.simpl.SimpleThreadPool # 线程池类型 jdbc: # 使用 JDBC 的 JobStore 的时候,JDBC 的配置 initialize-schema: NEVER # 是否自动使用 SQL 初始化 Quartz 表结构。这里设置成 never ,我们手动创建表结构。 --- #################### 消息队列相关 #################### # rocketmq 配置项,对应 RocketMQProperties 配置类 rocketmq: name-server: 127.0.0.1:9876 # RocketMQ Namesrv spring: # RabbitMQ 配置项,对应 RabbitProperties 配置类 rabbitmq: host: 127.0.0.1 # RabbitMQ 服务的地址 port: 5672 # RabbitMQ 服务的端口 username: guest # RabbitMQ 服务的账号 password: guest # RabbitMQ 服务的密码 # Kafka 配置项,对应 KafkaProperties 配置类 kafka: bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔 --- #################### 服务保障相关配置 #################### # Lock4j 配置项 lock4j: acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒 expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒 --- #################### 监控相关配置 #################### # Actuator 监控端点的配置项 management: endpoints: web: base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator exposure: include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。 # Spring Boot Admin 配置项 spring: boot: admin: # Spring Boot Admin Client 客户端的相关配置 client: url: http://127.0.0.1:${server.port}/${spring.boot.admin.context-path} # 设置 Spring Boot Admin Server 地址 instance: service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME] # Spring Boot Admin Server 服务端的相关配置 context-path: /admin # 配置 Spring # 日志文件配置 logging: file: name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径 --- #################### 微信公众号相关配置 #################### wx: # 参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md 文档 mp: # 公众号配置(必填) app-id: wxdbdbc123c8c30b45 secret: 3312eb2026a006bd0cc39af3021ef9c5 # 存储配置,解决 AccessToken 的跨节点的共享 config-storage: type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取 key-prefix: wx # Redis Key 的前缀 http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台 miniapp: # 小程序配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md 文档 appid: wx001e2dc50bf532df secret: d22aa6a98472ae0b5ee6dd9b7807520c config-storage: type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取 key-prefix: wa # Redis Key 的前缀 http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台 --- #################### yshop相关配置 #################### # yshop配置项,设置当前项目所有自定义的配置 yshop: xss: enable: false exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系 - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 pay: order-notify-url: http://yshop.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址 refund-notify-url: http://yshop.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址 demo: true # 开启演示模式 tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc file-path: /www/wwwroot/drink/file/ h5: https://www.yixiang.co/h5 justauth: enabled: true type: DINGTALK: # 钉钉 client-id: dingvrnreaje3yqvzhxg client-secret: i8E6iZyDvZj51JIb0tYsYfVQYOks9Cq1lgryEjFRqC79P3iJcrxEwT6Qk2QvLrLI ignore-check-redirect-uri: true WECHAT_ENTERPRISE: # 企业微信 client-id: wwd411c69a39ad2e54 client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw agent-id: 1000004 ignore-check-redirect-uri: true # noinspection SpringBootApplicationYaml WECHAT_MINI_APP: # 微信小程序 client-id: ${wx.miniapp.appid} client-secret: ${wx.miniapp.secret} ignore-check-redirect-uri: true ignore-check-state: true # 微信小程序,不会使用到 state,所以不进行校验 WECHAT_MP: # 微信公众号 client-id: ${wx.mp.app-id} client-secret: ${wx.mp.secret} ignore-check-redirect-uri: true cache: type: REDIS prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE:: timeout: 24h # 超时时长,目前只对 Redis 缓存生效,默认 3 分钟 ================================================ FILE: yshop-drink-boot3/yshop-server/src/main/resources/application-local.yaml ================================================ server: port: 48081 --- #################### 数据库相关配置 #################### spring: # 数据源配置项 autoconfigure: exclude: - org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration # 默认 local 环境,不开启 Quartz 的自动配置 - de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration # 禁用 Spring Boot Admin 的 Server 的自动配置 - de.codecentric.boot.admin.server.ui.config.AdminServerUiAutoConfiguration # 禁用 Spring Boot Admin 的 Server UI 的自动配置 - de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration # 禁用 Spring Boot Admin 的 Client 的自动配置 datasource: druid: # Druid 【监控】相关的全局配置 web-stat-filter: enabled: true stat-view-servlet: enabled: true allow: # 设置白名单,不填则允许所有访问 url-pattern: /druid/* login-username: # 控制台管理用户名和密码 login-password: filter: stat: enabled: true log-slow-sql: true # 慢 SQL 记录 slow-sql-millis: 100 merge-sql: true wall: config: multi-statement-allow: true dynamic: # 多数据源配置 druid: # Druid 【连接池】相关的全局配置 initial-size: 1 # 初始连接数 min-idle: 1 # 最小连接池数量 max-active: 20 # 最大连接池数量 max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒 time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒 max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒 validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 test-while-idle: true test-on-borrow: false test-on-return: false primary: master datasource: master: url: jdbc:mysql://127.0.0.1:3306/yixiang-drink-open?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 username: root password: root slave: # 模拟从库,可根据自己需要修改 lazy: true # 开启懒加载,保证启动速度 url: jdbc:mysql://127.0.0.1:3306/yixiang-drink-open?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true username: root password: root # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 data: redis: host: 127.0.0.1 # 地址 port: 6379 # 端口 database: 0 # 数据库索引 # password: dev # 密码,建议生产环境开启 --- #################### 定时任务相关配置 #################### # Quartz 配置项,对应 QuartzProperties 配置类 spring: quartz: auto-startup: true # 本地开发环境,尽量不要开启 Job scheduler-name: schedulerName # Scheduler 名字。默认为 schedulerName job-store-type: jdbc # Job 存储器类型。默认为 memory 表示内存,可选 jdbc 使用数据库。 wait-for-jobs-to-complete-on-shutdown: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true properties: # 添加 Quartz Scheduler 附加属性,更多可以看 http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html 文档 org: quartz: # Scheduler 相关配置 scheduler: instanceName: schedulerName instanceId: AUTO # 自动生成 instance ID # JobStore 相关配置 jobStore: # JobStore 实现类。可见博客:https://blog.csdn.net/weixin_42458219/article/details/122247162 class: org.springframework.scheduling.quartz.LocalDataSourceJobStore isClustered: true # 是集群模式 clusterCheckinInterval: 15000 # 集群检查频率,单位:毫秒。默认为 15000,即 15 秒 misfireThreshold: 60000 # misfire 阀值,单位:毫秒。 # 线程池相关配置 threadPool: threadCount: 25 # 线程池大小。默认为 10 。 threadPriority: 5 # 线程优先级 class: org.quartz.simpl.SimpleThreadPool # 线程池类型 jdbc: # 使用 JDBC 的 JobStore 的时候,JDBC 的配置 initialize-schema: NEVER # 是否自动使用 SQL 初始化 Quartz 表结构。这里设置成 never ,我们手动创建表结构。 --- #################### 消息队列相关 #################### # rocketmq 配置项,对应 RocketMQProperties 配置类 rocketmq: name-server: 127.0.0.1:9876 # RocketMQ Namesrv spring: # RabbitMQ 配置项,对应 RabbitProperties 配置类 rabbitmq: host: 127.0.0.1 # RabbitMQ 服务的地址 port: 5672 # RabbitMQ 服务的端口 username: rabbit # RabbitMQ 服务的账号 password: rabbit # RabbitMQ 服务的密码 # Kafka 配置项,对应 KafkaProperties 配置类 kafka: bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔 --- #################### 服务保障相关配置 #################### # Lock4j 配置项 lock4j: acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒 expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒 --- #################### 监控相关配置 #################### # Actuator 监控端点的配置项 management: endpoints: web: base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator exposure: include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。 # Spring Boot Admin 配置项 spring: boot: admin: # Spring Boot Admin Client 客户端的相关配置 client: url: http://127.0.0.1:${server.port}/${spring.boot.admin.context-path} # 设置 Spring Boot Admin Server 地址 instance: service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME] # Spring Boot Admin Server 服务端的相关配置 context-path: /admin # 配置 Spring # 日志文件配置 logging: file: name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径 level: # 配置自己写的 MyBatis Mapper 打印日志 co.yixiang.yshop.module.bpm.dal.mysql: debug co.yixiang.yshop.module.infra.dal.mysql: debug co.yixiang.yshop.module.infra.dal.mysql.logger.ApiErrorLogMapper: INFO # 配置 ApiErrorLogMapper 的日志级别为 info,避免和 GlobalExceptionHandler 重复打印 co.yixiang.yshop.module.infra.dal.mysql.job.JobLogMapper: INFO # 配置 JobLogMapper 的日志级别为 info co.yixiang.yshop.module.infra.dal.mysql.file.FileConfigMapper: INFO # 配置 FileConfigMapper 的日志级别为 info co.yixiang.yshop.module.pay.dal.mysql: debug co.yixiang.yshop.module.pay.dal.mysql.notify.PayNotifyTaskMapper: INFO # 配置 PayNotifyTaskMapper 的日志级别为 info co.yixiang.yshop.module.system.dal.mysql: debug co.yixiang.yshop.module.system.dal.mysql.sms.SmsChannelMapper: INFO # 配置 SmsChannelMapper 的日志级别为 info co.yixiang.yshop.module.tool.dal.mysql: debug co.yixiang.yshop.module.member.dal.mysql: debug co.yixiang.yshop.module.order.dal.mysql: debug co.yixiang.yshop.module.product.dal.mysql: debug co.yixiang.yshop.module.store.dal.mysql: debug co.yixiang.yshop.module.shop.dal.mysql: debug co.yixiang.yshop.module.coupon.dal.mysql: debug org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO yshop:先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示 debug: false --- #################### 微信公众号、小程序相关配置 #################### wx: mp: # 公众号配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md 文档 app-id: wxdbdbc123c8c30b45 # 测试号(自己的) secret: 3312eb2026a006bd0cc39af3021ef9c5 # 存储配置,解决 AccessToken 的跨节点的共享 config-storage: type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取 key-prefix: wx # Redis Key 的前缀 http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台 miniapp: # 小程序配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md 文档 appid: wx001e2dc50bf532df # wenhualian的接口测试号 secret: d22aa6a98472ae0b5ee6dd9b7807520c config-storage: type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取 key-prefix: wa # Redis Key 的前缀 http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台 --- #################### yshop相关配置 #################### # yshop配置项,设置当前项目所有自定义的配置 yshop: captcha: enable: false # 本地环境,暂时关闭图片验证码,方便登录等接口的测试; security: mock-enable: true xss: enable: false exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系 - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 pay: order-notify-url: http://yshop.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址 refund-notify-url: http://yshop.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址 access-log: # 访问日志的配置项 enable: false demo: false # 关闭演示模式 tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc file-path: /Users/hupeng/drink/file/ h5: http://localhost:80 justauth: enabled: true type: DINGTALK: # 钉钉 client-id: dingvrnreaje3yqvzhxg client-secret: i8E6iZyDvZj51JIb0tYsYfVQYOks9Cq1lgryEjFRqC79P3iJcrxEwT6Qk2QvLrLI ignore-check-redirect-uri: true WECHAT_ENTERPRISE: # 企业微信 client-id: wwd411c69a39ad2e54 client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw agent-id: 1000004 ignore-check-redirect-uri: true # noinspection SpringBootApplicationYaml WECHAT_MINI_APP: # 微信小程序 client-id: ${wx.miniapp.appid} client-secret: ${wx.miniapp.secret} ignore-check-redirect-uri: true ignore-check-state: true # 微信小程序,不会使用到 state,所以不进行校验 WECHAT_MP: # 微信公众号 client-id: ${wx.mp.app-id} client-secret: ${wx.mp.secret} ignore-check-redirect-uri: true cache: type: REDIS prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE:: timeout: 24h # 超时时长,目前只对 Redis 缓存生效,默认 3 分钟 ================================================ FILE: yshop-drink-boot3/yshop-server/src/main/resources/application.yaml ================================================ spring: application: name: yshop-server profiles: active: local main: allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。 # Servlet 配置 servlet: # 文件上传相关配置项 multipart: max-file-size: 16MB # 单个文件大小 max-request-size: 32MB # 设置总上传的文件大小 mvc: pathmatch: matching-strategy: ANT_PATH_MATCHER # 解决 SpringFox 与 SpringBoot 2.6.x 不兼容的问题,参见 SpringFoxHandlerProviderBeanPostProcessor 类 # throw-exception-if-no-handler-found: true # 404 错误时抛出异常,方便统一处理 # static-path-pattern: /static/** # 静态资源路径; 注意:如果不配置,则 throw-exception-if-no-handler-found 不生效!!! TODO yshop:不能配置,会导致 swagger 不生效 # Jackson 配置项 jackson: serialization: write-dates-as-timestamps: true # 设置 Date 的格式,使用时间戳 write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401 write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳 fail-on-empty-beans: false # 允许序列化无属性的 Bean # Cache 配置项 cache: type: REDIS redis: time-to-live: 1h # 设置过期时间为 1 小时 --- #################### 接口文档配置 #################### springdoc: api-docs: enabled: true path: /v3/api-docs swagger-ui: enabled: true path: /swagger-ui default-flat-param-object: true # 参见 https://doc.xiaominfo.com/docs/faq/v4/knife4j-parameterobject-flat-param 文档 knife4j: enable: true setting: language: zh_cn # 工作流 Flowable 配置 #flowable: # # 1. false: 默认值,Flowable 启动时,对比数据库表中保存的版本,如果不匹配。将抛出异常 # # 2. true: 启动时会对数据库中所有表进行更新操作,如果表存在,不做处理,反之,自动创建表 # # 3. create_drop: 启动时自动创建表,关闭时自动删除表 # # 4. drop_create: 启动时,删除旧表,再创建新表 # database-schema-update: true # 设置为 false,可通过 https://github.com/flowable/flowable-sql 初始化 # db-history-used: true # flowable6 默认 true 生成信息表,无需手动设置 # check-process-definitions: false # 设置为 false,禁用 /resources/processes 自动部署 BPMN XML 流程 # history-level: audit # full:保存历史数据的最高级别,可保存全部流程相关细节,包括流程流转各节点参数 # MyBatis Plus 的配置项 mybatis-plus: configuration: map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。 global-config: db-config: id-type: NONE # “智能”模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。 # id-type: AUTO # 自增 ID,适合 MySQL 等直接自增的数据库 # id-type: INPUT # 用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库 # id-type: ASSIGN_ID # 分配 ID,默认使用雪花算法。注意,Oracle、PostgreSQL、Kingbase、DB2、H2 数据库时,需要去除实体类上的 @KeySequence 注解 logic-delete-value: 1 # 逻辑已删除值(默认为 1) logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) banner: false # 关闭控制台的 Banner 打印 type-aliases-package: ${yshop.info.base-package}.module.*.dal.dataobject encryptor: password: XDV71a+xqStEA3WH # 加解密的秘钥,可使用 https://www.imaegoo.com/2020/aes-key-generator/ 网站生成 mybatis-plus-join: banner: false # 是否打印 mybatis plus join banner,默认true sub-table-logic: true # 全局启用副表逻辑删除,默认true。关闭后关联查询不会加副表逻辑删除 ms-cache: true # 拦截器MappedStatement缓存,默认 true table-alias: t # 表别名(默认 t) logic-del-type: on # 副表逻辑删除条件的位置,支持 WHERE、ON,默认 ON # Spring Data Redis 配置 spring: data: redis: repositories: enabled: false # 项目未使用到 Spring Data Redis 的 Repository,所以直接禁用,保证启动速度 # VO 转换(数据翻译)相关 easy-trans: is-enable-global: true # 启用全局翻译(拦截所有 SpringMVC ResponseBody 进行自动翻译 )。如果对于性能要求很高可关闭此配置,或通过 @IgnoreTrans 忽略某个接口 is-enable-cloud: false # 禁用 TransType.RPC 微服务模式 --- #################### 验证码相关配置 #################### aj: captcha: jigsaw: classpath:images/jigsaw # 滑动验证,底图路径,不配置将使用默认图片;以 classpath: 开头,取 resource 目录下路径 pic-click: classpath:images/pic-click # 滑动验证,底图路径,不配置将使用默认图片;以 classpath: 开头,取 resource 目录下路径 cache-type: redis # 缓存 local/redis... cache-number: 1000 # local 缓存的阈值,达到这个值,清除缓存 timing-clear: 180 # local定时清除过期缓存(单位秒),设置为0代表不执行 type: blockPuzzle # 验证码类型 default两种都实例化。 blockPuzzle 滑块拼图 clickWord 文字点选 water-mark: yshop # 右下角水印文字(我的水印),可使用 https://tool.chinaz.com/tools/unicode.aspx 中文转 Unicode,Linux 可能需要转 unicode interference-options: 0 # 滑动干扰项(0/1/2) req-frequency-limit-enable: false # 接口请求次数一分钟限制是否开启 true|false req-get-lock-limit: 5 # 验证失败 5 次,get接口锁定 req-get-lock-seconds: 10 # 验证失败后,锁定时间间隔 req-get-minute-limit: 30 # get 接口一分钟内请求数限制 req-check-minute-limit: 60 # check 接口一分钟内请求数限制 req-verify-minute-limit: 60 # verify 接口一分钟内请求数限制 --- #################### 消息队列相关 #################### # rocketmq 配置项,对应 RocketMQProperties 配置类 rocketmq: # Producer 配置项 producer: group: ${spring.application.name}_PRODUCER # 生产者分组 spring: # Kafka 配置项,对应 KafkaProperties 配置类 kafka: # Kafka Producer 配置项 producer: acks: 1 # 0-不应答。1-leader 应答。all-所有 leader 和 follower 应答。 retries: 3 # 发送失败时,重试发送的次数 value-serializer: org.springframework.kafka.support.serializer.JsonSerializer # 消息的 value 的序列化 # Kafka Consumer 配置项 consumer: auto-offset-reset: earliest # 设置消费者分组最初的消费进度为 earliest 。可参考博客 https://blog.csdn.net/lishuangzhe7047/article/details/74530417 理解 value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer properties: spring.json.trusted.packages: '*' # Kafka Consumer Listener 监听器配置 listener: missing-topics-fatal: false # 消费监听接口监听的主题不存在时,默认会报错。所以通过设置为 false ,解决报错 --- #################### yshop相关配置 #################### yshop: info: version: 3.0.0 base-package: co.yixiang.yshop isActive: true web: admin-ui: url: http://dashboard.yshop.yixiang.co # Admin 管理后台 UI 的地址 security: permit-all_urls: - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录 websocket: enable: true # websocket的开关 path: /infra/ws # 路径 sender-type: local # 消息发送的类型,可选值为 local、redis、rocketmq、kafka、rabbitmq sender-rocketmq: topic: ${spring.application.name}-websocket # 消息发送的 RocketMQ Topic consumer-group: ${spring.application.name}-websocket-consumer # 消息发送的 RocketMQ Consumer Group sender-rabbitmq: exchange: ${spring.application.name}-websocket-exchange # 消息发送的 RabbitMQ Exchange queue: ${spring.application.name}-websocket-queue # 消息发送的 RabbitMQ Queue sender-kafka: topic: ${spring.application.name}-websocket # 消息发送的 Kafka Topic consumer-group: ${spring.application.name}-websocket-consumer # 消息发送的 Kafka Consumer Group swagger: title: yshop意象点餐系统 description: 提供管理后台、用户 App 的所有功能 version: ${yshop.info.version} url: ${yshop.web.admin-ui.url} email: yshop@yixiang.co license: MIT license-url: https://gitee.com/guchengwuyue/yshop-drink captcha: enable: true # 验证码的开关,默认为 true codegen: base-package: ${yshop.info.base-package} db-schemas: ${spring.datasource.dynamic.datasource.master.name} front-type: 10 # 前端模版的类型,参见 CodegenFrontTypeEnum 枚举类 tenant: # 多租户相关配置项 enable: false ignore-urls: - /admin-api/system/tenant/get-id-by-name # 基于名字获取租户,不许带租户编号 - /admin-api/system/tenant/get-by-website # 基于域名获取租户,不许带租户编号 - /admin-api/system/captcha/get # 获取图片验证码,和租户无关 - /admin-api/system/captcha/check # 校验图片验证码,和租户无关 - /admin-api/infra/file/*/get/** # 获取图片,和租户无关 - /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号 - /admin-api/pay/notify/** # 支付回调通知,不携带租户编号 - /jmreport/* # 积木报表,无法携带租户编号 - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,无法携带租户编号 - /admin-api/file/qrcode/** # 二维码不需要 - /app-api/order/notify/* # 支付回调通知,不携带租户编号 ignore-tables: - system_tenant - system_tenant_package - system_dict_data - system_dict_type - system_error_code - system_menu - system_sms_channel - system_sms_template - system_sms_log - system_sensitive_word - system_oauth2_client - system_mail_account - system_mail_template - system_mail_log - system_notify_template - infra_codegen_column - infra_codegen_table - infra_config - infra_file_config - infra_file - infra_file_content - infra_job - infra_job_log - infra_job_log - infra_data_source_config - yshop_store_product_attr - yshop_store_product_attr_result - yshop_store_product_attr_value - yshop_store_order_cart_info - yshop_store_order_status - yshop_order_number - yshop_qrcode sms-code: # 短信验证码相关的配置项 expire-times: 10m send-frequency: 1m send-maximum-quantity-per-day: 10 begin-code: 9999 # 这里配置 9999 的原因是,测试方便。 end-code: 9999 # 这里配置 9999 的原因是,测试方便。 trade: order: app-id: 1 # 商户编号 pay-expire-time: 2h # 支付的过期时间 receive-expire-time: 14d # 收货的过期时间 comment-expire-time: 7d # 评论的过期时间 express: client: kd_niao kd-niao: api-key: cb022f1e-48f1-4c4a-a723-9001ac9676b8 business-id: 1809751 kd100: key: pLXUGAwK5305 customer: E77DF18BE109F454A5CD319E44BF5177 debug: false # 积木报表配置 jeecg: jmreport: saas-mode: tenant ================================================ FILE: yshop-drink-boot3/yshop-server/src/main/resources/logback-spring.xml ================================================       ${PATTERN_DEFAULT} ${PATTERN_DEFAULT} ${LOG_FILE} ${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz} ${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false} ${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB} ${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0} ${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-30} 0 256 ${PATTERN_DEFAULT} ================================================ FILE: yshop-drink-boot3/yshop-server/src/test/java/co/yixiang/yshop/ProjectReactor.java ================================================ package co.yixiang.yshop; import cn.hutool.core.io.FileTypeUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.StrUtil; import co.yixiang.yshop.framework.common.util.collection.SetUtils; import lombok.extern.slf4j.Slf4j; import java.io.File; import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.Set; import java.util.regex.Matcher; import java.util.stream.Collectors; import static java.io.File.separator; /** * 项目修改器,一键替换 Maven 的 groupId、artifactId,项目的 package 等 *

* 通过修改 groupIdNew、artifactIdNew、projectBaseDirNew 三个变量 * * @author yshop */ @Slf4j public class ProjectReactor { private static final String GROUP_ID = "co.yixiang.boot"; private static final String ARTIFACT_ID = "yshop"; private static final String PACKAGE_NAME = "co.yixiang.yshop"; private static final String TITLE = "意象商城管理系统"; /** * 白名单文件,不进行重写,避免出问题 */ private static final Set WHITE_FILE_TYPES = SetUtils.asSet("gif", "jpg", "svg", "png", // 图片 "eot", "woff2", "ttf", "woff", // 字体 "xdb"); // IP 库 public static void main(String[] args) { long start = System.currentTimeMillis(); String projectBaseDir = getProjectBaseDir(); log.info("[main][原项目路劲改地址 ({})]", projectBaseDir); // ========== 配置,需要你手动修改 ========== String groupIdNew = "co.yixiang.boot"; String artifactIdNew = "yshop"; String packageNameNew = "co.yixiang.yshop"; String titleNew = "意象商城管理系统"; String projectBaseDirNew = projectBaseDir + "-new"; // 一键改名后,“新”项目所在的目录 log.info("[main][检测新项目目录 ({})是否存在]", projectBaseDirNew); if (FileUtil.exist(projectBaseDirNew)) { log.error("[main][新项目目录检测 ({})已存在,请更改新的目录!程序退出]", projectBaseDirNew); return; } // 如果新目录中存在 PACKAGE_NAME,ARTIFACT_ID 等关键字,路径会被替换,导致生成的文件不在预期目录 if (StrUtil.containsAny(projectBaseDirNew, PACKAGE_NAME, ARTIFACT_ID, StrUtil.upperFirst(ARTIFACT_ID))) { log.error("[main][新项目目录 `projectBaseDirNew` 检测 ({}) 存在冲突名称「{}」或者「{}」,请更改新的目录!程序退出]", projectBaseDirNew, PACKAGE_NAME, ARTIFACT_ID); return; } log.info("[main][完成新项目目录检测,新项目路径地址 ({})]", projectBaseDirNew); // 获得需要复制的文件 log.info("[main][开始获得需要重写的文件,预计需要 10-20 秒]"); Collection files = listFiles(projectBaseDir); log.info("[main][需要重写的文件数量:{},预计需要 15-30 秒]", files.size()); // 写入文件 files.forEach(file -> { // 如果是白名单的文件类型,不进行重写,直接拷贝 String fileType = getFileType(file); if (WHITE_FILE_TYPES.contains(fileType)) { copyFile(file, projectBaseDir, projectBaseDirNew, packageNameNew, artifactIdNew); return; } // 如果非白名单的文件类型,重写内容,在生成文件 String content = replaceFileContent(file, groupIdNew, artifactIdNew, packageNameNew, titleNew); writeFile(file, content, projectBaseDir, projectBaseDirNew, packageNameNew, artifactIdNew); }); log.info("[main][重写完成]共耗时:{} 秒", (System.currentTimeMillis() - start) / 1000); } private static String getProjectBaseDir() { String baseDir = System.getProperty("user.dir"); if (StrUtil.isEmpty(baseDir)) { throw new NullPointerException("项目基础路径不存在"); } return baseDir; } private static Collection listFiles(String projectBaseDir) { Collection files = FileUtil.loopFiles(projectBaseDir); // 移除 IDEA、Git 自身的文件、Node 编译出来的文件 files = files.stream() .filter(file -> !file.getPath().contains(separator + "target" + separator) && !file.getPath().contains(separator + "node_modules" + separator) && !file.getPath().contains(separator + ".idea" + separator) && !file.getPath().contains(separator + ".git" + separator) && !file.getPath().contains(separator + "dist" + separator) && !file.getPath().contains(".iml") && !file.getPath().contains(".html.gz")) .collect(Collectors.toList()); return files; } private static String replaceFileContent(File file, String groupIdNew, String artifactIdNew, String packageNameNew, String titleNew) { String content = FileUtil.readString(file, StandardCharsets.UTF_8); // 如果是白名单的文件类型,不进行重写 String fileType = getFileType(file); if (WHITE_FILE_TYPES.contains(fileType)) { return content; } // 执行文件内容都重写 return content.replaceAll(GROUP_ID, groupIdNew) .replaceAll(PACKAGE_NAME, packageNameNew) .replaceAll(ARTIFACT_ID, artifactIdNew) // 必须放在最后替换,因为 ARTIFACT_ID 太短! .replaceAll(StrUtil.upperFirst(ARTIFACT_ID), StrUtil.upperFirst(artifactIdNew)) .replaceAll(TITLE, titleNew); } private static void writeFile(File file, String fileContent, String projectBaseDir, String projectBaseDirNew, String packageNameNew, String artifactIdNew) { String newPath = buildNewFilePath(file, projectBaseDir, projectBaseDirNew, packageNameNew, artifactIdNew); FileUtil.writeUtf8String(fileContent, newPath); } private static void copyFile(File file, String projectBaseDir, String projectBaseDirNew, String packageNameNew, String artifactIdNew) { String newPath = buildNewFilePath(file, projectBaseDir, projectBaseDirNew, packageNameNew, artifactIdNew); FileUtil.copyFile(file, new File(newPath)); } private static String buildNewFilePath(File file, String projectBaseDir, String projectBaseDirNew, String packageNameNew, String artifactIdNew) { return file.getPath().replace(projectBaseDir, projectBaseDirNew) // 新目录 .replace(PACKAGE_NAME.replaceAll("\\.", Matcher.quoteReplacement(separator)), packageNameNew.replaceAll("\\.", Matcher.quoteReplacement(separator))) .replace(ARTIFACT_ID, artifactIdNew) // .replaceAll(StrUtil.upperFirst(ARTIFACT_ID), StrUtil.upperFirst(artifactIdNew)); } private static String getFileType(File file) { return file.length() > 0 ? FileTypeUtil.getType(file) : ""; } } ================================================ FILE: yshop-drink-uniapp-vue3/.gitignore ================================================ # See https://help.github.com/ignore-files/ for more about ignoring files. # dependencies /node_modules # testing /coverage # production /build /unpackage /dist # misc .DS_Store .cache npm-debug.log* yarn-debug.log* yarn-error.log* .tmp* .svn .tags *.sublime-* sftp-config.json logs *.log .idea* .yo-rc.json *.swo *.swp /dist /deps yarn.lock dev-stats.json .vscode .history ================================================ FILE: yshop-drink-uniapp-vue3/.hbuilderx/launch.json ================================================ { // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/ // launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数 "version" : "0.0", "configurations" : [ { "app-plus" : { "launchtype" : "local" }, "default" : { "launchtype" : "local" }, "mp-weixin" : { "launchtype" : "local" }, "type" : "uniCloud" }, { "playground" : "standard", "type" : "uni-app:app-ios" }, { "playground" : "custom", "type" : "uni-app:app-android" } ] } ================================================ FILE: yshop-drink-uniapp-vue3/App.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/api/address.js ================================================ import api from './api' // 删除用户地址 export function addressDelete(data) { return api.post(`/address/del/${data.id}`, undefined, { login: true }) } // 设置默认地址 export function shopGetDistanceFromLocation(data) { return api.post(`/address/getDistanceFromLocation`, data, { login: true }) } // 添加或修改地址 export function getAddressAddAndEdit(data) { return api.post(`/address/addAndEdit`, data, { login: true }) } // 用户地址列表 l export function addressAll(data) { return api.get(`/address/list`, data, { login: true }) } ================================================ FILE: yshop-drink-uniapp-vue3/api/api.js ================================================ // #ifdef H5 // h5端 import Fly from 'flyio/dist/npm/fly' // #endif // #ifdef APP-PLUS // app端 import Fly from 'flyio/dist/npm/wx' // #endif // #ifdef MP-WEIXIN import Fly from 'flyio/dist/npm/wx' // #endif import { handleLoginFailure } from '@/utils' import { isWeixin } from '@/utils/util' import { VUE_APP_API_URL } from '@/config' import cookie from '@/utils/cookie' import { replace } from '@/utils/router' const fly = new Fly() fly.config.baseURL = VUE_APP_API_URL fly.interceptors.response.use( response => { // console.log(response) // 定时刷新access-token return response }, error => { if (error.toString() == 'Error: Network Error') { handleLoginFailure() return Promise.reject({ msg: '未登录', toLogin: true }) } if (error.status == 401) { handleLoginFailure() return Promise.reject({ msg: '未登录', toLogin: true }) } if (error.response.data.status == 5109) { uni.showToast({ title: error.response.data.msg, icon: 'none', duration: 2000, }) } return Promise.reject(error) } ) const defaultOpt = { login: true } function baseRequest(options) { const token = cookie.get('accessToken') console.log('--> % token % token:\n', token) options.headers = { ...options.headers, } // if (options.login === true) { options.headers = { ...options.headers, Authorization: 'Bearer ' + token, } // } // 结构请求需要的参数 const { url, params, data, login, ...option } = options // 发起请求 return fly .request(url, params || data, { ...option, }) .then(res => { const data = res.data || {} //console.log('res.status:',res) // console.log('res.code:',res.code) // #ifdef H5 if (res.data.code == 1004004002) { if(isWeixin()){ const url = cookie.get('index_url') console.log('redirect_uri:',url) //const url = `${location.origin}/h5/#/pages/index/index` location.href = url return } } // #endif if (res.status !== 200) { return Promise.reject({ msg: '请求失败', res, data }) } if (data.code == 401) { uni.hideLoading() handleLoginFailure() uni.showToast({ title: data.msg, icon: 'none', duration: 2000, }) return Promise.reject({ msg: data.msg, res, data }) } if (data.code != 0) { uni.showToast({ title: data.msg, icon: 'none', duration: 2000, }) return Promise.reject({ data, res }) } return Promise.resolve(data.data, res) // if ([401, 403].indexOf(data.status) !== -1) { // handleLoginFailure() // return Promise.reject({ msg: res.data.msg, res, data, toLogin: true }) // } else if (data.status === 200) { // return Promise.resolve(data, res) // } else if (data.status == 5101) { // return Promise.reject({ msg: res.data.msg, res, data }) // } else { // return Promise.reject({ msg: res.data.msg, res, data }) // } }) } /** * http 请求基础类 * 参考文档 https://www.kancloud.cn/yunye/axios/234845 * */ const request = ['post', 'put', 'patch'].reduce((request, method) => { /** * * @param url string 接口地址 * @param data object get参数 * @param options object axios 配置项 * @returns {AxiosPromise} */ request[method] = (url, data = {}, options = {}) => { console.log(url, data) return baseRequest(Object.assign({ url, data, method }, defaultOpt, options)) } return request }, {}) ;['get', 'delete', 'head'].forEach(method => { /** * * @param url string 接口地址 * @param params object get参数 * @param options object axios 配置项 * @returns {AxiosPromise} */ request[method] = (url, params = {}, options = {}) => { return baseRequest(Object.assign({ url, params, method }, defaultOpt, options)) } }) export default request ================================================ FILE: yshop-drink-uniapp-vue3/api/auth.js ================================================ import api from './api' /** * 使用手机 + 验证码登录 */ export function userLogin(data) { return api.post('/member/auth/sms-login', data, { login: false }) } /** * 使用手机 + 验证码登录 member/auth/send-sms-code */ export function smsSend(data) { return api.post('/member/auth/send-sms-code', data, { login: false }) } /** * 小程序 member/auth/auth-miniapp-login */ export function userLoginForWechatMini(data) { return api.post('/member/auth/auth-miniapp-login', data, { login: false }) } /** * userAuthSession */ export function userAuthSession(data) { return api.post('/member/auth/auth-session', data, { login: false }) } /** * wechatAuth */ export function wechatAuth(data) { return api.get('/member/auth/auth-wechat-login', data, { login: false }) } ================================================ FILE: yshop-drink-uniapp-vue3/api/coupon.js ================================================ import api from './api' /** * couponReceive */ export function couponReceive(data) { return api.post('/coupon/receive', data, { login: false }) } /** * couponMine */ export function couponMine(data) { return api.get(`/coupon/my`, data, { login: false }) } /** * couponIndex let couponCount = (params = {}) => vm.$u.get('/coupon/count', params); */ export function couponIndexApi(data) { return api.get(`/coupon/not`, data, { login: false }) } /** * couponCount */ export function couponCount(data) { return api.get(`/coupon/count`, data, { login: false }) } ================================================ FILE: yshop-drink-uniapp-vue3/api/goods.js ================================================ import api from './api' /** * 获得banner列表 */ export function shopNearby(data) { return api.get('/store/nearby', data, { login: false }) } /** * 获取首页信息 */ export function menuGoods(data) { return api.get('/product/products', data, { login: false }) } ================================================ FILE: yshop-drink-uniapp-vue3/api/market.js ================================================ import api from './api' /** * shopGetList */ export function shopGetList(data) { return api.get('/store/list', data, { login: false }) } export function menuAds(data) { return api.get('/ad/list', data, { login: false }) } ================================================ FILE: yshop-drink-uniapp-vue3/api/order.js ================================================ import api from './api' /** * 订单列表 */ export function orderTakeFoods(data) { return api.get('/order/list', data, { login: false }) } /** * 订单创建 */ export function orderSubmit(data) { return api.post(`/order/create`, data, { login: false }) } /** * 订单列表 */ export function orderGetOrders(data) { return api.get(`/order/list`, data, { login: false }) } /** * 计算详情 */ export function orderDetail(data) { return api.get(`/order/detail/${data}`, data, { login: false }) } /** * 订单收货 */ export function orderReceive(data) { return api.post(`/order/take`, data, { login: false }) } /** * 订单退款 */ export function orderRefund(data) { return api.post(`/order/refund`, data, { login: false }) } /** * 订单支付 */ export function payUnify(data) { return api.post(`/order/pay`, data, { login: false }) } /** * getWechatConfig */ export function getWechatConfig() { return api.get(`/member/wx-mp/create-jsapi-signature`, { url: location.href }, { login: false }) } ================================================ FILE: yshop-drink-uniapp-vue3/api/score.js ================================================ import api from './api' /** * 详情 */ export function scoreShopDetail(data) { return api.get('/score-product/detail', data, { login: false }) } /** * 列表 */ export function scoreShopIndex(data) { return api.get('/score-product/list', data, { login: false }) } /** * 提交 */ export function scoreShopExchange(data) { return api.post('/score-order/submit', data, { login: false }) } /** * 订单列表 */ export function scoreShopOrder(data) { return api.get('/score-order/list', data, { login: false }) } /** * 订单列表 */ export function scoreShopOrderDetail(data) { return api.get('/score-order/detail', data, { login: false }) } /** * 确认收货 */ export function scoreShopReceive(data) { return api.get('/score-order/take', data, { login: false }) } /** * 查询物流 */ export function getLogistic(data) { return api.get('/express/getLogistic', data, { login: false }) } ================================================ FILE: yshop-drink-uniapp-vue3/api/user.js ================================================ import api from './api' /** * 基本信息 */ export function userGetUserInfo(data) { return api.get('/member/user/get-info', data, { login: true }) } /** * 获取菜单 */ export function mineService(data) { return api.get('/service/list', data, { login: true }) } /** * 获取内容 */ export function mineServiceContent(data) { return api.get('/service/content', data, { login: true }) } /** * save */ export function userEdit(data) { return api.post('/member/user/update-nickname', data, { login: true }) } /** * balanceGetBillList */ export function balanceGetBillList(data) { return api.get('/member/user/getBill', data, { login: true }) } /** * 充值列表 */ export function balanceGetMoneyList(data) { return api.get('/recharge/getMoneyList', data, { login: true }) } /** * 充值 */ export function balanceRecharge(data) { return api.post('/member/user/recharge', data, { login: true }) } ================================================ FILE: yshop-drink-uniapp-vue3/components/blank/blank.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/components/card/card.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/components/city-select/city-select.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/components/container/container.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/components/layout/layout.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/components/list-cell/list-cell.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/components/logo/logo.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/components/modal/modal.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/components/space/space.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/components/upload-file/upload-file.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/components/verification/verification.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/config/index.js ================================================ export const VUE_APP_API_URL = 'http://localhost:48081/app-api' //export const VUE_APP_API_URL = 'https://apidc.yixiang.co/app-api' export const VUE_APP_RESOURCES_URL = 'https://h5.yixiang.co/static' export const VUE_APP_UPLOAD_URL = VUE_APP_API_URL + '/infra/file/upload' export const APP_ID = 'wxdbdbc123c8c30b45' const orderListStatus = {} // -1:申请退款 // -2:退货成功 // 0:待发货; // 1:待收货; // 2:已收货; // 3:待评价; // -1:已退款 export const orderStatus = { 0: '未支付', 1: '待发货', 2: '待收货', 3: '待评价', 4: '已完成', 5: '退款中', 6: '已退款', 7: '退款', } export const orderReStatus = { 0: '等待买家付款', // 1: '等待卖家发货', 1: '卖家已发货', 2: '等待买家待评价', 3: '订单已完成', 4: '订单退款中', 5: '订单已退款', 6: '退款已完成', } // export const orderReStatus = { // 0: '等待买家付款', // 1: '等待卖家发货', // 2: '卖家已发货', // 3: '等待买家待评价', // 4: '订单已完成', // 5: '订单退款中', // 6: '订单已退款', // 7: '退款已完成', // } ================================================ FILE: yshop-drink-uniapp-vue3/hooks/index.js ================================================ export * from './usePage' export * from './useGlobalProperties' ================================================ FILE: yshop-drink-uniapp-vue3/hooks/useGlobalProperties.js ================================================ // mouse.js import { ref, onMounted, onUnmounted, getCurrentInstance } from 'vue' import { onReady, onReachBottom } from '@dcloudio/uni-app' export const useGlobalProperties = () => { const instance = getCurrentInstance() return instance.appContext.app.config.globalProperties } ================================================ FILE: yshop-drink-uniapp-vue3/hooks/usePage.js ================================================ // mouse.js import { ref, onMounted, onUnmounted } from 'vue' import { onReady, onReachBottom } from '@dcloudio/uni-app' export const usePage = getPage => { // 页码,默认为1 const page = ref(1) // 页大小,默认为10 const limit = ref(10) // 关键字 const keyword = ref('') // 类别 const type = ref('') // 分类ID const sid = ref('') // 是否新品,不为空的字符串即可 const news = ref('') // 是否积分兑换商品 const isIntegral = ref('') // 到底了 const loadend = ref(false) // 加载中 const loading = ref(false) const dataList = ref([]) const handleGetDataList = async () => { console.log('--> % handleGetDataList % loading:\n', loading.value) console.log('--> % handleGetDataList % loadend:\n', loadend.value) if (loading.value || loadend.value) return loading.value = true const products = await getPage({ page: page.value, limit: limit.value, keyword: keyword.value, type: type.value, sid: sid.value, news: news.value, isIntegral: isIntegral.value, }) console.log('--> % handleGetDataList % products:\n', products) if (products) { if (products.length <= 0) { loadend.value = true } dataList.value = dataList.value.concat(products) } loading.value = false } const handleRefresh = () => { loadend.value = false loading.value = false dataList.value = [] handleGetDataList() } onReady(() => { console.log('onReady') // handleGetDataList() }) onReachBottom(() => { if (loading.value) return page.value += 1 }) // 通过返回值暴露所管理的状态 return { type, dataList, page, limit, keyword, loading, loadend, refresh: handleRefresh, } } ================================================ FILE: yshop-drink-uniapp-vue3/index.html ================================================

================================================ FILE: yshop-drink-uniapp-vue3/jsconfig.json ================================================ { "compilerOptions": { "baseUrl": "./", "paths": { "@/*": ["./*"], "@/api": ["./api"], "@/utils": ["./utils"] } }, "exclude": ["node_modules", "dist"], "include": ["/**/*"] } ================================================ FILE: yshop-drink-uniapp-vue3/main.js ================================================ /* * @Author: Gaoxs * @Date: 2023-04-07 15:12:06 * @LastEditors: Gaoxs * @Description: */ import util from '@/utils' import App from './App' import { createPinia } from 'pinia' import { createSSRApp } from 'vue' export function createApp() { const app = createSSRApp(App) app.use(util) app.use(createPinia()) return { app, } } ================================================ FILE: yshop-drink-uniapp-vue3/manifest.json ================================================ { "name" : "yshop-miniapp", "appid" : "__UNI__ADC0FB0", "description" : "", "versionName" : "1.0.0", "versionCode" : 1, "transformPx" : false, /* 5+App特有相关 */ "app-plus" : { "usingComponents" : true, "nvueStyleCompiler" : "uni-app", "compilerVersion" : 3, "splashscreen" : { "alwaysShowBeforeRender" : true, "waiting" : true, "autoclose" : true, "delay" : 0 }, /* 模块配置 */ "modules" : { "Payment" : {}, "OAuth" : {} }, /* 应用发布信息 */ "distribute" : { //必选,JSON对象,云端打包配置 "android" : { //可选,JSON对象,Android平台云端打包配置 "packagename" : "", //必填,字符串类型,Android包名 "keystore" : "", //必填,字符串类型,Android签名证书文件路径 "password" : "", //必填,字符串类型,Android签名证书文件的密码 "aliasname" : "", //必填,字符串类型,Android签名证书别名 "schemes" : "", //可选,字符串类型,参考:https://uniapp.dcloud.io/tutorial/app-android-schemes "abiFilters" : [ //可选,字符串数组类型,参考:https://uniapp.dcloud.io/tutorial/app-android-abifilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" ], "permissions" : [ "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "" ], "custompermissions" : false, //可选,Boolean类型,是否自定义Android权限配置 "permissionExternalStorage" : { //可选,JSON对象,Android平台应用启动时申请读写手机存储权限策略 "request" : "always", //必填,字符串类型,申请读写手机存储权限策略,可取值none、once、always "prompt" : "" //可选,字符串类型,当request设置为always值用户拒绝时弹出提示框上的内容 }, "permissionPhoneState" : { //可选,JSON对象,Android平台应用启动时申请读取设备信息权限配置 "request" : "always", //必填,字符串类型,申请读取设备信息权限策略,可取值none、once、always "prompt" : "" //可选,字符串类型,当request设置为always值用户拒绝时弹出提示框上的内容 }, "minSdkVersion" : 21, //可选,数字类型,Android平台最低支持版本,参考:https://uniapp.dcloud.io/tutorial/app-android-minsdkversion "targetSdkVersion" : 30, //可选,数字类型,Android平台目标版本,参考:https://uniapp.dcloud.io/tutorial/app-android-targetsdkversion "packagingOptions" : [ //可选,字符串数组类型,Android平台云端打包时build.gradle的packagingOptions配置项 "doNotStrip '*/armeabi-v7a/*.so'", "merge '**/LICENSE.txt'" ], "jsEngine" : "v8", //可选,字符串类型,uni-app使用的JS引擎,可取值v8、jsc "debuggable" : false, //可选,Boolean类型,是否开启Android调试开关 "locale" : "default", //可选,应用的语言 "forceDarkAllowed" : false, //可选,Boolean类型,是否强制允许暗黑模式 "resizeableActivity" : false, //可选,Boolean类型,是否支持分屏调整窗口大小 "hasTaskAffinity" : false, //可选,Boolean类型,是否设置android:taskAffinity "buildFeatures" : { //(HBuilderX3.5.0+版本支持)可选,JSON对象,Android平台云端打包时build.gradle的buildFeatures配置项 "dataBinding" : false, //可选,Boolean类型,是否设置dataBinding "viewBinding" : false //可选,Boolean类型,是否设置viewBinding } }, "ios" : { //可选,JSON对象,iOS平台云端打包配置 "appid" : "", //必填,字符串类型,iOS平台Bundle ID "mobileprovision" : "", //必填,字符串类型,iOS打包使用的profile文件路径 "p12" : "", //必填,字符串类型,iOS打包使用的证书文件路径 "password" : "", //必填,字符串类型,iOS打包使用的证书密码 "devices" : "iphone", //必填,字符串类型,iOS支持的设备类型,可取值iphone、ipad、universal "urlschemewhitelist" : "baidumap", //可选,字符串类型,应用访问白名单列表,参考:https://uniapp.dcloud.io/tutorial/app-ios-schemewhitelist "urltypes" : "", //可选,字符串类型,参考:https://uniapp.dcloud.io/tutorial/app-ios-schemes "UIBackgroundModes" : "audio", //可选,字符串类型,应用后台运行模式,参考:https://uniapp.dcloud.io/tutorial/app-ios-uibackgroundmodes "frameworks" : [ //可选,字符串数组类型,依赖的系统库,已废弃,推荐使用uni原生插件扩展使用系统依赖库 "CoreLocation.framework" ], "deploymentTarget" : "10.0", //可选,字符串类型,iOS支持的最低版本 "privacyDescription" : { //可选,JSON对象,iOS隐私信息访问的许可描述 "NSPhotoLibraryUsageDescription" : "", //可选,字符串类型,系统相册读取权限描述 "NSPhotoLibraryAddUsageDescription" : "", //可选,字符串类型,系统相册写入权限描述 "NSCameraUsageDescription" : "", //可选,字符串类型,摄像头使用权限描述 "NSMicrophoneUsageDescription" : "", //可选,字符串类型,麦克风使用权限描述 "NSLocationWhenInUseUsageDescription" : "", //可选,字符串类型,运行期访问位置权限描述 "NSLocationAlwaysUsageDescription" : "", //可选,字符串类型,后台运行访问位置权限描述 "NSLocationAlwaysAndWhenInUseUsageDescription" : "", //可选,字符串类型,运行期后后台访问位置权限描述 "NSCalendarsUsageDescription" : "", //可选,字符串类型,使用日历权限描述 "NSContactsUsageDescription" : "", //可选,字符串类型,使用通讯录权限描述 "NSBluetoothPeripheralUsageDescription" : "", //可选,字符串类型,使用蓝牙权限描述 "NSBluetoothAlwaysUsageDescription" : "", //可选,字符串类型,后台使用蓝牙权限描述 "NSSpeechRecognitionUsageDescription" : "", //可选,字符串类型,系统语音识别权限描述 "NSRemindersUsageDescription" : "", //可选,字符串类型,系统提醒事项权限描述 "NSMotionUsageDescription" : "", //可选,字符串类型,使用运动与健康权限描述 "NSHealthUpdateUsageDescription" : "", //可选,字符串类型,使用健康更新权限描述 "NSHealthShareUsageDescription" : "", //可选,字符串类型,使用健康分享权限描述 "NSAppleMusicUsageDescription" : "", //可选,字符串类型,使用媒体资料库权限描述 "NFCReaderUsageDescription" : "", //可选,字符串类型,使用NFC权限描述 "NSHealthClinicalHealthRecordsShareUsageDescription" : "", //可选,字符串类型,访问临床记录权限描述 "NSHomeKitUsageDescription" : "", //可选,字符串类型,访问HomeKit权限描述 "NSSiriUsageDescription" : "", //可选,字符串类型,访问Siri权限描述 "NSFaceIDUsageDescription" : "", //可选,字符串类型,使用FaceID权限描述 "NSLocalNetworkUsageDescription" : "", //可选,字符串类型,访问本地网络权限描述 "NSUserTrackingUsageDescription" : "" //可选,字符串类型,跟踪用户活动权限描述 }, "idfa" : true, //可选,Boolean类型,是否使用广告标识 "capabilities" : {}, //可选,JSON对象,应用的能力配置(Capabilities) "CFBundleName" : "HBuilder", //可选,字符串类型,CFBundleName名称 "validArchitectures" : [ //可选,字符串数组类型,编译时支持的CPU指令,可取值arm64、arm64e、armv7、armv7s、x86_64 "arm64" ], "pushRegisterMode" : "manual", //可选,使用“Push(消息推送)”模块时申请系统推送权限模式,manual表示调用push相关API时申请,其它值表示应用启动时自动申请 "privacyRegisterMode" : "manual", //可选,仅iOS有效,设置为manual表示用户同意隐私政策后才获取idfv,设置为其它值表示应用启动时自动获取 "dSYMs" : false }, "sdkConfigs" : { //可选,JSON对象,三方SDK相关配置 "geolocation" : { //可选,JSON对象,Geolocation(定位)模块三方SDK配置 "system" : { //可选,JSON对象,使用系统定位 "__platform__" : [ "ios", "android" ] //可选,字符串数组类型,支持的平台 }, "amap" : { //可选,JSON对象,使用高德定位SDK配置 "__platform__" : [ "ios", "android" ], //可选,字符串数组类型,支持的平台 "appkey_ios" : "", //必填,字符串类型,iOS平台高德定位appkey "appkey_android" : "" //必填,字符串类型,Android平台高德定位appkey }, "baidu" : { //可选,JSON对象,使用百度定位SDK配置 "__platform__" : [ "ios", "android" ], //可选,字符串数组类型,支持的平台 "appkey_ios" : "", //必填,字符串类型,iOS平台百度定位appkey "appkey_android" : "" //必填,字符串类型,Android平台百度定位appkey } }, "maps" : { //可选,JSON对象,Maps(地图)模块三方SDK配置 "amap" : { //可选,JSON对象,使用高德地图SDK配置 "appkey_ios" : "", //必填,字符串类型,iOS平台高德地图appkey "appkey_android" : "" //必填,字符串类型,Android平台高德地图appkey }, "baidu" : { //可选,JSON对象,使用百度地图SDK配置 "appkey_ios" : "", //必填,字符串类型,iOS平台百度地图appkey "appkey_android" : "" //必填,字符串类型,Android平台百度地图appkey }, "google" : { //可选,JSON对象,使用Google地图SDK配置 "APIKey_ios" : "", //必填,字符串类型,iOS平台Google地图APIKey "APIKey_android" : "" //必填,字符串类型,Android平台Google地图APIKey } }, "oauth" : { //可选,JSON对象,使用苹果登录(Sign in with Apple)SDK配置,无配置参数,仅iOS平台支持 "weixin" : { "appid" : "wx7c84ede33062d1e4", "UniversalLinks" : "https://yixiang.co/app/" } }, "payment" : { "weixin" : { "__platform__" : [ "ios", "android" ], "appid" : "wx7c84ede33062d1e4", "UniversalLinks" : "https://yixiang.co/app/" } }, //可选,JSON对象,使用google支付SDK配置,无配置参数,仅Android平台支持 "push" : { //可选,JSON对象,Push(消息推送)模块三方SDK配置 "unipush" : { //可选,JSON对象,使用UniPush SDK配置,无需手动配置参数,云端打包自动获取配置参数 "icons" : { //可选,JSON对象,推送图标配置 "push" : { //可选,JSON对象,Push图标配置 "ldpi" : "", //可选,字符串类型,普通屏设备推送图标路径,分辨率要求48x48 "mdpi" : "", //可选,字符串类型,大屏设备设备推送图标路径,分辨率要求48x48 "hdpi" : "", //可选,字符串类型,高分屏设备推送图标路径,分辨率要求72x72 "xdpi" : "", //可选,字符串类型,720P高分屏设备推送图标路径,分辨率要求96x96 "xxdpi" : "", //可选,字符串类型,1080P高密度屏幕推送图标路径,分辨率要求144x144 "xxxdpi" : "" //可选,字符串类型,4K屏设备推送图标路径,分辨率要求192x192 }, "smal" : { //可选,JSON对象,Push小图标配置 "ldpi" : "", //可选,字符串类型,普通屏设备推送小图标路径,分辨率要求18x18 "mdpi" : "", //可选,字符串类型,大屏设备设备推送小图标路径,分辨率要求24x24 "hdpi" : "", //可选,字符串类型,高分屏设备推送小图标路径,分辨率要求36x36 "xdpi" : "", //可选,字符串类型,720P高分屏设备推送小图标路径,分辨率要求48x48 "xxdpi" : "", //可选,字符串类型,1080P高密度屏幕推送小图标路径,分辨率要求72x72 "xxxdpi" : "" //可选,字符串类型,4K屏设备推送小图标路径,分辨率要求96x96 } } }, "igexin" : { //可选,JSON对象,使用个推推送SDK配置,**已废弃,推荐使用UniPush,UniPush是个推推送VIP版,功能更强大** "appid" : "", //必填,字符串类型,个推开放平台申请的appid "appkey" : "", //必填,字符串类型,个推开放平台申请的appkey "appsecret" : "", //必填,字符串类型,个推开放平台申请的appsecret "icons" : { //可选,JSON对象,推送图标配置 "push" : { //可选,JSON对象,Push图标配置 "ldpi" : "", //可选,字符串类型,普通屏设备推送图标路径,分辨率要求48x48 "mdpi" : "", //可选,字符串类型,大屏设备设备推送图标路径,分辨率要求48x48 "hdpi" : "", //可选,字符串类型,高分屏设备推送图标路径,分辨率要求72x72 "xdpi" : "", //可选,字符串类型,720P高分屏设备推送图标路径,分辨率要求96x96 "xxdpi" : "", //可选,字符串类型,1080P高密度屏幕推送图标路径,分辨率要求144x144 "xxxdpi" : "" //可选,字符串类型,4K屏设备推送图标路径,分辨率要求192x192 }, "smal" : { //可选,JSON对象,Push小图标配置 "ldpi" : "", //可选,字符串类型,普通屏设备推送小图标路径,分辨率要求18x18 "mdpi" : "", //可选,字符串类型,大屏设备设备推送小图标路径,分辨率要求24x24 "hdpi" : "", //可选,字符串类型,高分屏设备推送小图标路径,分辨率要求36x36 "xdpi" : "", //可选,字符串类型,720P高分屏设备推送小图标路径,分辨率要求48x48 "xxdpi" : "", //可选,字符串类型,1080P高密度屏幕推送小图标路径,分辨率要求72x72 "xxxdpi" : "" //可选,字符串类型,4K屏设备推送小图标路径,分辨率要求96x96 } } } }, "share" : { //可选,JSON对象,Share(分享)模块三方SDK配置 "weixin" : { //可选,JSON对象,使用微信分享SDK配置 "appid" : "", //必填,字符串类型,微信开放平台申请的appid "UniversalLinks" : "" //可选,字符串类型,微信开放平台配置的iOS平台通用链接 }, "qq" : { //可选,JSON对象,使用QQ分享SDK配置 "appid" : "", //必填,字符串类型,QQ开放平台申请的appid "UniversalLinks" : "" //可选,字符串类型,QQ开放平台配置的iOS平台通用链接 }, "sina" : { //可选,JSON对象,使用新浪微博分享SDK配置 "appkey" : "", //必填,字符串类型,新浪微博开放平台申请的appid "redirect_uri" : "", //必填,字符串类型,新浪微博开放平台配置的redirect_uri "UniversalLinks" : "" //可选,字符串类型,新浪微博开放平台配置的iOS平台通用链接 } }, "speech" : { //可选,JSON对象,Speech(语音识别)模块三方SDK配置 "baidu" : { //可选,JSON对象,使用百度语音识别SDK配置 "appid" : "", //必填,字符串类型,百度开放平台申请的appid "apikey" : "", //必填,字符串类型,百度开放平台申请的apikey "secretkey" : "" //必填,字符串类型,百度开放平台申请的secretkey } }, "statics" : { //可选,JSON对象,Statistic(统计)模块三方SDK配置 "umeng" : { //可选,JSON对象,使用友盟统计SDK配置 "appkey_ios" : "", //必填,字符串类型,友盟统计开放平台申请的iOS平台appkey "channelid_ios" : "", //可选,字符串类型,友盟统计iOS平台的渠道标识 "appkey_android" : "", //必填,字符串类型,友盟统计开放平台申请的Android平台appkey "channelid_android" : "" //可选,字符串类型,友盟统计Android平台的渠道标识 }, "google" : { //可选,JSON对象,使用Google Analytics for Firebase配置 "config_ios" : "", //必填,字符串类型,Google Firebase统计开发者后台获取的iOS平台配置文件路径 "config_android" : "" //必填,字符串类型,Google Firebase统计开发者后台获取的Android平台配置文件路径 } }, "ad" : {} }, //可选,JSON对象,使用互动游戏(变现猫)SDK,无需手动配置,在uni-AD后台申请开通后自动获取配置参数 "icons" : { //可选,JSON对象,应用图标相关配置 "ios" : { //可选,JSON对象,iOS平台图标配置 "appstore" : "", //必填,字符串类型,分辨率1024x1024, 提交app sotre使用的图标路径 "iphone" : { //可选,JSON对象,iPhone设备图标配置 "app@2x" : "", //可选,字符串类型,分辨率120x120,程序图标路径 "app@3x" : "", //可选,字符串类型,分辨率180x180,程序图标路径 "spotlight@2x" : "", //可选,字符串类型,分辨率80x80,Spotlight搜索图标路径 "spotlight@3x" : "", //可选,字符串类型,分辨率120x120,Spotlight搜索图标路径 "settings@2x" : "", //可选,字符串类型,分辨率58x58,Settings设置图标路径 "settings@3x" : "", //可选,字符串类型,分辨率87x87,Settings设置图标路径 "notification@2x" : "", //可选,字符串类型,分辨率40x40,通知栏图标路径 "notification@3x" : "" //可选,字符串类型,分辨率60x60,通知栏图标路径 }, "ipad" : { //可选,JSON对象,iPad设备图标配置 "app" : "", //可选,字符串类型,分辨率76x76,程序图标图标路径 "app@2x" : "", //可选,字符串类型,分辨率152x152,程序图标图标路径 "proapp@2x" : "", //可选,字符串类型,分辨率167x167,程序图标图标路径 "spotlight" : "", //可选,字符串类型,分辨率40x40,Spotlight搜索图标路径 "spotlight@2x" : "", //可选,字符串类型,分辨率80x80,Spotlight搜索图标路径 "settings" : "", //可选,字符串类型,分辨率29x29,Settings设置图标路径 "settings@2x" : "", //可选,字符串类型,分辨率58x58,Settings设置图标路径 "notification" : "", //可选,字符串类型,分辨率20x20,通知栏图标路径 "notification@2x" : "" //可选,字符串类型,分辨率740x40,通知栏图标路径 } }, "android" : { //可选,JSON对象,Android平台图标配置 "ldpi" : "", //可选,字符串类型,普通屏设备程序图标,分辨率要求48x48,已废弃 "mdpi" : "", //可选,字符串类型,大屏设备程序图标,分辨率要求48x48,已废弃 "hdpi" : "", //可选,字符串类型,高分屏设备程序图标,分辨率要求72x72 "xhdpi" : "", //可选,字符串类型,720P高分屏设备程序图标,分辨率要求96x96 "xxhdpi" : "", //可选,字符串类型,1080P高分屏设备程序图标,分辨率要求144x144 "xxxhdpi" : "" //可选,字符串类型,2K屏设备程序图标,分辨率要求192x192 } }, "splashscreen" : { //可选,JSON对象,启动界面配置 "iosStyle" : "common", //可选,字符串类型,iOS平台启动界面样式,可取值common、default、storyboard "ios" : { //可选,JSON对象,iOS平台启动界面配置 "storyboard" : "", //可选,字符串类型,自定义storyboard启动界面文件路径,iosStyle值为storyboard时生效 "iphone" : { //可选,JSON对象,iPhone设备启动图配置,iosStyle值为default时生效 "default" : "", //可选,字符串类型,分辨率320x480,iPhone3(G/GS)启动图片路径,已废弃 "retina35" : "", //可选,字符串类型,分辨率640x960,3.5英寸设备(iPhone4/4S)启动图片路径,已废弃 "retina40" : "", //可选,字符串类型,分辨率640x1136,4.0英寸设备(iPhone5/5S)启动图片路径 "retina40l" : "", //可选,字符串类型,分辨率1136x640,4.0英寸设备(iPhone5/5S)横屏启动图片路径 "retina47" : "", //可选,字符串类型,分辨率750x1334,4.7英寸设备(iPhone6/7/8)启动图片路径 "retina47l" : "", //可选,字符串类型,分辨率1334x750,4.7英寸设备(iPhone6/7/8)横屏启动图片路径 "retina55" : "", //可选,字符串类型,分辨率1242x2208,5.5英寸设备(iPhone6/7/8Plus)启动图片路径 "retina55l" : "", //可选,字符串类型,分辨率2208x1242,5.5英寸设备(iPhone6/7/8Plus)横屏启动图片路径 "iphonex" : "", //可选,字符串类型,分辨率1125x2436,5.8英寸设备(iPhoneX/XS)启动图片路径 "iphonexl" : "", //可选,字符串类型,分辨率2436x1125,5.8英寸设备(iPhoneX/XS)横屏启动图片路径 "portrait-896h@2x" : "", //可选,字符串类型,分辨率828x1792,6.1英寸设备(iPhoneXR)启动图片路径 "landscape-896h@2x" : "", //可选,字符串类型,分辨率1792x828,6.1英寸设备(iPhoneXR)iPhoneXR横屏启动图片路径 "portrait-896h@3x" : "", //可选,字符串类型,分辨率1242x2688,6.5英寸设备(iPhoneXS Max)启动图片路径 "landscape-896h@3x" : "" //可选,字符串类型,分辨率2688x1242,6.5英寸设备(iPhoneXS Max)横屏启动图片路径 }, "ipad" : { //可选,JSON对象,iPad设备启动图配置,iosStyle值为default时生效 "portrait" : "", //可选,字符串类型,分辨率768x1004,iPad竖屏启动图片路径,已废弃 "portrait-retina" : "", //可选,字符串类型,分辨率1536x2008,iPad高分屏竖屏启动图片路径,已废弃 "landscape" : "", //可选,字符串类型,分辨率1024x748,iPad横屏启动图片路径,已废弃 "landscape-retina" : "", //可选,字符串类型,分辨率2048x1496,iPad高分屏横屏启动图片路径,已废弃 "portrait7" : "", //可选,字符串类型,分辨率768x1024,9.7/7.9英寸iPad/mini竖屏启动图片路径 "landscape7" : "", //可选,字符串类型,分辨率1024x768,9.7/7.9英寸iPad/mini横屏启动图片路径 "portrait-retina7" : "", //可选,字符串类型,分辨率1536x2048,9.7/7.9英寸iPad/mini高分屏竖屏图片路径 "landscape-retina7" : "", //可选,字符串类型,分辨率2048x1536,9.7/7.9英寸iPad/mini高分屏横屏启动图片路径 "portrait-1112h@2x" : "", //可选,字符串类型,分辨率1668x2224,10.5英寸iPad Pro竖屏启动图片路径 "landscape-1112h@2x" : "", //可选,字符串类型,分辨率2224x1668,10.5英寸iPad Pro横屏启动图片路径 "portrait-1194h@2x" : "", //可选,字符串类型,分辨率1668x2388,11英寸iPad Pro竖屏启动图片路径 "landscape-1194h@2x" : "", //可选,字符串类型,分辨率2388x1668,11英寸iPad Pro横屏启动图片路径 "portrait-1366h@2x" : "", //可选,字符串类型,分辨率2048x2732,12.9英寸iPad Pro竖屏启动图片路径 "landscape-1366h@2x" : "" //可选,字符串类型,分辨率2732x2048,12.9英寸iPad Pro横屏启动图片路径 } }, "androidStyle" : "common", //可选,字符串类型,Android平台启动界面样式,可取值common、default "android" : { //可选,JSON对象,Android平台启动图片配置, androidStyle值为default时生效 "ldpi" : "", //可选,字符串类型,分辨率320x442,低密度屏幕启动图片路径,已废弃 "mdpi" : "", //可选,字符串类型,分辨率240x282,中密度屏幕启动图片路径,已废弃 "hdpi" : "", //可选,字符串类型,分辨率480x762,高密度屏幕启动图片路径 "xhdpi" : "", //可选,字符串类型,分辨率720x1242,720P高密度屏幕启动图片路径 "xxhdpi" : "" //可选,字符串类型,分辨率1080x1882,1080P高密度屏幕启动图片路径 } }, "orientation" : [ //可选,字符串数组类型,应用支持的横竖屏,**已废弃,使用screenOrientation配置** "portrait-primary", "portrait-secondary", "landscape-primary", "landscape-secondary" ] } }, /* 快应用特有相关 */ "quickapp" : {}, /* 小程序特有相关 */ "mp-weixin" : { "appid" : "wx001e2dc50bf532df", "setting" : { "urlCheck" : false }, "usingComponents" : true, "permission" : { "scope.userLocation" : { "desc" : "定位最近的门店" } }, "requiredPrivateInfos" : [ "getLocation", "chooseLocation" ] }, "mp-alipay" : { "usingComponents" : true }, "mp-baidu" : { "usingComponents" : true }, "mp-toutiao" : { "usingComponents" : true }, "uniStatistics" : { "enable" : false }, "vueVersion" : "3", "fallbackLocale" : "zh-Hans", "h5" : { "router" : { "base" : "/h5/" }, "sdkConfigs" : { // 使用地图或位置相关功能必须填写其一 "maps" : { "bmap" : { // 百度地图秘钥(HBuilderX 3.99+)http://lbsyun.baidu.com/apiconsole/key#/home "key" : "" }, "qqmap" : { "key" : "OGABZ-Y5OCF-5UWJ5-N7DHH-VFIG7-DHFEB" } } } } } /* 模块配置 *//* 应用发布信息 *//* android打包配置 */ ================================================ FILE: yshop-drink-uniapp-vue3/package.json ================================================ { "name": "yshop-miniapp", "version": "1.2.0", "description": "", "main": "main.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "@vant/area-data": "^1.5.0", "add": "^2.0.6", "flyio": "^0.6.14", "jweixin-module": "^1.6.0", "pinia": "^2.1.6", "vant": "^4.6.2", "weixin-js-sdk": "^1.6.3", "yarn": "^1.22.19" } } ================================================ FILE: yshop-drink-uniapp-vue3/pages/cart/cart.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/pages/components/pages/address/add.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/pages/components/pages/address/address.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/pages/components/pages/balance/bill.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/pages/components/pages/coupons/coupons.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/pages/components/pages/login/login.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/pages/components/pages/login/logout.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/pages/components/pages/mine/content.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/pages/components/pages/mine/userinfo.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/pages/components/pages/orders/detail.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/pages/components/pages/orders/orders.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/pages/components/pages/orders/refund.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/pages/components/pages/packages/index.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/pages/components/pages/pay/pay.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/pages/components/pages/remark/remark.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-textarea/package.json ================================================ { "id": "uv-textarea", "displayName": "uv-textarea 文本域 全面兼容vue3+2、app、h5、小程序等多端", "version": "1.0.10", "description": "文本域此组件满足了可能出现的表单信息补充,编辑等实际逻辑的功能,内置了字数校验等。", "keywords": [ "uv-textarea", "uvui", "uv-ui", "textarea", "文本域" ], "repository": "", "engines": { "HBuilderX": "^3.1.0" }, "dcloudext": { "type": "component-vue", "sale": { "regular": { "price": "0.00" }, "sourcecode": { "price": "0.00" } }, "contact": { "qq": "" }, "declaration": { "ads": "无", "data": "插件不采集任何数据", "permissions": "无" }, "npmurl": "" }, "uni_modules": { "dependencies": [ "uv-ui-tools" ], "encrypt": [], "platforms": { "cloud": { "tcb": "y", "aliyun": "y" }, "client": { "Vue": { "vue2": "y", "vue3": "y" }, "App": { "app-vue": "y", "app-nvue": "y" }, "H5-mobile": { "Safari": "y", "Android Browser": "y", "微信浏览器(Android)": "y", "QQ浏览器(Android)": "y" }, "H5-pc": { "Chrome": "y", "IE": "y", "Edge": "y", "Firefox": "y", "Safari": "y" }, "小程序": { "微信": "y", "阿里": "y", "百度": "y", "字节跳动": "y", "QQ": "y", "钉钉": "u", "快手": "u", "飞书": "u", "京东": "u" }, "快应用": { "华为": "u", "联盟": "u" } } } } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-textarea/readme.md ================================================ ## Textarea 文本域 > **组件名:uv-textarea** 文本域此组件满足了可能出现的表单信息补充,编辑等实际逻辑的功能,内置了字数校验等。 # 查看文档 ## [下载完整示例项目](https://ext.dcloud.net.cn/plugin?name=uv-ui) (请不要 下载插件ZIP) ### [更多插件,请关注uv-ui组件库](https://ext.dcloud.net.cn/plugin?name=uv-ui) ![image](https://mp-a667b617-c5f1-4a2d-9a54-683a67cff588.cdn.bspapp.com/uv-ui/banner.png) #### 如使用过程中有任何问题反馈,或者您对uv-ui有一些好的建议,欢迎加入uv-ui官方交流群:官方QQ群 ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-toast/changelog.md ================================================ ## 1.0.2(2023-10-13) 1. unmounted兼容vue3 ## 1.0.1(2023-05-16) 1. 优化组件依赖,修改后无需全局引入,组件导入即可使用 2. 优化部分功能 ## 1.0.0(2023-05-10) uv-toast 消息提示 ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-toast/components/uv-toast/uv-toast.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-toast/package.json ================================================ { "id": "uv-toast", "displayName": "uv-toast 消息提示 全面兼容小程序、nvue、vue2、vue3等多端", "version": "1.0.2", "description": "Toast 组件主要用于消息通知、加载提示、操作结果提示等醒目提示效果,我们为其提供了多种丰富的API。", "keywords": [ "uv-toast", "uvui", "uv-ui", "toast", "消息提示" ], "repository": "", "engines": { "HBuilderX": "^3.1.0" }, "dcloudext": { "type": "component-vue", "sale": { "regular": { "price": "0.00" }, "sourcecode": { "price": "0.00" } }, "contact": { "qq": "" }, "declaration": { "ads": "无", "data": "插件不采集任何数据", "permissions": "无" }, "npmurl": "" }, "uni_modules": { "dependencies": [ "uv-ui-tools", "uv-overlay", "uv-loading-icon", "uv-gap" ], "encrypt": [], "platforms": { "cloud": { "tcb": "y", "aliyun": "y" }, "client": { "Vue": { "vue2": "y", "vue3": "y" }, "App": { "app-vue": "y", "app-nvue": "y" }, "H5-mobile": { "Safari": "y", "Android Browser": "y", "微信浏览器(Android)": "y", "QQ浏览器(Android)": "y" }, "H5-pc": { "Chrome": "y", "IE": "y", "Edge": "y", "Firefox": "y", "Safari": "y" }, "小程序": { "微信": "y", "阿里": "y", "百度": "y", "字节跳动": "y", "QQ": "y", "钉钉": "u", "快手": "u", "飞书": "u", "京东": "u" }, "快应用": { "华为": "u", "联盟": "u" } } } } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-toast/readme.md ================================================ ## Toast 消息提示 > **组件名:uv-toast** Toast 组件主要用于消息通知、加载提示、操作结果提示等醒目提示效果,我们为其提供了多种丰富的API。 ### 查看文档 ### [完整示例项目下载 | 关注更多组件](https://ext.dcloud.net.cn/plugin?name=uv-ui) #### 如使用过程中有任何问题,或者您对uv-ui有一些好的建议,欢迎加入 uv-ui 交流群:uv-ui官方QQ群 ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-toolbar/changelog.md ================================================ ## 1.0.0(2023-08-02) 1. 新增工具条组件 ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-toolbar/components/uv-toolbar/props.js ================================================ export default { props: { // 是否展示工具条 show: { type: Boolean, default: true }, // 是否显示下边框 showBorder: { type: Boolean, default: false }, // 取消按钮的文字 cancelText: { type: String, default: '取消' }, // 确认按钮的文字 confirmText: { type: String, default: '确认' }, // 取消按钮的颜色 cancelColor: { type: String, default: '#909193' }, // 确认按钮的颜色 confirmColor: { type: String, default: '#3c9cff' }, // 标题文字 title: { type: String, default: '' }, ...uni.$uv?.props?.toolbar } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-toolbar/components/uv-toolbar/uv-toolbar.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-toolbar/package.json ================================================ { "id": "uv-toolbar", "displayName": "uv-toolbar 工具条", "version": "1.0.0", "description": "该组价是仅用于uv-ui中一个公共小工具,提供一个取消和确定的样式,可以设置标题,主要用于弹窗顶部的选择确定工具条", "keywords": [ "uv-toolbar", "uvui", "uv-ui", "工具条", "工具" ], "repository": "", "engines": { "HBuilderX": "^3.1.0" }, "dcloudext": { "type": "component-vue", "sale": { "regular": { "price": "0.00" }, "sourcecode": { "price": "0.00" } }, "contact": { "qq": "" }, "declaration": { "ads": "无", "data": "插件不采集任何数据", "permissions": "无" }, "npmurl": "" }, "uni_modules": { "dependencies": [ "uv-ui-tools" ], "encrypt": [], "platforms": { "cloud": { "tcb": "y", "aliyun": "y" }, "client": { "Vue": { "vue2": "y", "vue3": "y" }, "App": { "app-vue": "y", "app-nvue": "y" }, "H5-mobile": { "Safari": "y", "Android Browser": "y", "微信浏览器(Android)": "y", "QQ浏览器(Android)": "y" }, "H5-pc": { "Chrome": "y", "IE": "y", "Edge": "y", "Firefox": "y", "Safari": "y" }, "小程序": { "微信": "y", "阿里": "y", "百度": "y", "字节跳动": "y", "QQ": "y", "钉钉": "u", "快手": "u", "飞书": "u", "京东": "u" }, "快应用": { "华为": "u", "联盟": "u" } } } } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-toolbar/readme.md ================================================ ## Toolbar 工具条 > **组件名:uv-toolbar** 该组价是仅用于uv-ui中一个公共小工具,提供一个取消和确定的样式,可以设置标题,主要用于弹窗顶部的选择确定工具条。 ### 基本使用 ```vue ``` ### Toolbar Props | 属性名 | 类型 | 默认值 | 说明 | |:-|:-|:-|:-| | show | Boolean | true | 是否展示工具条 | | showBorder | Boolean | false | 是否显示下边框 | | cancelText | String | '取消' | 取消按钮的文字 | | confirmText | String | '确定' | 确定按钮的文字 | | cancelColor | String | '#909193' | 取消按钮的颜色 | | confirmColor | String | '#3c9cff' | 确认按钮的颜色 | | title | String | - | 标题文字 | ## [下载完整示例项目](https://ext.dcloud.net.cn/plugin?name=uv-ui) ### [更多插件,请关注uv-ui组件库](https://ext.dcloud.net.cn/plugin?name=uv-ui) ![image](https://mp-a667b617-c5f1-4a2d-9a54-683a67cff588.cdn.bspapp.com/uv-ui/banner.png) #### 如使用过程中有任何问题反馈,或者您对uv-ui有一些好的建议,欢迎加入uv-ui官方交流群:官方QQ群 ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-tooltip/changelog.md ================================================ ## 1.0.5(2023-07-02) 修改VUE3模式不显示的BUG ## 1.0.4(2023-07-02) uv-tooltip 由于弹出层uv-transition的修改,组件内部做了相应的修改,参数不变。 ## 1.0.3(2023-05-17) 1. 修复报错的BUG ## 1.0.2(2023-05-17) 1. vue2模式下报错的BUG修复 ## 1.0.1(2023-05-16) 1. 优化组件依赖,修改后无需全局引入,组件导入即可使用 2. 优化部分功能 ## 1.0.0(2023-05-10) uv-tooltip 长按提示 ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-tooltip/components/uv-tooltip/props.js ================================================ export default { props: { // 需要显示的提示文字 text: { type: [String, Number], default: '' }, // 点击复制按钮时,复制的文本,为空则使用text值 copyText: { type: [String, Number], default: '' }, // 文本大小 size: { type: [String, Number], default: 14 }, // 字体颜色 color: { type: String, default: '#606266' }, // 弹出提示框时,文本的背景色 bgColor: { type: String, default: 'transparent' }, // 弹出提示的方向,top-上方,bottom-下方 direction: { type: String, default: 'top' }, // 弹出提示的z-index,nvue无效 zIndex: { type: [String, Number], default: 10071 }, // 是否显示复制按钮 showCopy: { type: Boolean, default: true }, // 扩展的按钮组 buttons: { type: Array, default: () => [] }, // 是否显示透明遮罩以防止触摸穿透 overlay: { type: Boolean, default: true }, // 是否显示复制成功或者失败的toast showToast: { type: Boolean, default: true }, ...uni.$uv?.props?.tooltip } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-tooltip/components/uv-tooltip/uv-tooltip.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-tooltip/package.json ================================================ { "id": "uv-tooltip", "displayName": "uv-tooltip 长按提示", "version": "1.0.5", "description": "Tooltip组件主要用于长按操作,类似微信的长按气泡。", "keywords": [ "uv-tooltip", "uvui", "uv-ui", "tooltip", "长按提示" ], "repository": "", "engines": { "HBuilderX": "^3.1.0" }, "dcloudext": { "type": "component-vue", "sale": { "regular": { "price": "0.00" }, "sourcecode": { "price": "0.00" } }, "contact": { "qq": "" }, "declaration": { "ads": "无", "data": "插件不采集任何数据", "permissions": "无" }, "npmurl": "" }, "uni_modules": { "dependencies": [ "uv-ui-tools", "uv-overlay", "uv-transition", "uv-line" ], "encrypt": [], "platforms": { "cloud": { "tcb": "y", "aliyun": "y" }, "client": { "Vue": { "vue2": "y", "vue3": "y" }, "App": { "app-vue": "y", "app-nvue": "n" }, "H5-mobile": { "Safari": "y", "Android Browser": "y", "微信浏览器(Android)": "y", "QQ浏览器(Android)": "y" }, "H5-pc": { "Chrome": "y", "IE": "y", "Edge": "y", "Firefox": "y", "Safari": "y" }, "小程序": { "微信": "y", "阿里": "y", "百度": "y", "字节跳动": "y", "QQ": "y", "钉钉": "u", "快手": "u", "飞书": "u", "京东": "u" }, "快应用": { "华为": "u", "联盟": "u" } } } } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-tooltip/readme.md ================================================ ## Tooltip 长按提示 > **组件名:uv-tooltip** Tooltip组件主要用于长按操作,类似微信的长按气泡。 ### 查看文档 ### [完整示例项目下载 | 关注更多组件](https://ext.dcloud.net.cn/plugin?name=uv-ui) #### 如使用过程中有任何问题,或者您对uv-ui有一些好的建议,欢迎加入 uv-ui 交流群:uv-ui官方QQ群 ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-transition/changelog.md ================================================ ## 1.0.8(2023-10-18) 1. 修复在APP上不能正常显示的BUG ## 1.0.7(2023-10-12) 1. 修复部分情况,修改某属性自动关闭的BUG ## 1.0.6(2023-07-24) 1. 优化 nvue模式下增加cellChild参数,是否在list中cell节点下,nvue中cell下建议设置成true ## 1.0.5(2023-07-02) 修改VUE3模式下可能存在的BUG ## 1.0.4(2023-07-02) uv-transition 动画组件,代码重构优化,性能更加友好,增加自定义动画功能。详情参考文档:https://www.uvui.cn/components/transition.html ## 1.0.3(2023-06-12) 1. 恢复this.$nextTick的使用,经过测试百度等平台无问题 ## 1.0.2(2023-05-23) 1. 百度小程序等平台不支持this.$nextick,修改成延时 ## 1.0.1(2023-05-16) 1. 优化组件依赖,修改后无需全局引入,组件导入即可使用 2. 优化部分功能 ## 1.0.0(2023-05-10) 1. 新增动画组件 ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-transition/components/uv-transition/createAnimation.js ================================================ // const defaultOption = { // duration: 300, // timingFunction: 'linear', // delay: 0, // transformOrigin: '50% 50% 0' // } // #ifdef APP-NVUE const nvueAnimation = uni.requireNativePlugin('animation') // #endif class MPAnimation { constructor(options, _this) { this.options = options // 在iOS10+QQ小程序平台下,传给原生的对象一定是个普通对象而不是Proxy对象,否则会报parameter should be Object instead of ProxyObject的错误 this.animation = uni.createAnimation({ ...options }) this.currentStepAnimates = {} this.next = 0 this.$ = _this } _nvuePushAnimates(type, args) { let aniObj = this.currentStepAnimates[this.next] let styles = {} if (!aniObj) { styles = { styles: {}, config: {} } } else { styles = aniObj } if (animateTypes1.includes(type)) { if (!styles.styles.transform) { styles.styles.transform = '' } let unit = '' if(type === 'rotate'){ unit = 'deg' } styles.styles.transform += `${type}(${args+unit}) ` } else { styles.styles[type] = `${args}` } this.currentStepAnimates[this.next] = styles } _animateRun(styles = {}, config = {}) { let ref = this.$.$refs['ani'].ref if (!ref) return return new Promise((resolve, reject) => { nvueAnimation.transition(ref, { styles, ...config }, res => { resolve() }) }) } _nvueNextAnimate(animates, step = 0, fn) { let obj = animates[step] if (obj) { let { styles, config } = obj this._animateRun(styles, config).then(() => { step += 1 this._nvueNextAnimate(animates, step, fn) }) } else { this.currentStepAnimates = {} typeof fn === 'function' && fn() this.isEnd = true } } step(config = {}) { // #ifndef APP-NVUE this.animation.step(config) // #endif // #ifdef APP-NVUE this.currentStepAnimates[this.next].config = Object.assign({}, this.options, config) this.currentStepAnimates[this.next].styles.transformOrigin = this.currentStepAnimates[this.next].config.transformOrigin this.next++ // #endif return this } run(fn) { // #ifndef APP-NVUE this.$.animationData = this.animation.export() this.$.timer = setTimeout(() => { typeof fn === 'function' && fn() }, this.$.durationTime) // #endif // #ifdef APP-NVUE this.isEnd = false let ref = this.$.$refs['ani'] && this.$.$refs['ani'].ref if(!ref) return this._nvueNextAnimate(this.currentStepAnimates, 0, fn) this.next = 0 // #endif } } const animateTypes1 = ['matrix', 'matrix3d', 'rotate', 'rotate3d', 'rotateX', 'rotateY', 'rotateZ', 'scale', 'scale3d', 'scaleX', 'scaleY', 'scaleZ', 'skew', 'skewX', 'skewY', 'translate', 'translate3d', 'translateX', 'translateY', 'translateZ' ] const animateTypes2 = ['opacity', 'backgroundColor'] const animateTypes3 = ['width', 'height', 'left', 'right', 'top', 'bottom'] animateTypes1.concat(animateTypes2, animateTypes3).forEach(type => { MPAnimation.prototype[type] = function(...args) { // #ifndef APP-NVUE this.animation[type](...args) // #endif // #ifdef APP-NVUE this._nvuePushAnimates(type, args) // #endif return this } }) export function createAnimation(option, _this) { if(!_this) return clearTimeout(_this.timer) return new MPAnimation(option, _this) } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-transition/components/uv-transition/props.js ================================================ export default { props: { // 是否展示组件 show: { type: Boolean, default: false }, // 使用的动画模式 mode: { type: [Array, String, null], default() { return 'fade' } }, // 动画的执行时间,单位ms duration: { type: [String, Number], default: 300 }, // 使用的动画过渡函数 timingFunction: { type: String, default: 'ease-out' }, customClass: { type: String, default: '' }, ...uni.$uv?.props?.transition } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-transition/components/uv-transition/uv-transition.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-transition/package.json ================================================ { "id": "uv-transition", "displayName": "uv-transition 动画 全面兼容vue3+2、app、h5、小程序等多端", "version": "1.0.8", "description": "transition 该组件用于组件的动画过渡效果。", "keywords": [ "uv-transition", "uvui", "uv-ui", "transition", "动画" ], "repository": "", "engines": { "HBuilderX": "^3.1.0" }, "dcloudext": { "type": "component-vue", "sale": { "regular": { "price": "0.00" }, "sourcecode": { "price": "0.00" } }, "contact": { "qq": "" }, "declaration": { "ads": "无", "data": "插件不采集任何数据", "permissions": "无" }, "npmurl": "" }, "uni_modules": { "dependencies": [ "uv-ui-tools" ], "encrypt": [], "platforms": { "cloud": { "tcb": "y", "aliyun": "y" }, "client": { "Vue": { "vue2": "y", "vue3": "y" }, "App": { "app-vue": "y", "app-nvue": "y" }, "H5-mobile": { "Safari": "y", "Android Browser": "y", "微信浏览器(Android)": "y", "QQ浏览器(Android)": "y" }, "H5-pc": { "Chrome": "y", "IE": "y", "Edge": "y", "Firefox": "y", "Safari": "y" }, "小程序": { "微信": "y", "阿里": "y", "百度": "y", "字节跳动": "y", "QQ": "y", "钉钉": "u", "快手": "u", "飞书": "u", "京东": "u" }, "快应用": { "华为": "u", "联盟": "u" } } } } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-transition/readme.md ================================================ ## Transition 动画 > **组件名:uv-transition** 该组件用于组件的动画过渡效果,支持自定义动画,开箱即用。 # 查看文档 ## [下载完整示例项目](https://ext.dcloud.net.cn/plugin?name=uv-ui) ### [更多插件,请关注uv-ui组件库](https://ext.dcloud.net.cn/plugin?name=uv-ui) ![image](https://mp-a667b617-c5f1-4a2d-9a54-683a67cff588.cdn.bspapp.com/uv-ui/banner.png) #### 如使用过程中有任何问题反馈,或者您对uv-ui有一些好的建议,欢迎加入uv-ui官方交流群:官方QQ群 ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui/changelog.md ================================================ ## 1.1.15(2023-10-12) 1. 优化 uv-keyboard a. 增加disKeys参数,mode = "car"下,被禁用的键,如:['I','O']; b. 增加customabc参数,mode = "car"下,是否启用自定义中英文切换内容模式,为了兼容支付宝等小程序不兼容嵌套插槽,导致同时显示自定义内容和原始内容; c. 增加ref方法changeCarMode,mode = "car"下, 调用此方法可以切换中英文模式; d. 增加@changeCarInputMode,mode = "car"下,调用此方法可以进行切换中英文; e. 增加插槽abc,mode = "car"下,自定义中英文切换内容 2. 优化 uv-checkbox uv-radio 优化:https://gitee.com/climblee/uv-ui/issues/I872VD 3. 优化 uv-picker 将immediate-change默认值改为true,该值在于change回调的及时性,微信小程序生效 4. 优化 uv-tags 兼容customStyle参数等优化 5. 修复 uv-transition 部分情况,修改某属性自动关闭的BUG 6. 修复 uv-calendars 懒加载报错:https://gitee.com/climblee/uv-ui/issues/I869JS 7. 修复 uv-datetime-picker 设置minDate出现选择错乱的BUG 8. 修复 uv-input 搜狗输入法下存在不可清空的情况 9. 修复 uv-calendars selected没有设置了info或者info设置为空字符串后,文本则无法恢复BUG ## 1.1.14(2023-09-27) 1. 优化 uv-list-item 可使用customStyle变量进行样式控制 2. 优化 uv-cell 增加cellStyle参数,方便自定义单元格的样式 3. 优化 uv-switch 优化细节 4. 优化 不断优化[文档](https://www.uvui.cn/) 5. 修复 uv-button 通过customStyle修改按钮宽度,组件中最外层节点不改变的问题 6. 修复 uv-calendars a. 修复range模式下,selected设置了info后选中后,导致文本不恢复的问题;b. 修复multiple模式下,selected自定义信息的颜色没变,依然是白色 7. 修复 uv-checkbox uv-checkbox-group之change回调中v-model值不更新的BUG ## 1.1.13(2023-09-15) 1. 优化 uv-button a. 增加参数iconSize,用于控制图标的大小;b. 增加open-type="agreePrivacyAuthorization"类型,用户同意隐私协议事件回调 2. 优化 uv-picker 三级联动的案例:[https://www.uvui.cn/components/picker.html#省市区三级联动](https://www.uvui.cn/components/picker.html#省市区三级联动) 3. 修复 uv-read-more 全局设置rpx时,导致展开高度不对的BUG 4. 修复 uv-tabs a. 设置lineWidth未带单位产生的误差BUG;b. 首次加载时,处理下划线会有左到右的过渡效果 5. 修复 uv-textaera 设置autoHeight后出现高度异常的BUG 6. 修复 uv-input H5等情况设置禁用或可读情况下,点击事件无效的问题,nvue需要特殊处理 7. 修复 uv-calendars a. 在vue2+小程序渲染时闪烁的问题;b. 增加allowSameDay参数,是否允许日期范围的起止时间为同一天,mode=range时有效 8. 修复 uv-safe-bottom 兼容飞书小程序 9. 修复 uv-album 添加依赖,避免导入运行有误 10. 修复 uv-ui-tools 优化组件用到的相关 ## 1.1.12(2023-09-10) 1. 修复 uv-popup a. h5初始zIndex错误的问题;b. 修复全局设置prop无效的问题 2. 修复 uv-button 修复多个按钮由view包裹,显示在一行宽度不正常的BUG 3. 修复 uv-modal a. 修复两个按钮之间竖线不显示的问题;b. uv-ui项目自定义按钮示例修复 4. 修复 uv-calendars 修复国际化失效的BUG 5. 修复 uv-keyboard 修复键盘change回调事件产生冲突的BUG ## 1.1.11(2023-09-02) 1. 优化 uv-calendars a. 去除range参数,由mode="range"替换;b. 新增mode参数,不传 / multiple / range,分别为单日期, 多个日期,选择日期范围;c. 与uv-calendar选择日期的功能保持一致 2. 优化 uv-modal a. 增加align参数,设置文本对齐方式;b. 增加textStyle参数,扩展文本样式 3. 优化 uv-datetime-picker a. 增加mode="year"模式,方便只选择年;b. 增加clearDate参数,是否清除上次选择 4. 修复 uv-ui-tools 设置customstyle同名计算属性报错:The computed property "customStyle" is already defined as a prop 5. 修复 uv-image a. 设置widthFix时出现显示不全的BUG;b. 修复抖音等平台在width和height属性改变时出现不显示的BUG 6. 修复 uv-checkbox 点击空隙处或label插槽内容不会选中的问题 7. 修复 uv-radio 点击空隙处或label插槽内容不会选中的问题 8. 修复 uv-calendars 在pages.json中设置easycom会报错的BUG 9. 修复 uv-index-list 设置customNavHeight导致定位不准确的BUG ## 1.1.10(2023-08-30) 1. 交流反馈 欢迎加入uv-ui官方群1交流反馈: 549833913 2. 交流反馈 欢迎加入uv-ui官方群2交流反馈: 206060892 3. 优化 uv-calendars 1. 去除range参数,由mode="range"替换;2. 新增mode参数,不传 / multiple / range,分别为单日期, 多个日期,选择日期范围;3. 与uv-calendar选择日期的功能保持一致 4. 新增 uv-album 新增相册组件及相关文档 5. 优化 其他优化 6. 修复 uv-text app-nvue设置align不生效的BUG 7. 修复 uv-drop-down 自定义内容,点击自定义内容时会自动关闭弹窗的问题 8. 修复 uv-image 异步修改宽高不生效的问题,问题来源:https://gitee.com/climblee/uv-ui/issues/I7WUQ3 9. 修复 uv-calendars 通过setConfig修改属性不生效的问题,出自评论区:https://ext.dcloud.net.cn/plugin?id=12287 10. 修复 uv-list 设置边框不生效的BUG ## 1.1.9(2023-08-27) 1. 优化 uv-calendars 1. 去除range参数,由mode="range"替换;2. 新增mode参数,不传 / multiple / range,分别为单日期, 多个日期,选择日期范围;3. 与uv-calendar选择日期的功能保持一致 2. 优化 uv-picker 增加round属性,设置圆角 3. 修复 uv-calendars 点击返回今天按钮时,monthSwitch方法回调参数返回月份不是当天对应月份 4. 修复 uv-radio 1. 设置 labelSize 属性设置无效的问题:https://gitee.com/climblee/uv-ui/issues/I7W6UN;2. v-model 绑定布尔值控制台报警:https://gitee.com/climblee/uv-ui/issues/I7W714 5. 修复 uv-checkbox 1. 设置 label 属性为布尔值不生效的BUG ## 1.1.8(2023-08-24) 1. 优化 uv-popup 弹出不丝滑优化思路:https://www.uvui.cn/components/popup.html#yh 2. 修复 uv-switch 取消value传值,只能使用v-model传值,避免异步操作不生效的BUG 3. 修复 uv-index-list ios端滚动过程中+快速点击右侧导航页面出现空白的BUG 4. 修复 uv-rate 1. 支付宝报错的BUG; 2. 不能选半星的BUG 5. 修复 uv-model 异步loading时,确认回调还会一直触发的BUG 6. 修复 uv-swiper 标题文字过多未隐藏掉的BUG 7. 修复 uv-text app-nvue编译不能自动换行的BUG ## 1.1.7(2023-08-22) 1. 优化 uv-drop-down a. 增加@change回调,返回弹窗关闭状态; b. 增加init方法,方便位置改变进行调整 2. 优化 部分文档优化 3. 修复 uv-input a. app-nvue-ios端不能输入的BUG;b. 键盘高度等值不返回BUG 4. 修复 uv-scroll-list 报错导致不能移动指示器的BUG 5. 修复 uv-search 边距值在上次更新中误改导致不对的BUG 6. 修复 uv-image 设置width和height为100%不生效的BUG ## 1.1.6(2023-08-18) 1. 优化 优化文档 2. 修复 uv-list 使用列表右侧显示 switch,switchChange回调中返回数据为undefined的BUG 3. 修复 uv-checkbox 数据多不换行的BUG 4. 修复 uv-upload 1. 图片预览位置错误的BUG;2. 视频预览不生效的BUG;3. 改变上传视频宽高不生效的BUG 5. 修复 uv-navbar 在部分ios高版本机型,返回按钮不好操作的问题 6. 修复 uv-waterfall 只有一条数据的时候,切换的时候数据会左右显示错误的BUG ## 1.1.5(2023-08-14) 1. 优化 uv-pick-color 删除scrollTop参数,内部修改后就不需要了 2. 优化 uv-loading-icon 增加textStyle参数,可自定义文本样式,比如给上边距 3. 修复 uv-safe-bottom 百度小程序报错的BUG 4. 修复 uv-form 设置labelWidth属性时,节点渲染有闪动的BUG 5. 修复 uv-grid 设置col属性时,节点渲染有闪动的BUG 6. 修复 uv-parse 阻止a标签跳转文档说明 ## 1.1.4(2023-08-13) 1. 优化 nvue自定义图标 [详细文档-nvue中自定义图标库](https://www.uvui.cn/guide/customIcon.html#nvue%E4%B8%AD%E8%87%AA%E5%AE%9A%E4%B9%89%E5%9B%BE%E6%A0%87%E5%BA%93) 2. 优化 uv.$uv.http 在APP.vue页面使用报错的BUG: [Api集中管理](https://www.uvui.cn/js/http.html#_3-api%E9%9B%86%E4%B8%AD%E7%AE%A1%E7%90%86) 3. 修复 uv-navbar app-nvue运行ios存在背景图片错乱的问题 4. 修复 uv-list app-nvue运行ios存在,分包页面不滚动 5. 修复 uv-textarea 值为null或undefined时显示错误的bug 6. 修复 uv-search 值为null或undefined时显示错误的bug 7. 修复 uv-scroll-list vue2编译报错的BUG 8. 修复 uv-calendars 选择月份弹窗层级的问题 9. 修复 uv-form 动画在vue3 setup语法糖中错乱,以及表单其他相关问题解决: [Issues](https://gitee.com/my_dear_li_pan/uv-ui/issues/I7SNTT) 10. 修复 uv-picker-color 滚动页面无法点击的BUG:增加scrollTop参数,设置滚动条的位置。不设置如果页面出现滚动就需要传该值,会出现颜色面板无法进行选颜色的情况。 11. 交流反馈 欢迎加入uv-ui官方群1交流反馈: [549833913](https://www.uvui.cn/components/addQQGroup.html) 12. 交流反馈 欢迎加入uv-ui官方群2交流反馈: [206060892](https://www.uvui.cn/components/addQQGroup.html) ## 1.1.3(2023-08-06) 1. 优化 uv-calendars 1. 增加startText参数; 2. 增加endText参数; 3. 增加selected中的参数; 4. 优化日历范围选择 2. 优化 uv-empty icon属性支持base64图片 3. 优化 uv-navbar 增加背景图片的裁剪模式参数imgMode 4. 优化 uv-picker-color 颜色值不对的BUG 5. 优化 [API文档优化](https://www.uvui.cn/components/changelog.html) 6. 优化 常见问题增加:[怎么隐藏uv-tabs等组件的滚动条](https://www.uvui.cn/components/problem.html#%E4%B9%9D%E3%80%81%E6%80%8E%E4%B9%88%E9%9A%90%E8%97%8Fuv-tabs%E7%AD%89%E7%BB%84%E4%BB%B6%E7%9A%84%E6%BB%9A%E5%8A%A8%E6%9D%A1) 7. 修复 uv-radio name为数字0时不能选中的BUG 8. 修复 uv-textarea 1. v-model设置为数据时的BUG;2. 复制过多内容,计数显示错误的BUG;3. maxlength为-1改成不显示计数 9. 修复 uv-code-input 在vue2模式下,v-model设置为0时不生效的BUG 10. 修复 uv-input 在vue2模式下,v-model设置为0时不生效的BUG 11. 修复 uv-search 在vue2模式下,v-model设置为0时不生效的BUG 12. 修复 uv-ui-tools 1. 路由拦截修复;2. 增加events参数 ## 1.1.2(2023-08-03) 1. 新增 uv-calendars 新版日历发布 2. 新增 uv-toolbar 组件独立发布,老用户更新uv-picker,需要手动删除uv-picker目录下的uv-toolbar目录,否则会有冲突提示 3. 优化 uv-tags 增加cellChild参数 4. 优化 uv-navbar 兼容背景图片 5. 优化 uv-notice-bar 竖向滚动时候增加change回调 ## 1.1.1(2023-07-30) 1. 新增 uv-drop-down 下拉筛选组件,兼容app-nvue及多端 2. 优化 uv-textarea 增加confirm-hold参数,方便设置进行换行处理 3. 优化 其他关于文档的优化等 ## 1.1.0(2023-07-26) 1. 重构 uv-list 全面重构,提高性能,放弃使用scroll-view,具体文档参考:uv-list列表 2. 优化 uv-search 1. 增加prefix和suffix 前置和后置插槽;2. 增加boxStyle参数,方便控制输入框部分的样式 3. 优化 文档优化:获取节点布局信息,文档新增nvue获取方式的说明 ## 1.0.22(2023-07-26) 1. 优化 uv-textarea 组件 增加textStyle和countStyle属性,方便控制文本样式 2. 优化 uv-swiper 增加竖向播放属性:vertical 3. 优化 uv-icon 支持base64图片格式 4. 优化 uv-transition 和 uv-image 增加参数cellChild属性,避免nvue中出现回收后不显示的BUG 5. 优化 uv-button 增加customTextStyle属性,方便自定义按钮文字样式 6. 优化 优化部分文档说明 7. 修复 uv-slider 修改背景颜色属性为backgroundColor,避免设置不生效 8. 修复 uv-index-list 1. 修复全局设置成rpx存在的高度BUG;2. 修复其他BUG ## 1.0.21(2023-07-22) 1. 新增 uv-scroll-list 横向滚动列表组件 2. 优化 增加测试占位图,方便开发者使用线上图片进行测试:[https://www.uvui.cn/components/testPic.html](https://www.uvui.cn/components/testPic.html) 3. 优化 uv-calendar 组件文档示例等优化,增加setFormatter说明 4. 优化 uv-notice-bar 优化文档,说明不显示左边图标的使用方法 5. 修复 uv-input 在微信小程序端清除内容存在不能清除的BUG 6. 修复 uv-button 1. 解决微信小程序动态设置hover-class点击态不消失的BUG; 2. 文档优化 7. 修复 uv-waterfall 在tab切换等场景快速切换时,会出现报错的BUG 8. 优化 优化其他 ## 1.0.20(2023-07-18) 1. 修复 uv-textarea 设置-1不生效 2. 修复 uv-icon 恢复uv-empty相关的图标 3. 修复 uv-empty 恢复设置mode属性的内置图标 4. 优化 [优化文档](https://www.uvui.cn) ## 1.0.19(2023-07-14) 1. 优化 uv-waterfall 当changeList未处理数据时,正确返回对应列的数据,避免误导 2. 修复 uv-rate VUE3模式下设置value属性不生效的BUG 3. 修复 uv-input VUE3模式下设置value属性不生效的BUG 4. 修复 uv-search VUE3模式下设置value属性不生效的BUG 5. 修复 uv-code-input VUE3模式下设置value属性不生效的BUG 6. 修复 uv-number-box VUE3模式下设置value属性不生效的BUG 7. 修复 uv-radio VUE3模式下设置value属性不生效的BUG 8. 修复 uv-checkbox VUE3模式下设置value属性不生效的BUG 9. 修复 uv-textarea VUE3模式下设置value属性不生效的BUG 10. 修复 uv-switch VUE3模式下设置value属性不生效的BUG 11. 修复 uv-slider VUE3模式下设置value属性不生效的BUG 12. 修复 uv-datetime-picker VUE3模式下设置value属性不生效的BUG 13. 修复 uv-icon 部分图标错误的BUG ## 1.0.18(2023-07-06) 1. 优化 uv-icon 1. 更新图标,删除一些不常用的图标;2. 删除base64,修改成ttf文件引入读取图标。uv-icon 图标 2. 优化 uv-icon nvue自定义图标用法,文档说明:[点击跳转](https://www.uvui.cn/guide/customIcon.html) 3. 优化 uv-upload 文档示例代码,增加fileList参数说明:[点击跳转](https://www.uvui.cn/components/upload.html#filelist-options) 4. 修复 uv-checkbox vue3模式下,动态修改v-model绑定的值无效的BUG 5. 修复 uv-radio vue3模式下,动态修改v-model绑定的值无效的BUG 6. 修复 uv-datetime-picker vue3模式下,动态修改v-model绑定的值无效的BUG ## 1.0.17(2023-07-04) 1. 优化 uv-icon 修复,NVUE平台主题颜色在APP不生效的BUG 2. 优化 uv-notice-bar 优化,增加disableScroll属性 3. 优化 uv-input uv-back-top uv-cell uv-form uv-search uv-modal uv-navbar uv-index-list uv-empty uv-upload 去除插槽判断,避免某些平台不显示的BUG 4. 优化 uv-form 优化文档 5. 优化 优化其他相关文档 ## 1.0.16(2023-07-03) 1. 优化 uv-transition 动画组件,代码重构优化,性能更加友好,增加自定义动画功能。详情[参考文档](https://www.uvui.cn/components/transition.html) 2. 优化 uv-popup 弹出层,代码重构优化,性能翻倍,小程序体验性能更加,避免卡顿。打开和关闭方法更改,详情[参考文档](https://www.uvui.cn/components/popup.html) 3. 优化 uv-calendar 由于弹出层uv-popup的修改,打开和关闭方法更改,详情[参考文档](https://www.uvui.cn/components/actionSheet.html) 4. 优化 uv-action-sheet 由于弹出层uv-popup的修改,打开和关闭方法更改,详情[参考文档](https://www.uvui.cn/components/calendar.html) 5. 优化 uv-datetime-picker 由于弹出层uv-popup的修改,打开和关闭方法更改,详情[参考文档](https://www.uvui.cn/components/datetimePicker.html) 6. 优化 uv-form 由于弹出层uv-transition的修改,组件内部做了相应的修改,参数不变。 7. 优化 uv-keyboard 由于弹出层uv-popup的修改,打开和关闭方法更改,详情[参考文档](https://www.uvui.cn/components/keyboard.html) 8. 优化 uv-modal 由于弹出层uv-popup的修改,打开和关闭方法更改,详情[参考文档](https://www.uvui.cn/components/modal.html) 9. 优化 uv-notify 由于弹出层uv-popup的修改,打开和关闭方法更改,详情[参考文档](https://www.uvui.cn/components/notify.html) 10. 优化 uv-overlay 由于弹出层uv-transition的修改,组件内部做了相应的修改,参数不变。 11. 优化 uv-pick-color 由于弹出层uv-popup的修改,打开和关闭方法更改,详情[参考文档](https://www.uvui.cn/components/pickColor.html) 12. 优化 uv-picker 由于弹出层uv-popup的修改,打开和关闭方法更改,详情[参考文档](https://www.uvui.cn/components/picker.html) 13. 优化 uv-tooltip 由于弹出层uv-transition的修改,组件内部做了相应的修改,参数不变。 14. 优化 uv-loading-page 由于弹出层uv-transition的修改,组件内部做了相应的修改,参数不变。 15. 优化 相关文档的优化更改。 16. 修复 uv-safe-bottom 修复,在百度程序,抖音小程序不生效的BUG ## 1.0.15(2023-06-29) 1. 欢迎加QQ群交流:[549833913](https://www.uvui.cn/components/addQQGroup.html) 2. 优化 uv-swiper 优化:1. 增加titleStyle属性,方便修改标题样式;2. 标题上去掉是否是图片的判断,避免无后缀的图片不显示 3. 优化 uv-steps 优化:1. 增加插槽title; 3. 文档关于插槽相关的参数说明完善;增加customStyle属性 4. 优化 uv-checkbox 优化:增加label文字插槽,与radio保持一致,优化文档相关说明 5. 优化 uv-modal 优化:增加closeLoading方法,方便异步加载手动取消加载状态,更新文档 6. 优化 uv-image 增加文档说明:uv-list、 uv-waterfall等组件在 Android平台使用了list封装,所以在该组件中仍然不能使用uv-image等组件 7. 优化 优化更多文档 8. 修复 uv-vtabs 修复非联动情况下,内容过多的情况,滚动一段距离,再切换未滚动到顶部的BUG 9. 修复 uv-image 修复:duration属性不生效的BUG 10. 修复 uv-code-input 修复:使用:disabledKeyboard="true"属性,事件全部失效的BUG 11. 修复 uv-button 修复:设置open-type="chooseAvatar"等值不生效的BUG ## 1.0.14(2023-06-25) 1. 欢迎加QQ群交流:[549833913](https://www.uvui.cn/components/addQQGroup.html) 2. 优化 uv-count-down 增加外部样式customStyle参数 3. 优化 文档的全面优化 4. 修复 uv-count-to 1. 修复继续滚动的函数 2. 修改文档错误 4. 适配px和rpx的单位 4. 适配customStyle参数 5. 修复 uv-load-more 修复customStyle参数设置背景等不生效的BUG 6. 修复 uv-code-input 优化下边框 7. 修复 uv-tabs 添加uv-icon依赖 8. 修复 uv-grid 优化修改 9. 修复 uv-cell 优化修改 ## 1.0.13(2023-06-20) 1. 优化 uv-calendar formatter格式化中增加topInfo参数 2. 优化 uv-tabs 增加customStyle参数 3. 优化 文档优化,便于开发者直接开干 4. 优化 uv-switch 优化size属性,适配单位传递 5. 修复 uv-ui-tools、uv-form、uv-picker 修复vue3编译支付宝异常 6. 修复 uv-ui-tools、uv-form、uv-picker 修复vue3编译支付宝异常 7. 修复 uv-parse 修复在nvue不显示的BUG 8. 修复 uv-form 修复某些条件下报错的BUG ## 1.0.12(2023-06-14) 1. 优化部分组件,优化文档部分细节 2. uv-popup、uv-modal 修复遮罩层zIndex问题 3. uv-form 在vue3的setup语法中ref使用uvForm会导致报错 4. uv-tabs activeStyle设置字体大小,可能会导致下划线位置不对BUG 5. uv-pick-color 百度小程序点击报错 6. uv-transition 恢复this.$nextTick 7. uv-picker 抖音小程序选择的时候报错,导致不能关闭的BUG 8. uv-checkbox 多余的属性labelDisabled,导致APP中报错提示 9. uv-tabbar 底部安全距离组件无效的BUG 10. uv-vtabs 头部存在的时候,联动不准确的BUG ## 1.0.11(2023-06-12) 1. uv-radio-group、uv-checkbox-group 兼容自定义样式customStyle,方便通过样式调整整体位置等,数据较多时允许换行 2. uv-ui-tools 优化内置样式等,解决微信小程序使用uvui提示 Some selectors are not allowed in component wxss, including tag name selectors, ID selectors, and attribute selectors,[详情](https://www.uvui.cn/components/problem.html) 3. uv-datetime-picker 取消defaultIndex参数,目前传该值也没实际意义 4. uv-tabbar 增加iconSize参数 5. uv-calendar 增加change回调 6. uv-calendar 修复BUG 7. uv-rate 修复只读或禁止状态下设置value无效的问题 8. uv-popup 修复zIndex问题 9. uv-modal 修复zIndex问题 10. 文档-扩展配置更新:[扩展配置](https://www.uvui.cn/components/setting.html) 11. 文档-优化更新:[uv-ui文档](https://www.uvui.cn/components/changelog.html) 12. 文档-新增常见问题:[常见问题](https://www.uvui.cn/components/problem.html) 13. 优化其他 ## 1.0.10(2023-06-05) 1. uv-navbar 渐变背景色兼容 2. uv-calendar 日历选择BUG修复 ## 1.0.9(2023-06-05) 1. 新增uv-vtabs垂直选项卡组件,主要用于分类展示,分类切换功能,支持联动和不联动两种模式 2. uv-qrcode,uv-datetime-picker,uv-subsection等文档说明优化,避免开发困难;优化API相关说明 3. uv-notice-bar 1. 修复在触发error函数报错的BUG;2. 修复在text值为undefined的时候,解决报错BUG 4. uv-button 等组件修复触发两次事件的BUG 5. uv-datetime-picker 1. 修复重置值存在不更新的BUG;2. 优化文档,增加filter使用方法说明 6. uv-badge 修复type等属性为null或undefined的时候不显示徽标的BUG 7. uv-ui-tools 优化工具组件,兼容更多功能,小程序分享功能优化等 ... ## 1.0.8(2023-05-27) 1. uv-waterfall修复在百度小程序中可能存在的BUG;去掉原有的slot方式 2. uv-image修复可能报错的问题 3. uv-pick-color 在文档预览模式中无法点击的问题 4. uv-index-list 修复select事件不触发的问题 5. 优化其他组件及示例项目等 ## 1.0.7(2023-05-25) 1. uv-icon 将线上ttf字体包替换成base64,避免加载时或者网络差时候显示白色方块 2. uv-text 去掉多余的data-index属性,避免警告 3. uv-upload 在fileList的watch中增加deep属性 4. uv-pick-color 去掉template中存在的this.导致头条小程序编译警告 5. uv-image 去掉template中存在的this.导致头条小程序编译警告 ## 1.0.6(2023-05-23) 1. 新增uv-pick-color颜色选择器组件 2. uv-toolbar组件增加showBorder属性,是否显示下边框 3. uv-transition组件在百度小程序等平台不支持this.$nextick导致下面的逻辑不执行,使用延时替换方案 4. uv-ui-tools组件中bem()函数兼容百度/头条小程序等 5. uv-waterfall组件修复在百度/头条小程序显示异常等BUG,增加changeList回调函数处理数据,同步更新示例等 6. uv-image组件修复在百度/头条小程序等开启observeLazyLoad后显示异常BUG 7. uv-tabs组件修复上次更新导致的在nvue中不滚动的BUG 8. uv-qrcode组件修复在部分平台不显示加载的BUG 9. 修复其他已知问题等 ## 1.0.5(2023-05-17) 1. 新增uv-qrcode二维码组件 2. 修复uv-tooltip在vue2模式下的BUG 3. 优化部分问题 ## 1.0.4(2023-05-16) 1. 优化组件依赖,修改后无需全局引入,组件导入即可使用 2. 优化部分功能 ## 1.0.3(2023-05-12) 1. 修复uv-input在vue3模式下双向绑定问题 2. 修复uv-textarea在vue3模式下双向绑定问题 3. 修复uv-rate在vue3模式下双向绑定问题 ## 1.0.2(2023-05-11) 1. 更新文档 2. 增加插件下载入口 ## 1.0.1(2023-05-10) 1. 所有组件依赖 2. 上传示例项目 ## 1.0.0(2023-05-10) 1. uv-ui ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui/components/uv-ui/uv-ui.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui/package.json ================================================ { "id": "uv-ui", "displayName": "uv-ui 破釜沉舟之兼容vue3+2、app、h5、小程序等多端,灵活导入,利剑出击", "version": "1.1.15", "description": "uv-ui 是基于uni-app、部分组件基于uView2.x、全端兼容、支持独立导入、内容丰富的UI框架。破釜沉舟之兼容vue3+2、app、h5、小程序等多端,利剑出击,开箱即用。", "keywords": [ "uv-ui", "uvui", "UI组件库", "ui框架", "ui库" ], "repository": "https://github.com/climblee/uv-ui", "engines": { "HBuilderX": "^3.1.0" }, "dcloudext": { "type": "component-vue", "sale": { "regular": { "price": "0.00" }, "sourcecode": { "price": "0.00" } }, "contact": { "qq": "" }, "declaration": { "ads": "无", "data": "无", "permissions": "无" }, "npmurl": "https://www.npmjs.com/package/@climblee/uv-ui" }, "uni_modules": { "dependencies": [ "uv-album", "uv-drop-down", "uv-calendars", "uv-scroll-list", "uv-vtabs", "uv-pick-color", "uv-qrcode", "uv-ui-tools", "uv-action-sheet", "uv-alert", "uv-avatar", "uv-back-top", "uv-badge", "uv-button", "uv-calendar", "uv-cell", "uv-checkbox", "uv-code", "uv-code-input", "uv-collapse", "uv-count-down", "uv-count-to", "uv-datetime-picker", "uv-divider", "uv-empty", "uv-form", "uv-gap", "uv-grid", "uv-icon", "uv-image", "uv-index-list", "uv-input", "uv-keyboard", "uv-line", "uv-line-progress", "uv-link", "uv-list", "uv-loading-icon", "uv-loading-page", "uv-load-more", "uv-modal", "uv-navbar", "uv-no-network", "uv-notice-bar", "uv-notify", "uv-number-box", "uv-overlay", "uv-parse", "uv-picker", "uv-popup", "uv-radio", "uv-rate", "uv-read-more", "uv-row", "uv-safe-bottom", "uv-search", "uv-skeleton", "uv-slider", "uv-status-bar", "uv-steps", "uv-sticky", "uv-subsection", "uv-swipe-action", "uv-swiper", "uv-switch", "uv-tabbar", "uv-tabs", "uv-tags", "uv-text", "uv-textarea", "uv-toast", "uv-tooltip", "uv-transition", "uv-upload", "uv-waterfall" ], "encrypt": [], "platforms": { "cloud": { "tcb": "y", "aliyun": "y" }, "client": { "Vue": { "vue2": "y", "vue3": "y" }, "App": { "app-vue": "y", "app-nvue": "y" }, "H5-mobile": { "Safari": "y", "Android Browser": "y", "微信浏览器(Android)": "y", "QQ浏览器(Android)": "y" }, "H5-pc": { "Chrome": "y", "IE": "y", "Edge": "y", "Firefox": "y", "Safari": "y" }, "小程序": { "微信": "y", "阿里": "y", "百度": "y", "字节跳动": "y", "QQ": "y", "钉钉": "u", "快手": "u", "飞书": "u", "京东": "u" }, "快应用": { "华为": "u", "联盟": "u" } } } } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui/readme.md ================================================

    logo

uv-ui

兼容vue3+2多平台快速开发的UI框架

[![star](https://gitee.com/climblee/uv-ui/badge/star.svg?theme=gvp)](https://gitee.com/climblee/uv-ui) [![star](https://gitee.com/climblee/uv-ui/badge/fork.svg?theme=gvp)](https://gitee.com/climblee/uv-ui) [![star](https://img.shields.io/github/stars/climblee/uv-ui?style=flat-square&logo=GitHub)](https://github.com/climblee/uv-ui) [![issues](https://img.shields.io/github/issues/climblee/uv-ui?style=flat-square&logo=GitHub)](https://github.com/climblee/uv-ui/issues) [![Website](https://img.shields.io/badge/uvui-up-blue?style=flat-square)](https://www.uvui.cn) [![version](https://img.shields.io/badge/version-1.1.15-brightgreen.svg)](https://www.uvui.cn/components/changelog.html) [![license](https://img.shields.io/github/license/climblee/uv-ui?style=flat-square)](https://en.wikipedia.org/wiki/MIT_License) ## 温馨提示:如需下载uv-ui示例项目,请不要使用【下载插件ZIP】按钮。 ### uvui官方群1:549833913 ### uvui官方群2:206060892 ## uvui特点 1. **uv-ui的前世今生**,`uv-ui` 是基于 `uview2.x` 版本改造而来。重命名也是为了避开发布冲突和很多组件 `u-`在 `nvue` 中不能使用的情况,所以这才诞生了`uv-ui`。感谢 `uview-ui` 作者的开源奉献,再次为开源点赞。 同时 `uv-ui` 也是无条件开源。 2. **全端兼容**,`uv-ui`支持vue3、vue2、app-vue、app-nvue、h5、小程序等。`uv-ui`的组件都是多端自适应的,底层会抹平很多小程序平台的差异或bug。 3. **扩展配置**,`uv-ui`内置的方法默认不再挂载到`uni`对象之上,也就意味着默认情况下不能在项目中直接使用`uni.$uv.xxx`使用内置方法。但是可以通过扩展可以解决,通过如下方式进行配置即可,使用方式请参考[扩展配置](https://www.uvui.cn/components/setting.html)。其中包括[ JS工具库](https://www.uvui.cn/components/setting.html#%E6%89%A9%E5%B1%95%E9%85%8D%E7%BD%AE-js%E5%B7%A5%E5%85%B7%E5%BA%93)、[ 自定义主题](https://www.uvui.cn/components/setting.html#%E6%89%A9%E5%B1%95%E9%85%8D%E7%BD%AE-%E8%87%AA%E5%AE%9A%E4%B9%89%E4%B8%BB%E9%A2%98)、[ 基础样式](https://www.uvui.cn/components/setting.html#%E6%89%A9%E5%B1%95%E9%85%8D%E7%BD%AE-%E5%9F%BA%E7%A1%80%E6%A0%B7%E5%BC%8F)、[ setconfig](https://www.uvui.cn/components/setting.html#%E6%89%A9%E5%B1%95%E9%85%8D%E7%BD%AE-setconfig)等。 ## 预览 通过微信(APP下载不支持微信扫码)或浏览器扫码查看演示效果。          ## 链接 - [官方文档](https://www.uvui.cn) - [演示地址](https://h5.uvui.cn) - [更新日志](https://www.uvui.cn/components/changelog.html) - [关于我们](https://www.uvui.cn/cooperation/about.html) - 组件列表 ## 交流反馈 欢迎加入我们的QQ群交流反馈:[点此跳转](https://www.uvui.cn/components/addQQGroup.html) ## 快速开始 方式一:`uv-ui` 强烈建议通过 `下载插件并导入HbuilderX` 导入组件。 方式二:下载完整 [uv-ui项目](https://ext.dcloud.net.cn/plugin?id=12287) 将 `uni_modules` 复制到自己的项目。 方式三:通过 `npm i @climblee/uv-ui` 下载,此方法需要配置 easycom,配置详情可查看[安装](https://www.uvui.cn/components/install.html)。 请通过[快速上手](https://www.uvui.cn/components/quickstart.html)了解更详细的内容。 **注意:导入插件后,建议`HBuilderX`重新运行项目,可能新导入的插件不能实时更新而导致不能运行。** ## 使用方法 组件导入 `uni_modules` 后,直接在项目中使用,无需通过import引入组件。 ```html ``` ## 扩展功能 `uv-ui` 内置了强大的工具函数、请求封装等,可以根据自身需求进行扩展配置,详情请查看[扩展配置](https://www.uvui.cn/components/setting.html)。 **注意:只有[扩展配置](https://www.uvui.cn/components/setting.html)后才能在自己的项目页面中使用组件库内置方法和变量等**。
## 组件列表 下表为 `uv-ui` 的扩展组件清单,点击每个组件**点击下载&安装**即可在详情页面导入组件到项目下,导入后建议重新运行即可直接使用,组件无需import和注册。 | 组件名 | 组件说明 | | --- | --- | | uv-calendars | [新版日历(推荐)](https://www.uvui.cn/components/calendars.html) | | uv-drop-down | [下拉筛选](https://www.uvui.cn/components/dropDown.html) | | uv-scroll-list | [横向滚动列表](https://www.uvui.cn/components/scrollList.html) | | uv-vtabs | [垂直选项卡](https://www.uvui.cn/components/vtabs.html) | | uv-pick-color | [颜色选择器](https://www.uvui.cn/components/pickColor.html) | | uv-qrcode | [二维码](https://www.uvui.cn/components/qrcode.html) | | uv-waterfall | [瀑布流](https://www.uvui.cn/components/waterfall.html) | | uv-row | [Layout 布局](https://www.uvui.cn/components/layout.html) | | uv-icon | [图标](https://www.uvui.cn/components/icon.html) | | uv-button | [按钮](https://www.uvui.cn/components/button.html) | | uv-text | [文本](https://www.uvui.cn/components/text.html) | | uv-link | [超链接](https://www.uvui.cn/components/link.html) | | uv-image | [图片](https://www.uvui.cn/components/image.html) | | uv-transition | [动画](https://www.uvui.cn/components/transition.html) | | uv-form | [表单](https://www.uvui.cn/components/form.html) | | uv-input | [增强输入框](https://www.uvui.cn/components/input.html) | | uv-textarea | [增强文本域](https://www.uvui.cn/components/textarea.html) | | uv-checkbox | [复选框](https://www.uvui.cn/components/checkbox.html) | | uv-radio | [单选框](https://www.uvui.cn/components/radio.html) | | uv-switch | [开关选择器](https://www.uvui.cn/components/switch.html) | | uv-calendar | [日历](https://www.uvui.cn/components/calendar.html) | | uv-picker | [选择器](https://www.uvui.cn/components/picker.html) | | uv-datetime-picker | [时间选择器](https://www.uvui.cn/components/datetimePicker.html) | | uv-code | [验证码倒计时](https://www.uvui.cn/components/code.html) | | uv-keyboard | [键盘](https://www.uvui.cn/components/keyboard.html) | | uv-rate | [评分](https://www.uvui.cn/components/rate.html) | | uv-search | [多功能搜索框](https://www.uvui.cn/components/search.html) | | uv-number-box | [步进器](https://www.uvui.cn/components/numberBox.html) | | uv-upload | [上传](https://www.uvui.cn/components/upload.html) | | uv-slider | [滑动选择器](https://www.uvui.cn/components/slider.html) | | uv-list | [列表](https://www.uvui.cn/components/list.html) | | uv-index-list | [索引列表](https://www.uvui.cn/components/indexList.html) | | uv-tags | [标签](https://www.uvui.cn/components/tag.html) | | uv-line-progress | [线形进度条](https://www.uvui.cn/components/lineProgress.html) | | uv-badge | [徽标数](https://www.uvui.cn/components/badge.html) | | uv-count-down | [倒计时](https://www.uvui.cn/components/countDown.html) | | uv-count-to | [数字滚动](https://www.uvui.cn/components/countTo.html) | | uv-avatar | [头像](https://www.uvui.cn/components/avatar.html) | | uv-skeleton | [骨架屏](https://www.uvui.cn/components/skeleton.html) | | uv-loading-icon | [加载动画](https://www.uvui.cn/components/loadingIcon.html) | | uv-loading-page | [加载页](https://www.uvui.cn/components/loadingPage.html) | | uv-load-more | [加载更多](https://www.uvui.cn/components/loadMore.html) | | uv-empty | [内容为空](https://www.uvui.cn/components/empty.html) | | uv-tooltip | [长按提示](https://www.uvui.cn/components/tooltip.html) | | uv-alert | [警告提示](https://www.uvui.cn/components/alert.html) | | uv-toast | [消息提示](https://www.uvui.cn/components/toast.html) | | uv-notice-bar | [滚动通知](https://www.uvui.cn/components/noticeBar.html) | | uv-notify | [消息提示](https://www.uvui.cn/components/notify.html) | | uv-no-network | [无网络提示](https://www.uvui.cn/components/noNetwork.html) | | uv-popup | [弹出层](https://www.uvui.cn/components/popup.html) | | uv-modal | [模态框](https://www.uvui.cn/components/modal.html) | | uv-cell | [单元格](https://www.uvui.cn/components/cell.html) | | uv-swipe-action | [滑动单元格](https://www.uvui.cn/components/swipeAction.html) | | uv-swiper | [轮播图](https://www.uvui.cn/components/swiper.html) | | uv-collapse | [折叠面板](https://www.uvui.cn/components/collapse.html) | | uv-grid | [宫格布局](https://www.uvui.cn/components/grid.html) | | uv-album | [相册](https://www.uvui.cn/components/album.html) | | uv-tabbar | [底部导航栏](https://www.uvui.cn/components/tabbar.html) | | uv-back-top | [返回顶部](https://www.uvui.cn/components/backTop.html) | | uv-navbar | [自定义导航栏](https://www.uvui.cn/components/navbar.html) | | uv-action-sheet | [底部操作菜单](https://www.uvui.cn/components/actionSheet.html) | | uv-tabs | [标签选项卡](https://www.uvui.cn/components/tabs.html) | | uv-steps | [步骤条](https://www.uvui.cn/components/steps.html) | | uv-subsection | [分段器](https://www.uvui.cn/components/subsection.html) | | uv-sticky | [吸顶](https://www.uvui.cn/components/sticky.html) | | uv-parse | [富文本解析器](https://www.uvui.cn/components/parse.html) | | uv-overlay | [遮罩层](https://www.uvui.cn/components/overlay.html) | | uv-code-input | [验证码输入](https://www.uvui.cn/components/codeInput.html) | | uv-read-more | [展开阅读更多](https://www.uvui.cn/components/readMore.html) | | uv-line | [线条](https://www.uvui.cn/components/line.html) | | uv-gap | [间隔槽](https://www.uvui.cn/components/gap.html) | | uv-divider | [分割线](https://www.uvui.cn/components/divider.html) | ## 版权信息 uv-ui遵循[MIT](https://en.wikipedia.org/wiki/MIT_License)开源协议,意味着您无需支付任何费用,也无需授权,即可将uv-ui应用到您的产品中。 ## 作者想说 - 开源真的不易,不图大家的钱财,所以希望大家多多鼓励支持,希望不要恶意评论,有问题加群快速解决。 - 遇到BUG,是一件很正常的事情,是程序肯定就有BUG,所以希望大家能以理解的心态去提出BUG,然后作者才有动力去努力修复。 - 最后觉得好用的小伙伴,不要吝啬你的双手,给个好评就是给我们最大的鼓励。 # 恶评者手下留情,有事加QQ群解决:549833913 ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/changelog.md ================================================ ## 1.1.19(2023-10-13) 1. 兼容vue3 ## 1.1.18(2023-10-12) 1. 1.1.15版本 ## 1.1.17(2023-09-27) 1. 1.1.14版本发布 ## 1.1.16(2023-09-15) 1. 1.1.13版本发布 ## 1.1.15(2023-09-15) 1. 更新button.js相关按钮支持open-type="agreePrivacyAuthorization" ## 1.1.14(2023-09-14) 1. 优化dayjs ## 1.1.13(2023-09-13) 1. 优化,$uv中增加unit参数,方便组件中使用 ## 1.1.12(2023-09-10) 1. 升级版本 ## 1.1.11(2023-09-04) 1. 1.1.11版本 ## 1.1.10(2023-08-31) 1. 修复customStyle和customClass存在冲突的问题 ## 1.1.9(2023-08-27) 1. 版本升级 2. 优化 ## 1.1.8(2023-08-24) 1. 版本升级 ## 1.1.7(2023-08-22) 1. 版本升级 ## 1.1.6(2023-08-18) uvui版本:1.1.6 ## 1.0.15(2023-08-14) 1. 更新uvui版本号 ## 1.0.13(2023-08-06) 1. 优化 ## 1.0.12(2023-08-06) 1. 修改版本号 ## 1.0.11(2023-08-06) 1. 路由增加events参数 2. 路由拦截修复 ## 1.0.10(2023-08-01) 1. 优化 ## 1.0.9(2023-06-28) 优化openType.js ## 1.0.8(2023-06-15) 1. 修改支付宝报错的BUG ## 1.0.7(2023-06-07) 1. 解决微信小程序使用uvui提示 Some selectors are not allowed in component wxss, including tag name selectors, ID selectors, and attribute selectors 2. 解决上述提示,需要在uni.scss配置$uvui-nvue-style: false; 然后在APP.vue下面引入uvui内置的基础样式:@import '@/uni_modules/uv-ui-tools/index.scss'; ## 1.0.6(2023-06-04) 1. uv-ui-tools 优化工具组件,兼容更多功能 2. 小程序分享功能优化等 ## 1.0.5(2023-06-02) 1. 修改扩展使用mixin中方法的问题 ## 1.0.4(2023-05-23) 1. 兼容百度小程序修改bem函数 ## 1.0.3(2023-05-16) 1. 优化组件依赖,修改后无需全局引入,组件导入即可使用 2. 优化部分功能 ## 1.0.2(2023-05-10) 1. 增加Http请求封装 2. 优化 ## 1.0.1(2023-05-04) 1. 修改名称及备注 ## 1.0.0(2023-05-04) 1. uv-ui工具集首次发布 ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/components/uv-ui-tools/uv-ui-tools.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/index.js ================================================ // 全局挂载引入http相关请求拦截插件 import Request from './libs/luch-request' // 引入全局mixin import mixin from './libs/mixin/mixin.js' // 小程序特有的mixin import mpMixin from './libs/mixin/mpMixin.js' // #ifdef MP import mpShare from '@/uni_modules/uv-ui-tools/libs/mixin/mpShare.js' // #endif // 路由封装 import route from './libs/util/route.js' // 公共工具函数 import * as index from './libs/function/index.js' // 防抖方法 import debounce from './libs/function/debounce.js' // 节流方法 import throttle from './libs/function/throttle.js' // 规则检验 import * as test from './libs/function/test.js' // 颜色渐变相关,colorGradient-颜色渐变,hexToRgb-十六进制颜色转rgb颜色,rgbToHex-rgb转十六进制 import * as colorGradient from './libs/function/colorGradient.js' // 配置信息 import config from './libs/config/config.js' // 平台 import platform from './libs/function/platform' const $uv = { route, config, test, date: index.timeFormat, // 另名date ...index, colorGradient: colorGradient.colorGradient, hexToRgb: colorGradient.hexToRgb, rgbToHex: colorGradient.rgbToHex, colorToRgba: colorGradient.colorToRgba, http: new Request(), debounce, throttle, platform, mixin, mpMixin } uni.$uv = $uv; const install = (Vue,options={}) => { // #ifndef APP-NVUE const cloneMixin = index.deepClone(mixin); delete cloneMixin?.props?.customClass; delete cloneMixin?.props?.customStyle; Vue.mixin(cloneMixin); // #ifdef MP if(options.mpShare){ Vue.mixin(mpShare); } // #endif // #endif // #ifdef VUE2 // 时间格式化,同时两个名称,date和timeFormat Vue.filter('timeFormat', (timestamp, format) => uni.$uv.timeFormat(timestamp, format)); Vue.filter('date', (timestamp, format) => uni.$uv.timeFormat(timestamp, format)); // 将多久以前的方法,注入到全局过滤器 Vue.filter('timeFrom', (timestamp, format) => uni.$uv.timeFrom(timestamp, format)); // 同时挂载到uni和Vue.prototype中 // #ifndef APP-NVUE // 只有vue,挂载到Vue.prototype才有意义,因为nvue中全局Vue.prototype和Vue.mixin是无效的 Vue.prototype.$uv = $uv; // #endif // #endif // #ifdef VUE3 Vue.config.globalProperties.$uv = $uv; // #endif } export default { install } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/index.scss ================================================ // 引入公共基础类 @import "./libs/css/common.scss"; // 非nvue的样式 /* #ifndef APP-NVUE */ @import "./libs/css/vue.scss"; /* #endif */ ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/config/config.js ================================================ // 此版本发布于2023-10-12 const version = '1.1.15' // 开发环境才提示,生产环境不会提示 if (process.env.NODE_ENV === 'development') { console.log(`\n %c uvui V${version} https://www.uvui.cn/ \n\n`, 'color: #ffffff; background: #3c9cff; padding:5px 0; border-radius: 5px;'); } export default { v: version, version, // 主题名称 type: [ 'primary', 'success', 'info', 'error', 'warning' ], // 颜色部分,本来可以通过scss的:export导出供js使用,但是奈何nvue不支持 color: { 'uv-primary': '#2979ff', 'uv-warning': '#ff9900', 'uv-success': '#19be6b', 'uv-error': '#fa3534', 'uv-info': '#909399', 'uv-main-color': '#303133', 'uv-content-color': '#606266', 'uv-tips-color': '#909399', 'uv-light-color': '#c0c4cc' }, // 默认单位,可以通过配置为rpx,那么在用于传入组件大小参数为数值时,就默认为rpx unit: 'px' } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/css/color.scss ================================================ $uv-main-color: #303133 !default; $uv-content-color: #606266 !default; $uv-tips-color: #909193 !default; $uv-light-color: #c0c4cc !default; $uv-border-color: #dadbde !default; $uv-bg-color: #f3f4f6 !default; $uv-disabled-color: #c8c9cc !default; $uv-primary: #3c9cff !default; $uv-primary-dark: #398ade !default; $uv-primary-disabled: #9acafc !default; $uv-primary-light: #ecf5ff !default; $uv-warning: #f9ae3d !default; $uv-warning-dark: #f1a532 !default; $uv-warning-disabled: #f9d39b !default; $uv-warning-light: #fdf6ec !default; $uv-success: #5ac725 !default; $uv-success-dark: #53c21d !default; $uv-success-disabled: #a9e08f !default; $uv-success-light: #f5fff0; $uv-error: #f56c6c !default; $uv-error-dark: #e45656 !default; $uv-error-disabled: #f7b2b2 !default; $uv-error-light: #fef0f0 !default; $uv-info: #909399 !default; $uv-info-dark: #767a82 !default; $uv-info-disabled: #c4c6c9 !default; $uv-info-light: #f4f4f5 !default; ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/css/common.scss ================================================ // 超出行数,自动显示行尾省略号,最多5行 // 来自uvui的温馨提示:当您在控制台看到此报错,说明需要在App.vue的style标签加上【lang="scss"】 @for $i from 1 through 5 { .uv-line-#{$i} { /* #ifdef APP-NVUE */ // nvue下,可以直接使用lines属性,这是weex特有样式 lines: $i; text-overflow: ellipsis; overflow: hidden; flex: 1; /* #endif */ /* #ifndef APP-NVUE */ // vue下,单行和多行显示省略号需要单独处理 @if $i == '1' { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } @else { display: -webkit-box!important; overflow: hidden; text-overflow: ellipsis; word-break: break-all; -webkit-line-clamp: $i; -webkit-box-orient: vertical!important; } /* #endif */ } } $uv-bordercolor: #dadbde; @if variable-exists(uv-border-color) { $uv-bordercolor: $uv-border-color; } // 此处加上!important并非随意乱用,而是因为目前*.nvue页面编译到H5时, // App.vue的样式会被uni-app的view元素的自带border属性覆盖,导致无效 // 综上,这是uni-app的缺陷导致我们为了多端兼容,而必须要加上!important // 移动端兼容性较好,直接使用0.5px去实现细边框,不使用伪元素形式实现 .uv-border { border-width: 0.5px!important; border-color: $uv-bordercolor!important; border-style: solid; } .uv-border-top { border-top-width: 0.5px!important; border-color: $uv-bordercolor!important; border-top-style: solid; } .uv-border-left { border-left-width: 0.5px!important; border-color: $uv-bordercolor!important; border-left-style: solid; } .uv-border-right { border-right-width: 0.5px!important; border-color: $uv-bordercolor!important; border-right-style: solid; } .uv-border-bottom { border-bottom-width: 0.5px!important; border-color: $uv-bordercolor!important; border-bottom-style: solid; } .uv-border-top-bottom { border-top-width: 0.5px!important; border-bottom-width: 0.5px!important; border-color: $uv-bordercolor!important; border-top-style: solid; border-bottom-style: solid; } // 去除button的所有默认样式,让其表现跟普通的view、text元素一样 .uv-reset-button { padding: 0; background-color: transparent; /* #ifndef APP-PLUS */ font-size: inherit; line-height: inherit; color: inherit; /* #endif */ /* #ifdef APP-NVUE */ border-width: 0; /* #endif */ } /* #ifndef APP-NVUE */ .uv-reset-button::after { border: none; } /* #endif */ .uv-hover-class { opacity: 0.7; } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/css/components.scss ================================================ @mixin flex($direction: row) { /* #ifndef APP-NVUE */ display: flex; /* #endif */ flex-direction: $direction; } /* #ifndef APP-NVUE */ // 由于uvui是基于nvue环境进行开发的,此环境中普通元素默认为flex-direction: column; // 所以在非nvue中,需要对元素进行重置为flex-direction: column; 否则可能会表现异常 $uvui-nvue-style: true !default; @if $uvui-nvue-style == true { view, scroll-view, swiper-item { display: flex; flex-direction: column; flex-shrink: 0; flex-grow: 0; flex-basis: auto; align-items: stretch; align-content: flex-start; } } /* #endif */ ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/css/variable.scss ================================================ // 超出行数,自动显示行尾省略号,最多5行 // 来自uvui的温馨提示:当您在控制台看到此报错,说明需要在App.vue的style标签加上【lang="scss"】 @if variable-exists(show-lines) { @for $i from 1 through 5 { .uv-line-#{$i} { /* #ifdef APP-NVUE */ // nvue下,可以直接使用lines属性,这是weex特有样式 lines: $i; text-overflow: ellipsis; overflow: hidden; flex: 1; /* #endif */ /* #ifndef APP-NVUE */ // vue下,单行和多行显示省略号需要单独处理 @if $i == '1' { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } @else { display: -webkit-box!important; overflow: hidden; text-overflow: ellipsis; word-break: break-all; -webkit-line-clamp: $i; -webkit-box-orient: vertical!important; } /* #endif */ } } } @if variable-exists(show-border) { $uv-bordercolor: #dadbde; @if variable-exists(uv-border-color) { $uv-bordercolor: $uv-border-color; } // 此处加上!important并非随意乱用,而是因为目前*.nvue页面编译到H5时, // App.vue的样式会被uni-app的view元素的自带border属性覆盖,导致无效 // 综上,这是uni-app的缺陷导致我们为了多端兼容,而必须要加上!important // 移动端兼容性较好,直接使用0.5px去实现细边框,不使用伪元素形式实现 @if variable-exists(show-border-surround) { .uv-border { border-width: 0.5px!important; border-color: $uv-bordercolor!important; border-style: solid; } } @if variable-exists(show-border-top) { .uv-border-top { border-top-width: 0.5px!important; border-color: $uv-bordercolor!important; border-top-style: solid; } } @if variable-exists(show-border-left) { .uv-border-left { border-left-width: 0.5px!important; border-color: $uv-bordercolor!important; border-left-style: solid; } } @if variable-exists(show-border-right) { .uv-border-right { border-right-width: 0.5px!important; border-color: $uv-bordercolor!important; border-right-style: solid; } } @if variable-exists(show-border-bottom) { .uv-border-bottom { border-bottom-width: 0.5px!important; border-color: $uv-bordercolor!important; border-bottom-style: solid; } } @if variable-exists(show-border-top-bottom) { .uv-border-top-bottom { border-top-width: 0.5px!important; border-bottom-width: 0.5px!important; border-color: $uv-bordercolor!important; border-top-style: solid; border-bottom-style: solid; } } } @if variable-exists(show-reset-button) { // 去除button的所有默认样式,让其表现跟普通的view、text元素一样 .uv-reset-button { padding: 0; background-color: transparent; /* #ifndef APP-PLUS */ font-size: inherit; line-height: inherit; color: inherit; /* #endif */ /* #ifdef APP-NVUE */ border-width: 0; /* #endif */ } /* #ifndef APP-NVUE */ .uv-reset-button::after { border: none; } /* #endif */ } @if variable-exists(show-hover) { .uv-hover-class { opacity: 0.7; } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/css/vue.scss ================================================ // 历遍生成4个方向的底部安全区 @each $d in top, right, bottom, left { .uv-safe-area-inset-#{$d} { padding-#{$d}: 0; padding-#{$d}: constant(safe-area-inset-#{$d}); padding-#{$d}: env(safe-area-inset-#{$d}); } } //提升H5端uni.toast()的层级,避免被uvui的modal等遮盖 /* #ifdef H5 */ uni-toast { z-index: 10090; } uni-toast .uni-toast { z-index: 10090; } /* #endif */ // 隐藏scroll-view的滚动条 ::-webkit-scrollbar { display: none; width: 0 !important; height: 0 !important; -webkit-appearance: none; background: transparent; } $uvui-nvue-style: true !default; @if $uvui-nvue-style == false { view, scroll-view, swiper-item { display: flex; flex-direction: column; flex-shrink: 0; flex-grow: 0; flex-basis: auto; align-items: stretch; align-content: flex-start; } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/function/colorGradient.js ================================================ /** * 求两个颜色之间的渐变值 * @param {string} startColor 开始的颜色 * @param {string} endColor 结束的颜色 * @param {number} step 颜色等分的份额 * */ function colorGradient(startColor = 'rgb(0, 0, 0)', endColor = 'rgb(255, 255, 255)', step = 10) { const startRGB = hexToRgb(startColor, false) // 转换为rgb数组模式 const startR = startRGB[0] const startG = startRGB[1] const startB = startRGB[2] const endRGB = hexToRgb(endColor, false) const endR = endRGB[0] const endG = endRGB[1] const endB = endRGB[2] const sR = (endR - startR) / step // 总差值 const sG = (endG - startG) / step const sB = (endB - startB) / step const colorArr = [] for (let i = 0; i < step; i++) { // 计算每一步的hex值 let hex = rgbToHex(`rgb(${Math.round((sR * i + startR))},${Math.round((sG * i + startG))},${Math.round((sB * i + startB))})`) // 确保第一个颜色值为startColor的值 if (i === 0) hex = rgbToHex(startColor) // 确保最后一个颜色值为endColor的值 if (i === step - 1) hex = rgbToHex(endColor) colorArr.push(hex) } return colorArr } // 将hex表示方式转换为rgb表示方式(这里返回rgb数组模式) function hexToRgb(sColor, str = true) { const reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/ sColor = String(sColor).toLowerCase() if (sColor && reg.test(sColor)) { if (sColor.length === 4) { let sColorNew = '#' for (let i = 1; i < 4; i += 1) { sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1)) } sColor = sColorNew } // 处理六位的颜色值 const sColorChange = [] for (let i = 1; i < 7; i += 2) { sColorChange.push(parseInt(`0x${sColor.slice(i, i + 2)}`)) } if (!str) { return sColorChange } return `rgb(${sColorChange[0]},${sColorChange[1]},${sColorChange[2]})` } if (/^(rgb|RGB)/.test(sColor)) { const arr = sColor.replace(/(?:\(|\)|rgb|RGB)*/g, '').split(',') return arr.map((val) => Number(val)) } return sColor } // 将rgb表示方式转换为hex表示方式 function rgbToHex(rgb) { const _this = rgb const reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/ if (/^(rgb|RGB)/.test(_this)) { const aColor = _this.replace(/(?:\(|\)|rgb|RGB)*/g, '').split(',') let strHex = '#' for (let i = 0; i < aColor.length; i++) { let hex = Number(aColor[i]).toString(16) hex = String(hex).length == 1 ? `${0}${hex}` : hex // 保证每个rgb的值为2位 if (hex === '0') { hex += hex } strHex += hex } if (strHex.length !== 7) { strHex = _this } return strHex } if (reg.test(_this)) { const aNum = _this.replace(/#/, '').split('') if (aNum.length === 6) { return _this } if (aNum.length === 3) { let numHex = '#' for (let i = 0; i < aNum.length; i += 1) { numHex += (aNum[i] + aNum[i]) } return numHex } } else { return _this } } /** * JS颜色十六进制转换为rgb或rgba,返回的格式为 rgba(255,255,255,0.5)字符串 * sHex为传入的十六进制的色值 * alpha为rgba的透明度 */ function colorToRgba(color, alpha) { color = rgbToHex(color) // 十六进制颜色值的正则表达式 const reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/ /* 16进制颜色转为RGB格式 */ let sColor = String(color).toLowerCase() if (sColor && reg.test(sColor)) { if (sColor.length === 4) { let sColorNew = '#' for (let i = 1; i < 4; i += 1) { sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1)) } sColor = sColorNew } // 处理六位的颜色值 const sColorChange = [] for (let i = 1; i < 7; i += 2) { sColorChange.push(parseInt(`0x${sColor.slice(i, i + 2)}`)) } // return sColorChange.join(',') return `rgba(${sColorChange.join(',')},${alpha})` } return sColor } export { colorGradient, hexToRgb, rgbToHex, colorToRgba } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/function/debounce.js ================================================ let timeout = null /** * 防抖原理:一定时间内,只有最后一次操作,再过wait毫秒后才执行函数 * * @param {Function} func 要执行的回调函数 * @param {Number} wait 延时的时间 * @param {Boolean} immediate 是否立即执行 * @return null */ function debounce(func, wait = 500, immediate = false) { // 清除定时器 if (timeout !== null) clearTimeout(timeout) // 立即执行,此类情况一般用不到 if (immediate) { const callNow = !timeout timeout = setTimeout(() => { timeout = null }, wait) if (callNow) typeof func === 'function' && func() } else { // 设置定时器,当最后一次操作后,timeout不会再被清除,所以在延时wait毫秒后执行func回调方法 timeout = setTimeout(() => { typeof func === 'function' && func() }, wait) } } export default debounce ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/function/digit.js ================================================ let _boundaryCheckingState = true; // 是否进行越界检查的全局开关 /** * 把错误的数据转正 * @private * @example strip(0.09999999999999998)=0.1 */ function strip(num, precision = 15) { return +parseFloat(Number(num).toPrecision(precision)); } /** * Return digits length of a number * @private * @param {*number} num Input number */ function digitLength(num) { // Get digit length of e const eSplit = num.toString().split(/[eE]/); const len = (eSplit[0].split('.')[1] || '').length - +(eSplit[1] || 0); return len > 0 ? len : 0; } /** * 把小数转成整数,如果是小数则放大成整数 * @private * @param {*number} num 输入数 */ function float2Fixed(num) { if (num.toString().indexOf('e') === -1) { return Number(num.toString().replace('.', '')); } const dLen = digitLength(num); return dLen > 0 ? strip(Number(num) * Math.pow(10, dLen)) : Number(num); } /** * 检测数字是否越界,如果越界给出提示 * @private * @param {*number} num 输入数 */ function checkBoundary(num) { if (_boundaryCheckingState) { if (num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) { console.warn(`${num} 超出了精度限制,结果可能不正确`); } } } /** * 把递归操作扁平迭代化 * @param {number[]} arr 要操作的数字数组 * @param {function} operation 迭代操作 * @private */ function iteratorOperation(arr, operation) { const [num1, num2, ...others] = arr; let res = operation(num1, num2); others.forEach((num) => { res = operation(res, num); }); return res; } /** * 高精度乘法 * @export */ export function times(...nums) { if (nums.length > 2) { return iteratorOperation(nums, times); } const [num1, num2] = nums; const num1Changed = float2Fixed(num1); const num2Changed = float2Fixed(num2); const baseNum = digitLength(num1) + digitLength(num2); const leftValue = num1Changed * num2Changed; checkBoundary(leftValue); return leftValue / Math.pow(10, baseNum); } /** * 高精度加法 * @export */ export function plus(...nums) { if (nums.length > 2) { return iteratorOperation(nums, plus); } const [num1, num2] = nums; // 取最大的小数位 const baseNum = Math.pow(10, Math.max(digitLength(num1), digitLength(num2))); // 把小数都转为整数然后再计算 return (times(num1, baseNum) + times(num2, baseNum)) / baseNum; } /** * 高精度减法 * @export */ export function minus(...nums) { if (nums.length > 2) { return iteratorOperation(nums, minus); } const [num1, num2] = nums; const baseNum = Math.pow(10, Math.max(digitLength(num1), digitLength(num2))); return (times(num1, baseNum) - times(num2, baseNum)) / baseNum; } /** * 高精度除法 * @export */ export function divide(...nums) { if (nums.length > 2) { return iteratorOperation(nums, divide); } const [num1, num2] = nums; const num1Changed = float2Fixed(num1); const num2Changed = float2Fixed(num2); checkBoundary(num1Changed); checkBoundary(num2Changed); // 重要,这里必须用strip进行修正 return times(num1Changed / num2Changed, strip(Math.pow(10, digitLength(num2) - digitLength(num1)))); } /** * 四舍五入 * @export */ export function round(num, ratio) { const base = Math.pow(10, ratio); let result = divide(Math.round(Math.abs(times(num, base))), base); if (num < 0 && result !== 0) { result = times(result, -1); } // 位数不足则补0 return result; } /** * 是否进行边界检查,默认开启 * @param flag 标记开关,true 为开启,false 为关闭,默认为 true * @export */ export function enableBoundaryChecking(flag = true) { _boundaryCheckingState = flag; } export default { times, plus, minus, divide, round, enableBoundaryChecking, }; ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/function/index.js ================================================ import { number, empty } from './test.js' import { round } from './digit.js' /** * @description 如果value小于min,取min;如果value大于max,取max * @param {number} min * @param {number} max * @param {number} value */ function range(min = 0, max = 0, value = 0) { return Math.max(min, Math.min(max, Number(value))) } /** * @description 用于获取用户传递值的px值 如果用户传递了"xxpx"或者"xxrpx",取出其数值部分,如果是"xxxrpx"还需要用过uni.upx2px进行转换 * @param {number|string} value 用户传递值的px值 * @param {boolean} unit * @returns {number|string} */ function getPx(value, unit = false) { if (number(value)) { return unit ? `${value}px` : Number(value) } // 如果带有rpx,先取出其数值部分,再转为px值 if (/(rpx|upx)$/.test(value)) { return unit ? `${uni.upx2px(parseInt(value))}px` : Number(uni.upx2px(parseInt(value))) } return unit ? `${parseInt(value)}px` : parseInt(value) } /** * @description 进行延时,以达到可以简写代码的目的 比如: await uni.$uv.sleep(20)将会阻塞20ms * @param {number} value 堵塞时间 单位ms 毫秒 * @returns {Promise} 返回promise */ function sleep(value = 30) { return new Promise((resolve) => { setTimeout(() => { resolve() }, value) }) } /** * @description 运行期判断平台 * @returns {string} 返回所在平台(小写) * @link 运行期判断平台 https://uniapp.dcloud.io/frame?id=判断平台 */ function os() { return uni.getSystemInfoSync().platform.toLowerCase() } /** * @description 获取系统信息同步接口 * @link 获取系统信息同步接口 https://uniapp.dcloud.io/api/system/info?id=getsysteminfosync */ function sys() { return uni.getSystemInfoSync() } /** * @description 取一个区间数 * @param {Number} min 最小值 * @param {Number} max 最大值 */ function random(min, max) { if (min >= 0 && max > 0 && max >= min) { const gab = max - min + 1 return Math.floor(Math.random() * gab + min) } return 0 } /** * @param {Number} len uuid的长度 * @param {Boolean} firstU 将返回的首字母置为"u" * @param {Nubmer} radix 生成uuid的基数(意味着返回的字符串都是这个基数),2-二进制,8-八进制,10-十进制,16-十六进制 */ function guid(len = 32, firstU = true, radix = null) { const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('') const uuid = [] radix = radix || chars.length if (len) { // 如果指定uuid长度,只是取随机的字符,0|x为位运算,能去掉x的小数位,返回整数位 for (let i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix] } else { let r // rfc4122标准要求返回的uuid中,某些位为固定的字符 uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-' uuid[14] = '4' for (let i = 0; i < 36; i++) { if (!uuid[i]) { r = 0 | Math.random() * 16 uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r] } } } // 移除第一个字符,并用u替代,因为第一个字符为数值时,该guuid不能用作id或者class if (firstU) { uuid.shift() return `u${uuid.join('')}` } return uuid.join('') } /** * @description 获取父组件的参数,因为支付宝小程序不支持provide/inject的写法 this.$parent在非H5中,可以准确获取到父组件,但是在H5中,需要多次this.$parent.$parent.xxx 这里默认值等于undefined有它的含义,因为最顶层元素(组件)的$parent就是undefined,意味着不传name 值(默认为undefined),就是查找最顶层的$parent * @param {string|undefined} name 父组件的参数名 */ function $parent(name = undefined) { let parent = this.$parent // 通过while历遍,这里主要是为了H5需要多层解析的问题 while (parent) { // 父组件 if (parent.$options && parent.$options.name !== name) { // 如果组件的name不相等,继续上一级寻找 parent = parent.$parent } else { return parent } } return false } /** * @description 样式转换 * 对象转字符串,或者字符串转对象 * @param {object | string} customStyle 需要转换的目标 * @param {String} target 转换的目的,object-转为对象,string-转为字符串 * @returns {object|string} */ function addStyle(customStyle, target = 'object') { // 字符串转字符串,对象转对象情形,直接返回 if (empty(customStyle) || typeof(customStyle) === 'object' && target === 'object' || target === 'string' && typeof(customStyle) === 'string') { return customStyle } // 字符串转对象 if (target === 'object') { // 去除字符串样式中的两端空格(中间的空格不能去掉,比如padding: 20px 0如果去掉了就错了),空格是无用的 customStyle = trim(customStyle) // 根据";"将字符串转为数组形式 const styleArray = customStyle.split(';') const style = {} // 历遍数组,拼接成对象 for (let i = 0; i < styleArray.length; i++) { // 'font-size:20px;color:red;',如此最后字符串有";"的话,会导致styleArray最后一个元素为空字符串,这里需要过滤 if (styleArray[i]) { const item = styleArray[i].split(':') style[trim(item[0])] = trim(item[1]) } } return style } // 这里为对象转字符串形式 let string = '' for (const i in customStyle) { // 驼峰转为中划线的形式,否则css内联样式,无法识别驼峰样式属性名 const key = i.replace(/([A-Z])/g, '-$1').toLowerCase() string += `${key}:${customStyle[i]};` } // 去除两端空格 return trim(string) } /** * @description 添加单位,如果有rpx,upx,%,px等单位结尾或者值为auto,直接返回,否则加上px单位结尾 * @param {string|number} value 需要添加单位的值 * @param {string} unit 添加的单位名 比如px */ function addUnit(value = 'auto', unit = uni?.$uv?.config?.unit ? uni?.$uv?.config?.unit : 'px') { value = String(value) // 用uvui内置验证规则中的number判断是否为数值 return number(value) ? `${value}${unit}` : value } /** * @description 深度克隆 * @param {object} obj 需要深度克隆的对象 * @param cache 缓存 * @returns {*} 克隆后的对象或者原值(不是对象) */ function deepClone(obj, cache = new WeakMap()) { if (obj === null || typeof obj !== 'object') return obj; if (cache.has(obj)) return cache.get(obj); let clone; if (obj instanceof Date) { clone = new Date(obj.getTime()); } else if (obj instanceof RegExp) { clone = new RegExp(obj); } else if (obj instanceof Map) { clone = new Map(Array.from(obj, ([key, value]) => [key, deepClone(value, cache)])); } else if (obj instanceof Set) { clone = new Set(Array.from(obj, value => deepClone(value, cache))); } else if (Array.isArray(obj)) { clone = obj.map(value => deepClone(value, cache)); } else if (Object.prototype.toString.call(obj) === '[object Object]') { clone = Object.create(Object.getPrototypeOf(obj)); cache.set(obj, clone); for (const [key, value] of Object.entries(obj)) { clone[key] = deepClone(value, cache); } } else { clone = Object.assign({}, obj); } cache.set(obj, clone); return clone; } /** * @description JS对象深度合并 * @param {object} target 需要拷贝的对象 * @param {object} source 拷贝的来源对象 * @returns {object|boolean} 深度合并后的对象或者false(入参有不是对象) */ function deepMerge(target = {}, source = {}) { target = deepClone(target) if (typeof target !== 'object' || target === null || typeof source !== 'object' || source === null) return target; const merged = Array.isArray(target) ? target.slice() : Object.assign({}, target); for (const prop in source) { if (!source.hasOwnProperty(prop)) continue; const sourceValue = source[prop]; const targetValue = merged[prop]; if (sourceValue instanceof Date) { merged[prop] = new Date(sourceValue); } else if (sourceValue instanceof RegExp) { merged[prop] = new RegExp(sourceValue); } else if (sourceValue instanceof Map) { merged[prop] = new Map(sourceValue); } else if (sourceValue instanceof Set) { merged[prop] = new Set(sourceValue); } else if (typeof sourceValue === 'object' && sourceValue !== null) { merged[prop] = deepMerge(targetValue, sourceValue); } else { merged[prop] = sourceValue; } } return merged; } /** * @description error提示 * @param {*} err 错误内容 */ function error(err) { // 开发环境才提示,生产环境不会提示 if (process.env.NODE_ENV === 'development') { console.error(`uvui提示:${err}`) } } /** * @description 打乱数组 * @param {array} array 需要打乱的数组 * @returns {array} 打乱后的数组 */ function randomArray(array = []) { // 原理是sort排序,Math.random()产生0<= x < 1之间的数,会导致x-0.05大于或者小于0 return array.sort(() => Math.random() - 0.5) } // padStart 的 polyfill,因为某些机型或情况,还无法支持es7的padStart,比如电脑版的微信小程序 // 所以这里做一个兼容polyfill的兼容处理 if (!String.prototype.padStart) { // 为了方便表示这里 fillString 用了ES6 的默认参数,不影响理解 String.prototype.padStart = function(maxLength, fillString = ' ') { if (Object.prototype.toString.call(fillString) !== '[object String]') { throw new TypeError( 'fillString must be String' ) } const str = this // 返回 String(str) 这里是为了使返回的值是字符串字面量,在控制台中更符合直觉 if (str.length >= maxLength) return String(str) const fillLength = maxLength - str.length let times = Math.ceil(fillLength / fillString.length) while (times >>= 1) { fillString += fillString if (times === 1) { fillString += fillString } } return fillString.slice(0, fillLength) + str } } /** * @description 格式化时间 * @param {String|Number} dateTime 需要格式化的时间戳 * @param {String} fmt 格式化规则 yyyy:mm:dd|yyyy:mm|yyyy年mm月dd日|yyyy年mm月dd日 hh时MM分等,可自定义组合 默认yyyy-mm-dd * @returns {string} 返回格式化后的字符串 */ function timeFormat(dateTime = null, formatStr = 'yyyy-mm-dd') { let date // 若传入时间为假值,则取当前时间 if (!dateTime) { date = new Date() } // 若为unix秒时间戳,则转为毫秒时间戳(逻辑有点奇怪,但不敢改,以保证历史兼容) else if (/^\d{10}$/.test(dateTime?.toString().trim())) { date = new Date(dateTime * 1000) } // 若用户传入字符串格式时间戳,new Date无法解析,需做兼容 else if (typeof dateTime === 'string' && /^\d+$/.test(dateTime.trim())) { date = new Date(Number(dateTime)) } // 处理平台性差异,在Safari/Webkit中,new Date仅支持/作为分割符的字符串时间 // 处理 '2022-07-10 01:02:03',跳过 '2022-07-10T01:02:03' else if (typeof dateTime === 'string' && dateTime.includes('-') && !dateTime.includes('T')) { date = new Date(dateTime.replace(/-/g, '/')) } // 其他都认为符合 RFC 2822 规范 else { date = new Date(dateTime) } const timeSource = { 'y': date.getFullYear().toString(), // 年 'm': (date.getMonth() + 1).toString().padStart(2, '0'), // 月 'd': date.getDate().toString().padStart(2, '0'), // 日 'h': date.getHours().toString().padStart(2, '0'), // 时 'M': date.getMinutes().toString().padStart(2, '0'), // 分 's': date.getSeconds().toString().padStart(2, '0') // 秒 // 有其他格式化字符需求可以继续添加,必须转化成字符串 } for (const key in timeSource) { const [ret] = new RegExp(`${key}+`).exec(formatStr) || [] if (ret) { // 年可能只需展示两位 const beginIndex = key === 'y' && ret.length === 2 ? 2 : 0 formatStr = formatStr.replace(ret, timeSource[key].slice(beginIndex)) } } return formatStr } /** * @description 时间戳转为多久之前 * @param {String|Number} timestamp 时间戳 * @param {String|Boolean} format * 格式化规则如果为时间格式字符串,超出一定时间范围,返回固定的时间格式; * 如果为布尔值false,无论什么时间,都返回多久以前的格式 * @returns {string} 转化后的内容 */ function timeFrom(timestamp = null, format = 'yyyy-mm-dd') { if (timestamp == null) timestamp = Number(new Date()) timestamp = parseInt(timestamp) // 判断用户输入的时间戳是秒还是毫秒,一般前端js获取的时间戳是毫秒(13位),后端传过来的为秒(10位) if (timestamp.toString().length == 10) timestamp *= 1000 let timer = (new Date()).getTime() - timestamp timer = parseInt(timer / 1000) // 如果小于5分钟,则返回"刚刚",其他以此类推 let tips = '' switch (true) { case timer < 300: tips = '刚刚' break case timer >= 300 && timer < 3600: tips = `${parseInt(timer / 60)}分钟前` break case timer >= 3600 && timer < 86400: tips = `${parseInt(timer / 3600)}小时前` break case timer >= 86400 && timer < 2592000: tips = `${parseInt(timer / 86400)}天前` break default: // 如果format为false,则无论什么时间戳,都显示xx之前 if (format === false) { if (timer >= 2592000 && timer < 365 * 86400) { tips = `${parseInt(timer / (86400 * 30))}个月前` } else { tips = `${parseInt(timer / (86400 * 365))}年前` } } else { tips = timeFormat(timestamp, format) } } return tips } /** * @description 去除空格 * @param String str 需要去除空格的字符串 * @param String pos both(左右)|left|right|all 默认both */ function trim(str, pos = 'both') { str = String(str) if (pos == 'both') { return str.replace(/^\s+|\s+$/g, '') } if (pos == 'left') { return str.replace(/^\s*/, '') } if (pos == 'right') { return str.replace(/(\s*$)/g, '') } if (pos == 'all') { return str.replace(/\s+/g, '') } return str } /** * @description 对象转url参数 * @param {object} data,对象 * @param {Boolean} isPrefix,是否自动加上"?" * @param {string} arrayFormat 规则 indices|brackets|repeat|comma */ function queryParams(data = {}, isPrefix = true, arrayFormat = 'brackets') { const prefix = isPrefix ? '?' : '' const _result = [] if (['indices', 'brackets', 'repeat', 'comma'].indexOf(arrayFormat) == -1) arrayFormat = 'brackets' for (const key in data) { const value = data[key] // 去掉为空的参数 if (['', undefined, null].indexOf(value) >= 0) { continue } // 如果值为数组,另行处理 if (value.constructor === Array) { // e.g. {ids: [1, 2, 3]} switch (arrayFormat) { case 'indices': // 结果: ids[0]=1&ids[1]=2&ids[2]=3 for (let i = 0; i < value.length; i++) { _result.push(`${key}[${i}]=${value[i]}`) } break case 'brackets': // 结果: ids[]=1&ids[]=2&ids[]=3 value.forEach((_value) => { _result.push(`${key}[]=${_value}`) }) break case 'repeat': // 结果: ids=1&ids=2&ids=3 value.forEach((_value) => { _result.push(`${key}=${_value}`) }) break case 'comma': // 结果: ids=1,2,3 let commaStr = '' value.forEach((_value) => { commaStr += (commaStr ? ',' : '') + _value }) _result.push(`${key}=${commaStr}`) break default: value.forEach((_value) => { _result.push(`${key}[]=${_value}`) }) } } else { _result.push(`${key}=${value}`) } } return _result.length ? prefix + _result.join('&') : '' } /** * 显示消息提示框 * @param {String} title 提示的内容,长度与 icon 取值有关。 * @param {Number} duration 提示的延迟时间,单位毫秒,默认:2000 */ function toast(title, duration = 2000) { uni.showToast({ title: String(title), icon: 'none', duration }) } /** * @description 根据主题type值,获取对应的图标 * @param {String} type 主题名称,primary|info|error|warning|success * @param {boolean} fill 是否使用fill填充实体的图标 */ function type2icon(type = 'success', fill = false) { // 如果非预置值,默认为success if (['primary', 'info', 'error', 'warning', 'success'].indexOf(type) == -1) type = 'success' let iconName = '' // 目前(2019-12-12),info和primary使用同一个图标 switch (type) { case 'primary': iconName = 'info-circle' break case 'info': iconName = 'info-circle' break case 'error': iconName = 'close-circle' break case 'warning': iconName = 'error-circle' break case 'success': iconName = 'checkmark-circle' break default: iconName = 'checkmark-circle' } // 是否是实体类型,加上-fill,在icon组件库中,实体的类名是后面加-fill的 if (fill) iconName += '-fill' return iconName } /** * @description 数字格式化 * @param {number|string} number 要格式化的数字 * @param {number} decimals 保留几位小数 * @param {string} decimalPoint 小数点符号 * @param {string} thousandsSeparator 千分位符号 * @returns {string} 格式化后的数字 */ function priceFormat(number, decimals = 0, decimalPoint = '.', thousandsSeparator = ',') { number = (`${number}`).replace(/[^0-9+-Ee.]/g, '') const n = !isFinite(+number) ? 0 : +number const prec = !isFinite(+decimals) ? 0 : Math.abs(decimals) const sep = (typeof thousandsSeparator === 'undefined') ? ',' : thousandsSeparator const dec = (typeof decimalPoint === 'undefined') ? '.' : decimalPoint let s = '' s = (prec ? round(n, prec) + '' : `${Math.round(n)}`).split('.') const re = /(-?\d+)(\d{3})/ while (re.test(s[0])) { s[0] = s[0].replace(re, `$1${sep}$2`) } if ((s[1] || '').length < prec) { s[1] = s[1] || '' s[1] += new Array(prec - s[1].length + 1).join('0') } return s.join(dec) } /** * @description 获取duration值 * 如果带有ms或者s直接返回,如果大于一定值,认为是ms单位,小于一定值,认为是s单位 * 比如以30位阈值,那么300大于30,可以理解为用户想要的是300ms,而不是想花300s去执行一个动画 * @param {String|number} value 比如: "1s"|"100ms"|1|100 * @param {boolean} unit 提示: 如果是false 默认返回number * @return {string|number} */ function getDuration(value, unit = true) { const valueNum = parseInt(value) if (unit) { if (/s$/.test(value)) return value return value > 30 ? `${value}ms` : `${value}s` } if (/ms$/.test(value)) return valueNum if (/s$/.test(value)) return valueNum > 30 ? valueNum : valueNum * 1000 return valueNum } /** * @description 日期的月或日补零操作 * @param {String} value 需要补零的值 */ function padZero(value) { return `00${value}`.slice(-2) } /** * @description 在uv-form的子组件内容发生变化,或者失去焦点时,尝试通知uv-form执行校验方法 * @param {*} instance * @param {*} event */ function formValidate(instance, event) { const formItem = $parent.call(instance, 'uv-form-item') const form = $parent.call(instance, 'uv-form') // 如果发生变化的input或者textarea等,其父组件中有uv-form-item或者uv-form等,就执行form的validate方法 // 同时将form-item的pros传递给form,让其进行精确对象验证 if (formItem && form) { form.validateField(formItem.prop, () => {}, event) } } /** * @description 获取某个对象下的属性,用于通过类似'a.b.c'的形式去获取一个对象的的属性的形式 * @param {object} obj 对象 * @param {string} key 需要获取的属性字段 * @returns {*} */ function getProperty(obj, key) { if (!obj) { return } if (typeof key !== 'string' || key === '') { return '' } if (key.indexOf('.') !== -1) { const keys = key.split('.') let firstObj = obj[keys[0]] || {} for (let i = 1; i < keys.length; i++) { if (firstObj) { firstObj = firstObj[keys[i]] } } return firstObj } return obj[key] } /** * @description 设置对象的属性值,如果'a.b.c'的形式进行设置 * @param {object} obj 对象 * @param {string} key 需要设置的属性 * @param {string} value 设置的值 */ function setProperty(obj, key, value) { if (!obj) { return } // 递归赋值 const inFn = function(_obj, keys, v) { // 最后一个属性key if (keys.length === 1) { _obj[keys[0]] = v return } // 0~length-1个key while (keys.length > 1) { const k = keys[0] if (!_obj[k] || (typeof _obj[k] !== 'object')) { _obj[k] = {} } const key = keys.shift() // 自调用判断是否存在属性,不存在则自动创建对象 inFn(_obj[k], keys, v) } } if (typeof key !== 'string' || key === '') { } else if (key.indexOf('.') !== -1) { // 支持多层级赋值操作 const keys = key.split('.') inFn(obj, keys, value) } else { obj[key] = value } } /** * @description 获取当前页面路径 */ function page() { const pages = getCurrentPages(); const route = pages[pages.length - 1]?.route; // 某些特殊情况下(比如页面进行redirectTo时的一些时机),pages可能为空数组 return `/${route ? route : ''}` } /** * @description 获取当前路由栈实例数组 */ function pages() { const pages = getCurrentPages() return pages } /** * 获取页面历史栈指定层实例 * @param back {number} [0] - 0或者负数,表示获取历史栈的哪一层,0表示获取当前页面实例,-1 表示获取上一个页面实例。默认0。 */ function getHistoryPage(back = 0) { const pages = getCurrentPages() const len = pages.length return pages[len - 1 + back] } /** * @description 修改uvui内置属性值 * @param {object} props 修改内置props属性 * @param {object} config 修改内置config属性 * @param {object} color 修改内置color属性 * @param {object} zIndex 修改内置zIndex属性 */ function setConfig({ props = {}, config = {}, color = {}, zIndex = {} }) { const { deepMerge, } = uni.$uv uni.$uv.config = deepMerge(uni.$uv.config, config) uni.$uv.props = deepMerge(uni.$uv.props, props) uni.$uv.color = deepMerge(uni.$uv.color, color) uni.$uv.zIndex = deepMerge(uni.$uv.zIndex, zIndex) } export { range, getPx, sleep, os, sys, random, guid, $parent, addStyle, addUnit, deepClone, deepMerge, error, randomArray, timeFormat, timeFrom, trim, queryParams, toast, type2icon, priceFormat, getDuration, padZero, formValidate, getProperty, setProperty, page, pages, getHistoryPage, setConfig } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/function/platform.js ================================================ /** * 注意: * 此部分内容,在vue-cli模式下,需要在vue.config.js加入如下内容才有效: * module.exports = { * transpileDependencies: ['uview-v2'] * } */ let platform = 'none' // #ifdef VUE3 platform = 'vue3' // #endif // #ifdef VUE2 platform = 'vue2' // #endif // #ifdef APP-PLUS platform = 'plus' // #endif // #ifdef APP-NVUE platform = 'nvue' // #endif // #ifdef H5 platform = 'h5' // #endif // #ifdef MP-WEIXIN platform = 'weixin' // #endif // #ifdef MP-ALIPAY platform = 'alipay' // #endif // #ifdef MP-BAIDU platform = 'baidu' // #endif // #ifdef MP-TOUTIAO platform = 'toutiao' // #endif // #ifdef MP-QQ platform = 'qq' // #endif // #ifdef MP-KUAISHOU platform = 'kuaishou' // #endif // #ifdef MP-360 platform = '360' // #endif // #ifdef MP platform = 'mp' // #endif // #ifdef QUICKAPP-WEBVIEW platform = 'quickapp-webview' // #endif // #ifdef QUICKAPP-WEBVIEW-HUAWEI platform = 'quickapp-webview-huawei' // #endif // #ifdef QUICKAPP-WEBVIEW-UNION platform = 'quckapp-webview-union' // #endif export default platform ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/function/test.js ================================================ /** * 验证电子邮箱格式 */ function email(value) { return /^\w+((-\w+)|(\.\w+))*\@[A-Za-z0-9]+((\.|-)[A-Za-z0-9]+)*\.[A-Za-z0-9]+$/.test(value) } /** * 验证手机格式 */ function mobile(value) { return /^1([3589]\d|4[5-9]|6[1-2,4-7]|7[0-8])\d{8}$/.test(value) } /** * 验证URL格式 */ function url(value) { return /^((https|http|ftp|rtsp|mms):\/\/)(([0-9a-zA-Z_!~*'().&=+$%-]+: )?[0-9a-zA-Z_!~*'().&=+$%-]+@)?(([0-9]{1,3}.){3}[0-9]{1,3}|([0-9a-zA-Z_!~*'()-]+.)*([0-9a-zA-Z][0-9a-zA-Z-]{0,61})?[0-9a-zA-Z].[a-zA-Z]{2,6})(:[0-9]{1,4})?((\/?)|(\/[0-9a-zA-Z_!~*'().;?:@&=+$,%#-]+)+\/?)$/ .test(value) } /** * 验证日期格式 */ function date(value) { if (!value) return false // 判断是否数值或者字符串数值(意味着为时间戳),转为数值,否则new Date无法识别字符串时间戳 if (number(value)) value = +value return !/Invalid|NaN/.test(new Date(value).toString()) } /** * 验证ISO类型的日期格式 */ function dateISO(value) { return /^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(value) } /** * 验证十进制数字 */ function number(value) { return /^[\+-]?(\d+\.?\d*|\.\d+|\d\.\d+e\+\d+)$/.test(value) } /** * 验证字符串 */ function string(value) { return typeof value === 'string' } /** * 验证整数 */ function digits(value) { return /^\d+$/.test(value) } /** * 验证身份证号码 */ function idCard(value) { return /^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$/.test( value ) } /** * 是否车牌号 */ function carNo(value) { // 新能源车牌 const xreg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}(([0-9]{5}[DF]$)|([DF][A-HJ-NP-Z0-9][0-9]{4}$))/ // 旧车牌 const creg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]{1}$/ if (value.length === 7) { return creg.test(value) } if (value.length === 8) { return xreg.test(value) } return false } /** * 金额,只允许2位小数 */ function amount(value) { // 金额,只允许保留两位小数 return /^[1-9]\d*(,\d{3})*(\.\d{1,2})?$|^0\.\d{1,2}$/.test(value) } /** * 中文 */ function chinese(value) { const reg = /^[\u4e00-\u9fa5]+$/gi return reg.test(value) } /** * 只能输入字母 */ function letter(value) { return /^[a-zA-Z]*$/.test(value) } /** * 只能是字母或者数字 */ function enOrNum(value) { // 英文或者数字 const reg = /^[0-9a-zA-Z]*$/g return reg.test(value) } /** * 验证是否包含某个值 */ function contains(value, param) { return value.indexOf(param) >= 0 } /** * 验证一个值范围[min, max] */ function range(value, param) { return value >= param[0] && value <= param[1] } /** * 验证一个长度范围[min, max] */ function rangeLength(value, param) { return value.length >= param[0] && value.length <= param[1] } /** * 是否固定电话 */ function landline(value) { const reg = /^\d{3,4}-\d{7,8}(-\d{3,4})?$/ return reg.test(value) } /** * 判断是否为空 */ function empty(value) { switch (typeof value) { case 'undefined': return true case 'string': if (value.replace(/(^[ \t\n\r]*)|([ \t\n\r]*$)/g, '').length == 0) return true break case 'boolean': if (!value) return true break case 'number': if (value === 0 || isNaN(value)) return true break case 'object': if (value === null || value.length === 0) return true for (const i in value) { return false } return true } return false } /** * 是否json字符串 */ function jsonString(value) { if (typeof value === 'string') { try { const obj = JSON.parse(value) if (typeof obj === 'object' && obj) { return true } return false } catch (e) { return false } } return false } /** * 是否数组 */ function array(value) { if (typeof Array.isArray === 'function') { return Array.isArray(value) } return Object.prototype.toString.call(value) === '[object Array]' } /** * 是否对象 */ function object(value) { return Object.prototype.toString.call(value) === '[object Object]' } /** * 是否短信验证码 */ function code(value, len = 6) { return new RegExp(`^\\d{${len}}$`).test(value) } /** * 是否函数方法 * @param {Object} value */ function func(value) { return typeof value === 'function' } /** * 是否promise对象 * @param {Object} value */ function promise(value) { return object(value) && func(value.then) && func(value.catch) } /** 是否图片格式 * @param {Object} value */ function image(value) { const newValue = value.split('?')[0] const IMAGE_REGEXP = /\.(jpeg|jpg|gif|png|svg|webp|jfif|bmp|dpg)/i return IMAGE_REGEXP.test(newValue) } /** * 是否视频格式 * @param {Object} value */ function video(value) { const VIDEO_REGEXP = /\.(mp4|mpg|mpeg|dat|asf|avi|rm|rmvb|mov|wmv|flv|mkv|m3u8)/i return VIDEO_REGEXP.test(value) } /** * 是否为正则对象 * @param {Object} * @return {Boolean} */ function regExp(o) { return o && Object.prototype.toString.call(o) === '[object RegExp]' } export { email, mobile, url, date, dateISO, number, digits, idCard, carNo, amount, chinese, letter, enOrNum, contains, range, rangeLength, empty, jsonString, landline, object, array, code, func, promise, video, image, regExp, string } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/function/throttle.js ================================================ let timer; let flag /** * 节流原理:在一定时间内,只能触发一次 * * @param {Function} func 要执行的回调函数 * @param {Number} wait 延时的时间 * @param {Boolean} immediate 是否立即执行 * @return null */ function throttle(func, wait = 500, immediate = true) { if (immediate) { if (!flag) { flag = true // 如果是立即执行,则在wait毫秒内开始时执行 typeof func === 'function' && func() timer = setTimeout(() => { flag = false }, wait) } } else if (!flag) { flag = true // 如果是非立即执行,则在wait毫秒内的结束处执行 timer = setTimeout(() => { flag = false typeof func === 'function' && func() }, wait) } } export default throttle ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/luch-request/adapters/index.js ================================================ import buildURL from '../helpers/buildURL' import buildFullPath from '../core/buildFullPath' import settle from '../core/settle' import { isUndefined } from '../utils' /** * 返回可选值存在的配置 * @param {Array} keys - 可选值数组 * @param {Object} config2 - 配置 * @return {{}} - 存在的配置项 */ const mergeKeys = (keys, config2) => { const config = {} keys.forEach((prop) => { if (!isUndefined(config2[prop])) { config[prop] = config2[prop] } }) return config } export default (config) => new Promise((resolve, reject) => { const fullPath = buildURL(buildFullPath(config.baseURL, config.url), config.params) const _config = { url: fullPath, header: config.header, complete: (response) => { config.fullPath = fullPath response.config = config try { // 对可能字符串不是json 的情况容错 if (typeof response.data === 'string') { response.data = JSON.parse(response.data) } // eslint-disable-next-line no-empty } catch (e) { } settle(resolve, reject, response) } } let requestTask if (config.method === 'UPLOAD') { delete _config.header['content-type'] delete _config.header['Content-Type'] const otherConfig = { // #ifdef MP-ALIPAY fileType: config.fileType, // #endif filePath: config.filePath, name: config.name } const optionalKeys = [ // #ifdef APP-PLUS || H5 'files', // #endif // #ifdef H5 'file', // #endif // #ifdef H5 || APP-PLUS 'timeout', // #endif 'formData' ] requestTask = uni.uploadFile({ ..._config, ...otherConfig, ...mergeKeys(optionalKeys, config) }) } else if (config.method === 'DOWNLOAD') { // #ifdef H5 || APP-PLUS if (!isUndefined(config.timeout)) { _config.timeout = config.timeout } // #endif requestTask = uni.downloadFile(_config) } else { const optionalKeys = [ 'data', 'method', // #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN 'timeout', // #endif 'dataType', // #ifndef MP-ALIPAY 'responseType', // #endif // #ifdef APP-PLUS 'sslVerify', // #endif // #ifdef H5 'withCredentials', // #endif // #ifdef APP-PLUS 'firstIpv4' // #endif ] requestTask = uni.request({ ..._config, ...mergeKeys(optionalKeys, config) }) } if (config.getTask) { config.getTask(requestTask, config) } }) ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/luch-request/core/InterceptorManager.js ================================================ 'use strict' function InterceptorManager() { this.handlers = [] } /** * Add a new interceptor to the stack * * @param {Function} fulfilled The function to handle `then` for a `Promise` * @param {Function} rejected The function to handle `reject` for a `Promise` * * @return {Number} An ID used to remove interceptor later */ InterceptorManager.prototype.use = function use(fulfilled, rejected) { this.handlers.push({ fulfilled, rejected }) return this.handlers.length - 1 } /** * Remove an interceptor from the stack * * @param {Number} id The ID that was returned by `use` */ InterceptorManager.prototype.eject = function eject(id) { if (this.handlers[id]) { this.handlers[id] = null } } /** * Iterate over all the registered interceptors * * This method is particularly useful for skipping over any * interceptors that may have become `null` calling `eject`. * * @param {Function} fn The function to call for each interceptor */ InterceptorManager.prototype.forEach = function forEach(fn) { this.handlers.forEach((h) => { if (h !== null) { fn(h) } }) } export default InterceptorManager ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/luch-request/core/Request.js ================================================ /** * @Class Request * @description luch-request http请求插件 * @version 3.0.7 * @Author lu-ch * @Date 2021-09-04 * @Email webwork.s@qq.com * 文档: https://www.quanzhan.co/luch-request/ * github: https://github.com/lei-mu/luch-request * DCloud: http://ext.dcloud.net.cn/plugin?id=392 * HBuilderX: beat-3.0.4 alpha-3.0.4 */ import dispatchRequest from './dispatchRequest' import InterceptorManager from './InterceptorManager' import mergeConfig from './mergeConfig' import defaults from './defaults' import { isPlainObject } from '../utils' import clone from '../utils/clone' export default class Request { /** * @param {Object} arg - 全局配置 * @param {String} arg.baseURL - 全局根路径 * @param {Object} arg.header - 全局header * @param {String} arg.method = [GET|POST|PUT|DELETE|CONNECT|HEAD|OPTIONS|TRACE] - 全局默认请求方式 * @param {String} arg.dataType = [json] - 全局默认的dataType * @param {String} arg.responseType = [text|arraybuffer] - 全局默认的responseType。支付宝小程序不支持 * @param {Object} arg.custom - 全局默认的自定义参数 * @param {Number} arg.timeout - 全局默认的超时时间,单位 ms。默认60000。H5(HBuilderX 2.9.9+)、APP(HBuilderX 2.9.9+)、微信小程序(2.10.0)、支付宝小程序 * @param {Boolean} arg.sslVerify - 全局默认的是否验证 ssl 证书。默认true.仅App安卓端支持(HBuilderX 2.3.3+) * @param {Boolean} arg.withCredentials - 全局默认的跨域请求时是否携带凭证(cookies)。默认false。仅H5支持(HBuilderX 2.6.15+) * @param {Boolean} arg.firstIpv4 - 全DNS解析时优先使用ipv4。默认false。仅 App-Android 支持 (HBuilderX 2.8.0+) * @param {Function(statusCode):Boolean} arg.validateStatus - 全局默认的自定义验证器。默认statusCode >= 200 && statusCode < 300 */ constructor(arg = {}) { if (!isPlainObject(arg)) { arg = {} console.warn('设置全局参数必须接收一个Object') } this.config = clone({ ...defaults, ...arg }) this.interceptors = { request: new InterceptorManager(), response: new InterceptorManager() } } /** * @Function * @param {Request~setConfigCallback} f - 设置全局默认配置 */ setConfig(f) { this.config = f(this.config) } middleware(config) { config = mergeConfig(this.config, config) const chain = [dispatchRequest, undefined] let promise = Promise.resolve(config) this.interceptors.request.forEach((interceptor) => { chain.unshift(interceptor.fulfilled, interceptor.rejected) }) this.interceptors.response.forEach((interceptor) => { chain.push(interceptor.fulfilled, interceptor.rejected) }) while (chain.length) { promise = promise.then(chain.shift(), chain.shift()) } return promise } /** * @Function * @param {Object} config - 请求配置项 * @prop {String} options.url - 请求路径 * @prop {Object} options.data - 请求参数 * @prop {Object} [options.responseType = config.responseType] [text|arraybuffer] - 响应的数据类型 * @prop {Object} [options.dataType = config.dataType] - 如果设为 json,会尝试对返回的数据做一次 JSON.parse * @prop {Object} [options.header = config.header] - 请求header * @prop {Object} [options.method = config.method] - 请求方法 * @returns {Promise} */ request(config = {}) { return this.middleware(config) } get(url, options = {}) { return this.middleware({ url, method: 'GET', ...options }) } post(url, data, options = {}) { return this.middleware({ url, data, method: 'POST', ...options }) } // #ifndef MP-ALIPAY put(url, data, options = {}) { return this.middleware({ url, data, method: 'PUT', ...options }) } // #endif // #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU delete(url, data, options = {}) { return this.middleware({ url, data, method: 'DELETE', ...options }) } // #endif // #ifdef H5 || MP-WEIXIN connect(url, data, options = {}) { return this.middleware({ url, data, method: 'CONNECT', ...options }) } // #endif // #ifdef H5 || MP-WEIXIN || MP-BAIDU head(url, data, options = {}) { return this.middleware({ url, data, method: 'HEAD', ...options }) } // #endif // #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU options(url, data, options = {}) { return this.middleware({ url, data, method: 'OPTIONS', ...options }) } // #endif // #ifdef H5 || MP-WEIXIN trace(url, data, options = {}) { return this.middleware({ url, data, method: 'TRACE', ...options }) } // #endif upload(url, config = {}) { config.url = url config.method = 'UPLOAD' return this.middleware(config) } download(url, config = {}) { config.url = url config.method = 'DOWNLOAD' return this.middleware(config) } } /** * setConfig回调 * @return {Object} - 返回操作后的config * @callback Request~setConfigCallback * @param {Object} config - 全局默认config */ ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/luch-request/core/buildFullPath.js ================================================ 'use strict' import isAbsoluteURL from '../helpers/isAbsoluteURL' import combineURLs from '../helpers/combineURLs' /** * Creates a new URL by combining the baseURL with the requestedURL, * only when the requestedURL is not already an absolute URL. * If the requestURL is absolute, this function returns the requestedURL untouched. * * @param {string} baseURL The base URL * @param {string} requestedURL Absolute or relative URL to combine * @returns {string} The combined full path */ export default function buildFullPath(baseURL, requestedURL) { if (baseURL && !isAbsoluteURL(requestedURL)) { return combineURLs(baseURL, requestedURL) } return requestedURL } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/luch-request/core/defaults.js ================================================ /** * 默认的全局配置 */ export default { baseURL: '', header: {}, method: 'GET', dataType: 'json', // #ifndef MP-ALIPAY responseType: 'text', // #endif custom: {}, // #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN timeout: 60000, // #endif // #ifdef APP-PLUS sslVerify: true, // #endif // #ifdef H5 withCredentials: false, // #endif // #ifdef APP-PLUS firstIpv4: false, // #endif validateStatus: function validateStatus(status) { return status >= 200 && status < 300 } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/luch-request/core/dispatchRequest.js ================================================ import adapter from '../adapters/index' export default (config) => adapter(config) ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/luch-request/core/mergeConfig.js ================================================ import { deepMerge, isUndefined } from '../utils' /** * 合并局部配置优先的配置,如果局部有该配置项则用局部,如果全局有该配置项则用全局 * @param {Array} keys - 配置项 * @param {Object} globalsConfig - 当前的全局配置 * @param {Object} config2 - 局部配置 * @return {{}} */ const mergeKeys = (keys, globalsConfig, config2) => { const config = {} keys.forEach((prop) => { if (!isUndefined(config2[prop])) { config[prop] = config2[prop] } else if (!isUndefined(globalsConfig[prop])) { config[prop] = globalsConfig[prop] } }) return config } /** * * @param globalsConfig - 当前实例的全局配置 * @param config2 - 当前的局部配置 * @return - 合并后的配置 */ export default (globalsConfig, config2 = {}) => { const method = config2.method || globalsConfig.method || 'GET' let config = { baseURL: globalsConfig.baseURL || '', method, url: config2.url || '', params: config2.params || {}, custom: { ...(globalsConfig.custom || {}), ...(config2.custom || {}) }, header: deepMerge(globalsConfig.header || {}, config2.header || {}) } const defaultToConfig2Keys = ['getTask', 'validateStatus'] config = { ...config, ...mergeKeys(defaultToConfig2Keys, globalsConfig, config2) } // eslint-disable-next-line no-empty if (method === 'DOWNLOAD') { // #ifdef H5 || APP-PLUS if (!isUndefined(config2.timeout)) { config.timeout = config2.timeout } else if (!isUndefined(globalsConfig.timeout)) { config.timeout = globalsConfig.timeout } // #endif } else if (method === 'UPLOAD') { delete config.header['content-type'] delete config.header['Content-Type'] const uploadKeys = [ // #ifdef APP-PLUS || H5 'files', // #endif // #ifdef MP-ALIPAY 'fileType', // #endif // #ifdef H5 'file', // #endif 'filePath', 'name', // #ifdef H5 || APP-PLUS 'timeout', // #endif 'formData' ] uploadKeys.forEach((prop) => { if (!isUndefined(config2[prop])) { config[prop] = config2[prop] } }) // #ifdef H5 || APP-PLUS if (isUndefined(config.timeout) && !isUndefined(globalsConfig.timeout)) { config.timeout = globalsConfig.timeout } // #endif } else { const defaultsKeys = [ 'data', // #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN 'timeout', // #endif 'dataType', // #ifndef MP-ALIPAY 'responseType', // #endif // #ifdef APP-PLUS 'sslVerify', // #endif // #ifdef H5 'withCredentials', // #endif // #ifdef APP-PLUS 'firstIpv4' // #endif ] config = { ...config, ...mergeKeys(defaultsKeys, globalsConfig, config2) } } return config } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/luch-request/core/settle.js ================================================ /** * Resolve or reject a Promise based on response status. * * @param {Function} resolve A function that resolves the promise. * @param {Function} reject A function that rejects the promise. * @param {object} response The response. */ export default function settle(resolve, reject, response) { const { validateStatus } = response.config const status = response.statusCode if (status && (!validateStatus || validateStatus(status))) { resolve(response) } else { reject(response) } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/luch-request/helpers/buildURL.js ================================================ 'use strict' import * as utils from '../utils' function encode(val) { return encodeURIComponent(val) .replace(/%40/gi, '@') .replace(/%3A/gi, ':') .replace(/%24/g, '$') .replace(/%2C/gi, ',') .replace(/%20/g, '+') .replace(/%5B/gi, '[') .replace(/%5D/gi, ']') } /** * Build a URL by appending params to the end * * @param {string} url The base of the url (e.g., http://www.google.com) * @param {object} [params] The params to be appended * @returns {string} The formatted url */ export default function buildURL(url, params) { /* eslint no-param-reassign:0 */ if (!params) { return url } let serializedParams if (utils.isURLSearchParams(params)) { serializedParams = params.toString() } else { const parts = [] utils.forEach(params, (val, key) => { if (val === null || typeof val === 'undefined') { return } if (utils.isArray(val)) { key = `${key}[]` } else { val = [val] } utils.forEach(val, (v) => { if (utils.isDate(v)) { v = v.toISOString() } else if (utils.isObject(v)) { v = JSON.stringify(v) } parts.push(`${encode(key)}=${encode(v)}`) }) }) serializedParams = parts.join('&') } if (serializedParams) { const hashmarkIndex = url.indexOf('#') if (hashmarkIndex !== -1) { url = url.slice(0, hashmarkIndex) } url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams } return url } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/luch-request/helpers/combineURLs.js ================================================ 'use strict' /** * Creates a new URL by combining the specified URLs * * @param {string} baseURL The base URL * @param {string} relativeURL The relative URL * @returns {string} The combined URL */ export default function combineURLs(baseURL, relativeURL) { return relativeURL ? `${baseURL.replace(/\/+$/, '')}/${relativeURL.replace(/^\/+/, '')}` : baseURL } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/luch-request/helpers/isAbsoluteURL.js ================================================ 'use strict' /** * Determines whether the specified URL is absolute * * @param {string} url The URL to test * @returns {boolean} True if the specified URL is absolute, otherwise false */ export default function isAbsoluteURL(url) { // A URL is considered absolute if it begins with "://" or "//" (protocol-relative URL). // RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed // by any combination of letters, digits, plus, period, or hyphen. return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url) } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/luch-request/index.d.ts ================================================ type AnyObject = Record type HttpPromise = Promise>; type Tasks = UniApp.RequestTask | UniApp.UploadTask | UniApp.DownloadTask export interface RequestTask { abort: () => void; offHeadersReceived: () => void; onHeadersReceived: () => void; } export interface HttpRequestConfig { /** 请求基地址 */ baseURL?: string; /** 请求服务器接口地址 */ url?: string; /** 请求查询参数,自动拼接为查询字符串 */ params?: AnyObject; /** 请求体参数 */ data?: AnyObject; /** 文件对应的 key */ name?: string; /** HTTP 请求中其他额外的 form data */ formData?: AnyObject; /** 要上传文件资源的路径。 */ filePath?: string; /** 需要上传的文件列表。使用 files 时,filePath 和 name 不生效,App、H5( 2.6.15+) */ files?: Array<{ name?: string; file?: File; uri: string; }>; /** 要上传的文件对象,仅H5(2.6.15+)支持 */ file?: File; /** 请求头信息 */ header?: AnyObject; /** 请求方式 */ method?: "GET" | "POST" | "PUT" | "DELETE" | "CONNECT" | "HEAD" | "OPTIONS" | "TRACE" | "UPLOAD" | "DOWNLOAD"; /** 如果设为 json,会尝试对返回的数据做一次 JSON.parse */ dataType?: string; /** 设置响应的数据类型,支付宝小程序不支持 */ responseType?: "text" | "arraybuffer"; /** 自定义参数 */ custom?: AnyObject; /** 超时时间,仅微信小程序(2.10.0)、支付宝小程序支持 */ timeout?: number; /** DNS解析时优先使用ipv4,仅 App-Android 支持 (HBuilderX 2.8.0+) */ firstIpv4?: boolean; /** 验证 ssl 证书 仅5+App安卓端支持(HBuilderX 2.3.3+) */ sslVerify?: boolean; /** 跨域请求时是否携带凭证(cookies)仅H5支持(HBuilderX 2.6.15+) */ withCredentials?: boolean; /** 返回当前请求的task, options。请勿在此处修改options。 */ getTask?: (task: T, options: HttpRequestConfig) => void; /** 全局自定义验证器 */ validateStatus?: (statusCode: number) => boolean | void; } export interface HttpResponse { config: HttpRequestConfig; statusCode: number; cookies: Array; data: T; errMsg: string; header: AnyObject; } export interface HttpUploadResponse { config: HttpRequestConfig; statusCode: number; data: T; errMsg: string; } export interface HttpDownloadResponse extends HttpResponse { tempFilePath: string; } export interface HttpError { config: HttpRequestConfig; statusCode?: number; cookies?: Array; data?: any; errMsg: string; header?: AnyObject; } export interface HttpInterceptorManager { use( onFulfilled?: (config: V) => Promise | V, onRejected?: (config: E) => Promise | E ): void; eject(id: number): void; } export abstract class HttpRequestAbstract { constructor(config?: HttpRequestConfig); config: HttpRequestConfig; interceptors: { request: HttpInterceptorManager; response: HttpInterceptorManager; } middleware(config: HttpRequestConfig): HttpPromise; request(config: HttpRequestConfig): HttpPromise; get(url: string, config?: HttpRequestConfig): HttpPromise; upload(url: string, config?: HttpRequestConfig): HttpPromise; delete(url: string, data?: AnyObject, config?: HttpRequestConfig): HttpPromise; head(url: string, data?: AnyObject, config?: HttpRequestConfig): HttpPromise; post(url: string, data?: AnyObject, config?: HttpRequestConfig): HttpPromise; put(url: string, data?: AnyObject, config?: HttpRequestConfig): HttpPromise; connect(url: string, data?: AnyObject, config?: HttpRequestConfig): HttpPromise; options(url: string, data?: AnyObject, config?: HttpRequestConfig): HttpPromise; trace(url: string, data?: AnyObject, config?: HttpRequestConfig): HttpPromise; download(url: string, config?: HttpRequestConfig): Promise; setConfig(onSend: (config: HttpRequestConfig) => HttpRequestConfig): void; } declare class HttpRequest extends HttpRequestAbstract { } export default HttpRequest; ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/luch-request/index.js ================================================ import Request from './core/Request' export default Request ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/luch-request/utils/clone.js ================================================ /* eslint-disable */ var clone = (function() { 'use strict'; function _instanceof(obj, type) { return type != null && obj instanceof type; } var nativeMap; try { nativeMap = Map; } catch(_) { // maybe a reference error because no `Map`. Give it a dummy value that no // value will ever be an instanceof. nativeMap = function() {}; } var nativeSet; try { nativeSet = Set; } catch(_) { nativeSet = function() {}; } var nativePromise; try { nativePromise = Promise; } catch(_) { nativePromise = function() {}; } /** * Clones (copies) an Object using deep copying. * * This function supports circular references by default, but if you are certain * there are no circular references in your object, you can save some CPU time * by calling clone(obj, false). * * Caution: if `circular` is false and `parent` contains circular references, * your program may enter an infinite loop and crash. * * @param `parent` - the object to be cloned * @param `circular` - set to true if the object to be cloned may contain * circular references. (optional - true by default) * @param `depth` - set to a number if the object is only to be cloned to * a particular depth. (optional - defaults to Infinity) * @param `prototype` - sets the prototype to be used when cloning an object. * (optional - defaults to parent prototype). * @param `includeNonEnumerable` - set to true if the non-enumerable properties * should be cloned as well. Non-enumerable properties on the prototype * chain will be ignored. (optional - false by default) */ function clone(parent, circular, depth, prototype, includeNonEnumerable) { if (typeof circular === 'object') { depth = circular.depth; prototype = circular.prototype; includeNonEnumerable = circular.includeNonEnumerable; circular = circular.circular; } // maintain two arrays for circular references, where corresponding parents // and children have the same index var allParents = []; var allChildren = []; var useBuffer = typeof Buffer != 'undefined'; if (typeof circular == 'undefined') circular = true; if (typeof depth == 'undefined') depth = Infinity; // recurse this function so we don't reset allParents and allChildren function _clone(parent, depth) { // cloning null always returns null if (parent === null) return null; if (depth === 0) return parent; var child; var proto; if (typeof parent != 'object') { return parent; } if (_instanceof(parent, nativeMap)) { child = new nativeMap(); } else if (_instanceof(parent, nativeSet)) { child = new nativeSet(); } else if (_instanceof(parent, nativePromise)) { child = new nativePromise(function (resolve, reject) { parent.then(function(value) { resolve(_clone(value, depth - 1)); }, function(err) { reject(_clone(err, depth - 1)); }); }); } else if (clone.__isArray(parent)) { child = []; } else if (clone.__isRegExp(parent)) { child = new RegExp(parent.source, __getRegExpFlags(parent)); if (parent.lastIndex) child.lastIndex = parent.lastIndex; } else if (clone.__isDate(parent)) { child = new Date(parent.getTime()); } else if (useBuffer && Buffer.isBuffer(parent)) { if (Buffer.from) { // Node.js >= 5.10.0 child = Buffer.from(parent); } else { // Older Node.js versions child = new Buffer(parent.length); parent.copy(child); } return child; } else if (_instanceof(parent, Error)) { child = Object.create(parent); } else { if (typeof prototype == 'undefined') { proto = Object.getPrototypeOf(parent); child = Object.create(proto); } else { child = Object.create(prototype); proto = prototype; } } if (circular) { var index = allParents.indexOf(parent); if (index != -1) { return allChildren[index]; } allParents.push(parent); allChildren.push(child); } if (_instanceof(parent, nativeMap)) { parent.forEach(function(value, key) { var keyChild = _clone(key, depth - 1); var valueChild = _clone(value, depth - 1); child.set(keyChild, valueChild); }); } if (_instanceof(parent, nativeSet)) { parent.forEach(function(value) { var entryChild = _clone(value, depth - 1); child.add(entryChild); }); } for (var i in parent) { var attrs = Object.getOwnPropertyDescriptor(parent, i); if (attrs) { child[i] = _clone(parent[i], depth - 1); } try { var objProperty = Object.getOwnPropertyDescriptor(parent, i); if (objProperty.set === 'undefined') { // no setter defined. Skip cloning this property continue; } child[i] = _clone(parent[i], depth - 1); } catch(e){ if (e instanceof TypeError) { // when in strict mode, TypeError will be thrown if child[i] property only has a getter // we can't do anything about this, other than inform the user that this property cannot be set. continue } else if (e instanceof ReferenceError) { //this may happen in non strict mode continue } } } if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(parent); for (var i = 0; i < symbols.length; i++) { // Don't need to worry about cloning a symbol because it is a primitive, // like a number or string. var symbol = symbols[i]; var descriptor = Object.getOwnPropertyDescriptor(parent, symbol); if (descriptor && !descriptor.enumerable && !includeNonEnumerable) { continue; } child[symbol] = _clone(parent[symbol], depth - 1); Object.defineProperty(child, symbol, descriptor); } } if (includeNonEnumerable) { var allPropertyNames = Object.getOwnPropertyNames(parent); for (var i = 0; i < allPropertyNames.length; i++) { var propertyName = allPropertyNames[i]; var descriptor = Object.getOwnPropertyDescriptor(parent, propertyName); if (descriptor && descriptor.enumerable) { continue; } child[propertyName] = _clone(parent[propertyName], depth - 1); Object.defineProperty(child, propertyName, descriptor); } } return child; } return _clone(parent, depth); } /** * Simple flat clone using prototype, accepts only objects, usefull for property * override on FLAT configuration object (no nested props). * * USE WITH CAUTION! This may not behave as you wish if you do not know how this * works. */ clone.clonePrototype = function clonePrototype(parent) { if (parent === null) return null; var c = function () {}; c.prototype = parent; return new c(); }; // private utility functions function __objToStr(o) { return Object.prototype.toString.call(o); } clone.__objToStr = __objToStr; function __isDate(o) { return typeof o === 'object' && __objToStr(o) === '[object Date]'; } clone.__isDate = __isDate; function __isArray(o) { return typeof o === 'object' && __objToStr(o) === '[object Array]'; } clone.__isArray = __isArray; function __isRegExp(o) { return typeof o === 'object' && __objToStr(o) === '[object RegExp]'; } clone.__isRegExp = __isRegExp; function __getRegExpFlags(re) { var flags = ''; if (re.global) flags += 'g'; if (re.ignoreCase) flags += 'i'; if (re.multiline) flags += 'm'; return flags; } clone.__getRegExpFlags = __getRegExpFlags; return clone; })(); export default clone ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/luch-request/utils.js ================================================ 'use strict' // utils is a library of generic helper functions non-specific to axios const { toString } = Object.prototype /** * Determine if a value is an Array * * @param {Object} val The value to test * @returns {boolean} True if value is an Array, otherwise false */ export function isArray(val) { return toString.call(val) === '[object Array]' } /** * Determine if a value is an Object * * @param {Object} val The value to test * @returns {boolean} True if value is an Object, otherwise false */ export function isObject(val) { return val !== null && typeof val === 'object' } /** * Determine if a value is a Date * * @param {Object} val The value to test * @returns {boolean} True if value is a Date, otherwise false */ export function isDate(val) { return toString.call(val) === '[object Date]' } /** * Determine if a value is a URLSearchParams object * * @param {Object} val The value to test * @returns {boolean} True if value is a URLSearchParams object, otherwise false */ export function isURLSearchParams(val) { return typeof URLSearchParams !== 'undefined' && val instanceof URLSearchParams } /** * Iterate over an Array or an Object invoking a function for each item. * * If `obj` is an Array callback will be called passing * the value, index, and complete array for each item. * * If 'obj' is an Object callback will be called passing * the value, key, and complete object for each property. * * @param {Object|Array} obj The object to iterate * @param {Function} fn The callback to invoke for each item */ export function forEach(obj, fn) { // Don't bother if no value provided if (obj === null || typeof obj === 'undefined') { return } // Force an array if not already something iterable if (typeof obj !== 'object') { /* eslint no-param-reassign:0 */ obj = [obj] } if (isArray(obj)) { // Iterate over array values for (let i = 0, l = obj.length; i < l; i++) { fn.call(null, obj[i], i, obj) } } else { // Iterate over object keys for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { fn.call(null, obj[key], key, obj) } } } } /** * 是否为boolean 值 * @param val * @returns {boolean} */ export function isBoolean(val) { return typeof val === 'boolean' } /** * 是否为真正的对象{} new Object * @param {any} obj - 检测的对象 * @returns {boolean} */ export function isPlainObject(obj) { return Object.prototype.toString.call(obj) === '[object Object]' } /** * Function equal to merge with the difference being that no reference * to original objects is kept. * * @see merge * @param {Object} obj1 Object to merge * @returns {Object} Result of all merge properties */ export function deepMerge(/* obj1, obj2, obj3, ... */) { const result = {} function assignValue(val, key) { if (typeof result[key] === 'object' && typeof val === 'object') { result[key] = deepMerge(result[key], val) } else if (typeof val === 'object') { result[key] = deepMerge({}, val) } else { result[key] = val } } for (let i = 0, l = arguments.length; i < l; i++) { forEach(arguments[i], assignValue) } return result } export function isUndefined(val) { return typeof val === 'undefined' } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/mixin/button.js ================================================ export default { props: { lang: String, sessionFrom: String, sendMessageTitle: String, sendMessagePath: String, sendMessageImg: String, showMessageCard: Boolean, appParameter: String, formType: String, openType: String } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/mixin/mixin.js ================================================ import * as index from '../function/index.js'; import * as test from '../function/test.js'; export default { // 定义每个组件都可能需要用到的外部样式以及类名 props: { // 每个组件都有的父组件传递的样式,可以为字符串或者对象形式 customStyle: { type: [Object, String], default: () => ({}) }, customClass: { type: String, default: '' }, // 跳转的页面路径 url: { type: String, default: '' }, // 页面跳转的类型 linkType: { type: String, default: 'navigateTo' } }, data() { return {} }, onLoad() { // getRect挂载到$uv上,因为这方法需要使用in(this),所以无法把它独立成一个单独的文件导出 this.$uv.getRect = this.$uvGetRect }, created() { // 组件当中,只有created声明周期,为了能在组件使用,故也在created中将方法挂载到$uv this.$uv.getRect = this.$uvGetRect }, computed: { $uv() { return { ...index, test, unit: uni?.$uv?.config?.unit } }, /** * 生成bem规则类名 * 由于微信小程序,H5,nvue之间绑定class的差异,无法通过:class="[bem()]"的形式进行同用 * 故采用如下折中做法,最后返回的是数组(一般平台)或字符串(支付宝和字节跳动平台),类似['a', 'b', 'c']或'a b c'的形式 * @param {String} name 组件名称 * @param {Array} fixed 一直会存在的类名 * @param {Array} change 会根据变量值为true或者false而出现或者隐藏的类名 * @returns {Array|string} */ bem() { return function(name, fixed, change) { // 类名前缀 const prefix = `uv-${name}--` const classes = {} if (fixed) { fixed.map((item) => { // 这里的类名,会一直存在 classes[prefix + this[item]] = true }) } if (change) { change.map((item) => { // 这里的类名,会根据this[item]的值为true或者false,而进行添加或者移除某一个类 this[item] ? (classes[prefix + item] = this[item]) : (delete classes[prefix + item]) }) } return Object.keys(classes) // 支付宝,头条小程序无法动态绑定一个数组类名,否则解析出来的结果会带有",",而导致失效 // #ifdef MP-ALIPAY || MP-TOUTIAO || MP-LARK || MP-BAIDU .join(' ') // #endif } } }, methods: { // 跳转某一个页面 openPage(urlKey = 'url') { const url = this[urlKey] if (url) { // 执行类似uni.navigateTo的方法 uni[this.linkType]({ url }) } }, // 查询节点信息 // 目前此方法在支付宝小程序中无法获取组件跟接点的尺寸,为支付宝的bug(2020-07-21) // 解决办法为在组件根部再套一个没有任何作用的view元素 $uvGetRect(selector, all) { return new Promise((resolve) => { uni.createSelectorQuery() .in(this)[all ? 'selectAll' : 'select'](selector) .boundingClientRect((rect) => { if (all && Array.isArray(rect) && rect.length) { resolve(rect) } if (!all && rect) { resolve(rect) } }) .exec() }) }, getParentData(parentName = '') { // 避免在created中去定义parent变量 if (!this.parent) this.parent = {} // 这里的本质原理是,通过获取父组件实例(也即类似uv-radio的父组件uv-radio-group的this) // 将父组件this中对应的参数,赋值给本组件(uv-radio的this)的parentData对象中对应的属性 // 之所以需要这么做,是因为所有端中,头条小程序不支持通过this.parent.xxx去监听父组件参数的变化 // 此处并不会自动更新子组件的数据,而是依赖父组件uv-radio-group去监听data的变化,手动调用更新子组件的方法去重新获取 this.parent = this.$uv.$parent.call(this, parentName) if (this.parent.children) { // 如果父组件的children不存在本组件的实例,才将本实例添加到父组件的children中 this.parent.children.indexOf(this) === -1 && this.parent.children.push(this) } if (this.parent && this.parentData) { // 历遍parentData中的属性,将parent中的同名属性赋值给parentData Object.keys(this.parentData).map((key) => { this.parentData[key] = this.parent[key] }) } }, // 阻止事件冒泡 preventEvent(e) { e && typeof(e.stopPropagation) === 'function' && e.stopPropagation() }, // 空操作 noop(e) { this.preventEvent(e) } }, onReachBottom() { uni.$emit('uvOnReachBottom') }, beforeDestroy() { // 判断当前页面是否存在parent和chldren,一般在checkbox和checkbox-group父子联动的场景会有此情况 // 组件销毁时,移除子组件在父组件children数组中的实例,释放资源,避免数据混乱 if (this.parent && test.array(this.parent.children)) { // 组件销毁时,移除父组件中的children数组中对应的实例 const childrenList = this.parent.children childrenList.map((child, index) => { // 如果相等,则移除 if (child === this) { childrenList.splice(index, 1) } }) } }, // 兼容vue3 unmounted() { if (this.parent && test.array(this.parent.children)) { // 组件销毁时,移除父组件中的children数组中对应的实例 const childrenList = this.parent.children childrenList.map((child, index) => { // 如果相等,则移除 if (child === this) { childrenList.splice(index, 1) } }) } } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js ================================================ export default { // #ifdef MP-WEIXIN // 将自定义节点设置成虚拟的(去掉自定义组件包裹层),更加接近Vue组件的表现,能更好的使用flex属性 options: { virtualHost: true } // #endif } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/mixin/mpShare.js ================================================ export default { onLoad() { // 设置默认的转发参数 uni.$uv.mpShare = { title: '', // 默认为小程序名称 path: '', // 默认为当前页面路径 imageUrl: '' // 默认为当前页面的截图 } }, onShareAppMessage() { return uni.$uv.mpShare } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/mixin/openType.js ================================================ export default { props: { openType: String }, emits: ['getphonenumber','getuserinfo','error','opensetting','launchapp','contact','chooseavatar','addgroupapp','chooseaddress','subscribe','login','im'], methods: { onGetPhoneNumber(event) { this.$emit('getphonenumber', event.detail) }, onGetUserInfo(event) { this.$emit('getuserinfo', event.detail) }, onError(event) { this.$emit('error', event.detail) }, onOpenSetting(event) { this.$emit('opensetting', event.detail) }, onLaunchApp(event) { this.$emit('launchapp', event.detail) }, onContact(event) { this.$emit('contact', event.detail) }, onChooseavatar(event) { this.$emit('chooseavatar', event.detail) }, onAgreeprivacyauthorization(event) { this.$emit('agreeprivacyauthorization', event.detail) }, onAddgroupapp(event) { this.$emit('addgroupapp', event.detail) }, onChooseaddress(event) { this.$emit('chooseaddress', event.detail) }, onSubscribe(event) { this.$emit('subscribe', event.detail) }, onLogin(event) { this.$emit('login', event.detail) }, onIm(event) { this.$emit('im', event.detail) } } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/mixin/touch.js ================================================ const MIN_DISTANCE = 10 function getDirection(x, y) { if (x > y && x > MIN_DISTANCE) { return 'horizontal' } if (y > x && y > MIN_DISTANCE) { return 'vertical' } return '' } export default { methods: { getTouchPoint(e) { if (!e) { return { x: 0, y: 0 } } if (e.touches && e.touches[0]) { return { x: e.touches[0].pageX, y: e.touches[0].pageY } } if (e.changedTouches && e.changedTouches[0]) { return { x: e.changedTouches[0].pageX, y: e.changedTouches[0].pageY } } return { x: e.clientX || 0, y: e.clientY || 0 } }, resetTouchStatus() { this.direction = '' this.deltaX = 0 this.deltaY = 0 this.offsetX = 0 this.offsetY = 0 }, touchStart(event) { this.resetTouchStatus() const touch = this.getTouchPoint(event) this.startX = touch.x this.startY = touch.y }, touchMove(event) { const touch = this.getTouchPoint(event) this.deltaX = touch.x - this.startX this.deltaY = touch.y - this.startY this.offsetX = Math.abs(this.deltaX) this.offsetY = Math.abs(this.deltaY) this.direction = this.direction || getDirection(this.offsetX, this.offsetY) } } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/util/dayjs.js ================================================ var __getOwnPropNames = Object.getOwnPropertyNames; var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var require_dayjs_min = __commonJS({ "uvuidayjs"(exports, module) { !function(t, e) { "object" == typeof exports && "undefined" != typeof module ? module.exports = e() : "function" == typeof define && define.amd ? define(e) : (t = "undefined" != typeof globalThis ? globalThis : t || self).dayjs = e(); }(exports, function() { "use strict"; var t = 1e3, e = 6e4, n = 36e5, r = "millisecond", i = "second", s = "minute", u = "hour", a = "day", o = "week", f = "month", h = "quarter", c = "year", d = "date", l = "Invalid Date", $ = /^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/, y = /\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g, M = { name: "en", weekdays: "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), months: "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), ordinal: function(t2) { var e2 = ["th", "st", "nd", "rd"], n2 = t2 % 100; return "[" + t2 + (e2[(n2 - 20) % 10] || e2[n2] || e2[0]) + "]"; } }, m = function(t2, e2, n2) { var r2 = String(t2); return !r2 || r2.length >= e2 ? t2 : "" + Array(e2 + 1 - r2.length).join(n2) + t2; }, v = { s: m, z: function(t2) { var e2 = -t2.utcOffset(), n2 = Math.abs(e2), r2 = Math.floor(n2 / 60), i2 = n2 % 60; return (e2 <= 0 ? "+" : "-") + m(r2, 2, "0") + ":" + m(i2, 2, "0"); }, m: function t2(e2, n2) { if (e2.date() < n2.date()) return -t2(n2, e2); var r2 = 12 * (n2.year() - e2.year()) + (n2.month() - e2.month()), i2 = e2.clone().add(r2, f), s2 = n2 - i2 < 0, u2 = e2.clone().add(r2 + (s2 ? -1 : 1), f); return +(-(r2 + (n2 - i2) / (s2 ? i2 - u2 : u2 - i2)) || 0); }, a: function(t2) { return t2 < 0 ? Math.ceil(t2) || 0 : Math.floor(t2); }, p: function(t2) { return { M: f, y: c, w: o, d: a, D: d, h: u, m: s, s: i, ms: r, Q: h }[t2] || String(t2 || "").toLowerCase().replace(/s$/, ""); }, u: function(t2) { return void 0 === t2; } }, g = "en", D = {}; D[g] = M; var p = function(t2) { return t2 instanceof _; }, S = function t2(e2, n2, r2) { var i2; if (!e2) return g; if ("string" == typeof e2) { var s2 = e2.toLowerCase(); D[s2] && (i2 = s2), n2 && (D[s2] = n2, i2 = s2); var u2 = e2.split("-"); if (!i2 && u2.length > 1) return t2(u2[0]); } else { var a2 = e2.name; D[a2] = e2, i2 = a2; } return !r2 && i2 && (g = i2), i2 || !r2 && g; }, w = function(t2, e2) { if (p(t2)) return t2.clone(); var n2 = "object" == typeof e2 ? e2 : {}; return n2.date = t2, n2.args = arguments, new _(n2); }, O = v; O.l = S, O.i = p, O.w = function(t2, e2) { return w(t2, { locale: e2.$L, utc: e2.$u, x: e2.$x, $offset: e2.$offset }); }; var _ = function() { function M2(t2) { this.$L = S(t2.locale, null, true), this.parse(t2); } var m2 = M2.prototype; return m2.parse = function(t2) { this.$d = function(t3) { var e2 = t3.date, n2 = t3.utc; if (null === e2) return new Date(NaN); if (O.u(e2)) return new Date(); if (e2 instanceof Date) return new Date(e2); if ("string" == typeof e2 && !/Z$/i.test(e2)) { var r2 = e2.match($); if (r2) { var i2 = r2[2] - 1 || 0, s2 = (r2[7] || "0").substring(0, 3); return n2 ? new Date(Date.UTC(r2[1], i2, r2[3] || 1, r2[4] || 0, r2[5] || 0, r2[6] || 0, s2)) : new Date(r2[1], i2, r2[3] || 1, r2[4] || 0, r2[5] || 0, r2[6] || 0, s2); } } return new Date(e2); }(t2), this.$x = t2.x || {}, this.init(); }, m2.init = function() { var t2 = this.$d; this.$y = t2.getFullYear(), this.$M = t2.getMonth(), this.$D = t2.getDate(), this.$W = t2.getDay(), this.$H = t2.getHours(), this.$m = t2.getMinutes(), this.$s = t2.getSeconds(), this.$ms = t2.getMilliseconds(); }, m2.$utils = function() { return O; }, m2.isValid = function() { return !(this.$d.toString() === l); }, m2.isSame = function(t2, e2) { var n2 = w(t2); return this.startOf(e2) <= n2 && n2 <= this.endOf(e2); }, m2.isAfter = function(t2, e2) { return w(t2) < this.startOf(e2); }, m2.isBefore = function(t2, e2) { return this.endOf(e2) < w(t2); }, m2.$g = function(t2, e2, n2) { return O.u(t2) ? this[e2] : this.set(n2, t2); }, m2.unix = function() { return Math.floor(this.valueOf() / 1e3); }, m2.valueOf = function() { return this.$d.getTime(); }, m2.startOf = function(t2, e2) { var n2 = this, r2 = !!O.u(e2) || e2, h2 = O.p(t2), l2 = function(t3, e3) { var i2 = O.w(n2.$u ? Date.UTC(n2.$y, e3, t3) : new Date(n2.$y, e3, t3), n2); return r2 ? i2 : i2.endOf(a); }, $2 = function(t3, e3) { return O.w(n2.toDate()[t3].apply(n2.toDate("s"), (r2 ? [0, 0, 0, 0] : [23, 59, 59, 999]).slice(e3)), n2); }, y2 = this.$W, M3 = this.$M, m3 = this.$D, v2 = "set" + (this.$u ? "UTC" : ""); switch (h2) { case c: return r2 ? l2(1, 0) : l2(31, 11); case f: return r2 ? l2(1, M3) : l2(0, M3 + 1); case o: var g2 = this.$locale().weekStart || 0, D2 = (y2 < g2 ? y2 + 7 : y2) - g2; return l2(r2 ? m3 - D2 : m3 + (6 - D2), M3); case a: case d: return $2(v2 + "Hours", 0); case u: return $2(v2 + "Minutes", 1); case s: return $2(v2 + "Seconds", 2); case i: return $2(v2 + "Milliseconds", 3); default: return this.clone(); } }, m2.endOf = function(t2) { return this.startOf(t2, false); }, m2.$set = function(t2, e2) { var n2, o2 = O.p(t2), h2 = "set" + (this.$u ? "UTC" : ""), l2 = (n2 = {}, n2[a] = h2 + "Date", n2[d] = h2 + "Date", n2[f] = h2 + "Month", n2[c] = h2 + "FullYear", n2[u] = h2 + "Hours", n2[s] = h2 + "Minutes", n2[i] = h2 + "Seconds", n2[r] = h2 + "Milliseconds", n2)[o2], $2 = o2 === a ? this.$D + (e2 - this.$W) : e2; if (o2 === f || o2 === c) { var y2 = this.clone().set(d, 1); y2.$d[l2]($2), y2.init(), this.$d = y2.set(d, Math.min(this.$D, y2.daysInMonth())).$d; } else l2 && this.$d[l2]($2); return this.init(), this; }, m2.set = function(t2, e2) { return this.clone().$set(t2, e2); }, m2.get = function(t2) { return this[O.p(t2)](); }, m2.add = function(r2, h2) { var d2, l2 = this; r2 = Number(r2); var $2 = O.p(h2), y2 = function(t2) { var e2 = w(l2); return O.w(e2.date(e2.date() + Math.round(t2 * r2)), l2); }; if ($2 === f) return this.set(f, this.$M + r2); if ($2 === c) return this.set(c, this.$y + r2); if ($2 === a) return y2(1); if ($2 === o) return y2(7); var M3 = (d2 = {}, d2[s] = e, d2[u] = n, d2[i] = t, d2)[$2] || 1, m3 = this.$d.getTime() + r2 * M3; return O.w(m3, this); }, m2.subtract = function(t2, e2) { return this.add(-1 * t2, e2); }, m2.format = function(t2) { var e2 = this, n2 = this.$locale(); if (!this.isValid()) return n2.invalidDate || l; var r2 = t2 || "YYYY-MM-DDTHH:mm:ssZ", i2 = O.z(this), s2 = this.$H, u2 = this.$m, a2 = this.$M, o2 = n2.weekdays, f2 = n2.months, h2 = function(t3, n3, i3, s3) { return t3 && (t3[n3] || t3(e2, r2)) || i3[n3].slice(0, s3); }, c2 = function(t3) { return O.s(s2 % 12 || 12, t3, "0"); }, d2 = n2.meridiem || function(t3, e3, n3) { var r3 = t3 < 12 ? "AM" : "PM"; return n3 ? r3.toLowerCase() : r3; }, $2 = { YY: String(this.$y).slice(-2), YYYY: this.$y, M: a2 + 1, MM: O.s(a2 + 1, 2, "0"), MMM: h2(n2.monthsShort, a2, f2, 3), MMMM: h2(f2, a2), D: this.$D, DD: O.s(this.$D, 2, "0"), d: String(this.$W), dd: h2(n2.weekdaysMin, this.$W, o2, 2), ddd: h2(n2.weekdaysShort, this.$W, o2, 3), dddd: o2[this.$W], H: String(s2), HH: O.s(s2, 2, "0"), h: c2(1), hh: c2(2), a: d2(s2, u2, true), A: d2(s2, u2, false), m: String(u2), mm: O.s(u2, 2, "0"), s: String(this.$s), ss: O.s(this.$s, 2, "0"), SSS: O.s(this.$ms, 3, "0"), Z: i2 }; return r2.replace(y, function(t3, e3) { return e3 || $2[t3] || i2.replace(":", ""); }); }, m2.utcOffset = function() { return 15 * -Math.round(this.$d.getTimezoneOffset() / 15); }, m2.diff = function(r2, d2, l2) { var $2, y2 = O.p(d2), M3 = w(r2), m3 = (M3.utcOffset() - this.utcOffset()) * e, v2 = this - M3, g2 = O.m(this, M3); return g2 = ($2 = {}, $2[c] = g2 / 12, $2[f] = g2, $2[h] = g2 / 3, $2[o] = (v2 - m3) / 6048e5, $2[a] = (v2 - m3) / 864e5, $2[u] = v2 / n, $2[s] = v2 / e, $2[i] = v2 / t, $2)[y2] || v2, l2 ? g2 : O.a(g2); }, m2.daysInMonth = function() { return this.endOf(f).$D; }, m2.$locale = function() { return D[this.$L]; }, m2.locale = function(t2, e2) { if (!t2) return this.$L; var n2 = this.clone(), r2 = S(t2, e2, true); return r2 && (n2.$L = r2), n2; }, m2.clone = function() { return O.w(this.$d, this); }, m2.toDate = function() { return new Date(this.valueOf()); }, m2.toJSON = function() { return this.isValid() ? this.toISOString() : null; }, m2.toISOString = function() { return this.$d.toISOString(); }, m2.toString = function() { return this.$d.toUTCString(); }, M2; }(), T = _.prototype; return w.prototype = T, [["$ms", r], ["$s", i], ["$m", s], ["$H", u], ["$W", a], ["$M", f], ["$y", c], ["$D", d]].forEach(function(t2) { T[t2[1]] = function(e2) { return this.$g(e2, t2[0], t2[1]); }; }), w.extend = function(t2, e2) { return t2.$i || (t2(e2, _, w), t2.$i = true), w; }, w.locale = S, w.isDayjs = p, w.unix = function(t2) { return w(1e3 * t2); }, w.en = D[g], w.Ls = D, w.p = {}, w; }); } }); export default require_dayjs_min(); ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/libs/util/route.js ================================================ /** * 路由跳转方法,该方法相对于直接使用uni.xxx的好处是使用更加简单快捷 * 并且带有路由拦截功能 */ import { queryParams, deepMerge, page } from '@/uni_modules/uv-ui-tools/libs/function/index.js' class Router { constructor() { // 原始属性定义 this.config = { type: 'navigateTo', url: '', delta: 1, // navigateBack页面后退时,回退的层数 params: {}, // 传递的参数 animationType: 'pop-in', // 窗口动画,只在APP有效 animationDuration: 300, // 窗口动画持续时间,单位毫秒,只在APP有效 intercept: false ,// 是否需要拦截 events: {} // 页面间通信接口,用于监听被打开页面发送到当前页面的数据。hbuilderx 2.8.9+ 开始支持。 } // 因为route方法是需要对外赋值给另外的对象使用,同时route内部有使用this,会导致route失去上下文 // 这里在构造函数中进行this绑定 this.route = this.route.bind(this) } // 判断url前面是否有"/",如果没有则加上,否则无法跳转 addRootPath(url) { return url[0] === '/' ? url : `/${url}` } // 整合路由参数 mixinParam(url, params) { url = url && this.addRootPath(url) // 使用正则匹配,主要依据是判断是否有"/","?","="等,如“/page/index/index?name=mary" // 如果有url中有get参数,转换后无需带上"?" let query = '' if (/.*\/.*\?.*=.*/.test(url)) { // object对象转为get类型的参数 query = queryParams(params, false) // 因为已有get参数,所以后面拼接的参数需要带上"&"隔开 return url += `&${query}` } // 直接拼接参数,因为此处url中没有后面的query参数,也就没有"?/&"之类的符号 query = queryParams(params) return url += query } // 对外的方法名称 async route(options = {}, params = {}) { // 合并用户的配置和内部的默认配置 let mergeConfig = {} if (typeof options === 'string') { // 如果options为字符串,则为route(url, params)的形式 mergeConfig.url = this.mixinParam(options, params) mergeConfig.type = 'navigateTo' } else { mergeConfig = deepMerge(this.config, options) // 否则正常使用mergeConfig中的url和params进行拼接 mergeConfig.url = this.mixinParam(options.url, options.params) } // 如果本次跳转的路径和本页面路径一致,不执行跳转,防止用户快速点击跳转按钮,造成多次跳转同一个页面的问题 if (mergeConfig.url === page()) return if (params.intercept) { mergeConfig.intercept = params.intercept } // params参数也带给拦截器 mergeConfig.params = params // 合并内外部参数 mergeConfig = deepMerge(this.config, mergeConfig) // 判断用户是否定义了拦截器 if (typeof mergeConfig.intercept === 'function') { // 定一个promise,根据用户执行resolve(true)或者resolve(false)来决定是否进行路由跳转 const isNext = await new Promise((resolve, reject) => { mergeConfig.intercept(mergeConfig, resolve) }) // 如果isNext为true,则执行路由跳转 isNext && this.openPage(mergeConfig) } else { this.openPage(mergeConfig) } } // 执行路由跳转 openPage(config) { // 解构参数 const { url, type, delta, animationType, animationDuration, events } = config if (config.type == 'navigateTo' || config.type == 'to') { uni.navigateTo({ url, animationType, animationDuration, events }) } if (config.type == 'redirectTo' || config.type == 'redirect') { uni.redirectTo({ url }) } if (config.type == 'switchTab' || config.type == 'tab') { uni.switchTab({ url }) } if (config.type == 'reLaunch' || config.type == 'launch') { uni.reLaunch({ url }) } if (config.type == 'navigateBack' || config.type == 'back') { uni.navigateBack({ delta }) } } } export default (new Router()).route ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/package.json ================================================ { "id": "uv-ui-tools", "displayName": "uv-ui-tools 工具集 全面兼容vue3+2、app、h5、小程序等多端", "version": "1.1.19", "description": "uv-ui-tools,集成工具库,强大的Http请求封装,清晰的文档说明,开箱即用。方便使用,可以全局使用", "keywords": [ "uv-ui-tools,uv-ui组件库,工具集,uvui,uView2.x" ], "repository": "", "engines": { "HBuilderX": "^3.1.0" }, "dcloudext": { "type": "component-vue", "sale": { "regular": { "price": "0.00" }, "sourcecode": { "price": "0.00" } }, "contact": { "qq": "" }, "declaration": { "ads": "无", "data": "插件不采集任何数据", "permissions": "无" }, "npmurl": "" }, "uni_modules": { "dependencies": [], "encrypt": [], "platforms": { "cloud": { "tcb": "y", "aliyun": "y" }, "client": { "Vue": { "vue2": "y", "vue3": "y" }, "App": { "app-vue": "y", "app-nvue": "y" }, "H5-mobile": { "Safari": "y", "Android Browser": "y", "微信浏览器(Android)": "y", "QQ浏览器(Android)": "y" }, "H5-pc": { "Chrome": "y", "IE": "y", "Edge": "y", "Firefox": "y", "Safari": "y" }, "小程序": { "微信": "y", "阿里": "y", "百度": "y", "字节跳动": "y", "QQ": "y", "钉钉": "y", "快手": "y", "飞书": "y", "京东": "y" }, "快应用": { "华为": "y", "联盟": "y" } } } } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/readme.md ================================================ ## uv-ui-tools 工具集 > **组件名:uv-ui-tools** uv-ui工具集成,包括网络Http请求、便捷工具、节流防抖、对象操作、时间格式化、路由跳转、全局唯一标识符、规则校验等等。 该组件推荐配合[uv-ui组件库](https://www.uvui.cn/components/intro.html)使用,单独下载也可以在自己项目中使用,需要做相应的配置,可查看文档。强烈推荐使用[uv-ui组件库](https://www.uvui.cn/components/intro.html),导入组件都会自动导入`uv-ui-tools`。需要在自己的项目中使用请参考[扩展配置](https://www.uvui.cn/components/setting.html)。 uv-ui破釜沉舟之兼容vue3+2、app、h5、多端小程序的uni-app生态框架,大部分组件基于uView2.x,在经过改进后全面支持vue3,部分组件做了进一步的优化,修复大量BUG,支持单独导入,方便开发者选择导入需要的组件。开箱即用,灵活配置。 # 查看文档 ## [下载完整示例项目](https://ext.dcloud.net.cn/plugin?name=uv-ui) (请不要 下载插件ZIP) ### [更多插件,请关注uv-ui组件库](https://ext.dcloud.net.cn/plugin?name=uv-ui) ![image](https://mp-a667b617-c5f1-4a2d-9a54-683a67cff588.cdn.bspapp.com/uv-ui/banner.png) #### 如使用过程中有任何问题反馈,或者您对uv-ui有一些好的建议,欢迎加入uv-ui官方交流群:官方QQ群 ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-ui-tools/theme.scss ================================================ // 此文件为uvUI的主题变量,这些变量目前只能通过uni.scss引入才有效,另外由于 // uni.scss中引入的样式会同时混入到全局样式文件和单独每一个页面的样式中,造成微信程序包太大, // 故uni.scss只建议放scss变量名相关样式,其他的样式可以通过main.js或者App.vue引入 $uv-main-color: #303133; $uv-content-color: #606266; $uv-tips-color: #909193; $uv-light-color: #c0c4cc; $uv-border-color: #dadbde; $uv-bg-color: #f3f4f6; $uv-disabled-color: #c8c9cc; $uv-primary: #3c9cff; $uv-primary-dark: #398ade; $uv-primary-disabled: #9acafc; $uv-primary-light: #ecf5ff; $uv-warning: #f9ae3d; $uv-warning-dark: #f1a532; $uv-warning-disabled: #f9d39b; $uv-warning-light: #fdf6ec; $uv-success: #5ac725; $uv-success-dark: #53c21d; $uv-success-disabled: #a9e08f; $uv-success-light: #f5fff0; $uv-error: #f56c6c; $uv-error-dark: #e45656; $uv-error-disabled: #f7b2b2; $uv-error-light: #fef0f0; $uv-info: #909399; $uv-info-dark: #767a82; $uv-info-disabled: #c4c6c9; $uv-info-light: #f4f4f5; @mixin flex($direction: row) { /* #ifndef APP-NVUE */ display: flex; /* #endif */ flex-direction: $direction; } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-upload/changelog.md ================================================ ## 1.0.5(2023-08-31) 1. 添加uv-popup依赖 ## 1.0.4(2023-08-18) 1. 修复图片预览位置错误的BUG 2. 修复视频预览不生效的BUG 3. 修复改变上传视频宽高不生效的BUG ## 1.0.3(2023-07-03) 去除插槽判断,避免某些平台不显示的BUG ## 1.0.2(2023-05-24) 1. 优化fileList,watch中增加deep属性 ## 1.0.1(2023-05-16) 1. 优化组件依赖,修改后无需全局引入,组件导入即可使用 2. 优化部分功能 ## 1.0.0(2023-05-10) uv-upload 上传 ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-upload/components/uv-preview-video/uv-preview-video.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-upload/components/uv-upload/mixin.js ================================================ import { error } from '@/uni_modules/uv-ui-tools/libs/function/index.js' export default { watch: { // 监听accept的变化,判断是否符合个平台要求 // 只有微信小程序才支持选择媒体,文件类型,所以这里做一个判断提示 accept: { immediate: true, handler(val) { // #ifndef MP-WEIXIN if (val === 'all' || val === 'media') { error('只有微信小程序才支持把accept配置为all、media之一') } // #endif // #ifndef H5 || MP-WEIXIN if (val === 'file') { error('只有微信小程序和H5(HX2.9.9)才支持把accept配置为file') } // #endif } } } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-upload/components/uv-upload/props.js ================================================ export default { props: { // 接受的文件类型, 可选值为all media image file video accept: { type: String, default: 'image' }, // 图片或视频拾取模式,当accept为image类型时设置capture可选额外camera可以直接调起摄像头 capture: { type: [String, Array], default: () => ['album', 'camera'] }, // 当accept为video时生效,是否压缩视频,默认为true compressed: { type: Boolean, default: true }, // 当accept为video时生效,可选值为back或front camera: { type: String, default: 'back' }, // 当accept为video时生效,拍摄视频最长拍摄时间,单位秒 maxDuration: { type: Number, default: 60 }, // 上传区域的图标,只能内置图标 uploadIcon: { type: String, default: 'camera-fill' }, // 上传区域的图标的颜色,默认 uploadIconColor: { type: String, default: '#D3D4D6' }, // 是否开启文件读取前事件 useBeforeRead: { type: Boolean, default: false }, // 读取后的处理函数 afterRead: { type: Function, default: null }, // 读取前的处理函数 beforeRead: { type: Function, default: null }, // 是否开启图片预览功能 previewFullImage: { type: Boolean, default: true }, // 是否开启视频预览功能 previewFullVideo: { type: Boolean, default: true }, // 最大上传数量 maxCount: { type: [String, Number], default: 52 }, // 是否禁用 disabled: { type: Boolean, default: false }, // 预览上传的图片时的裁剪模式,和image组件mode属性一致 imageMode: { type: String, default: 'aspectFill' }, // 标识符,可以在回调函数的第二项参数中获取 name: { type: String, default: '' }, // 所选的图片的尺寸, 可选值为original compressed sizeType: { type: Array, default: () => ['original', 'compressed'] }, // 是否开启图片多选,部分安卓机型不支持 multiple: { type: Boolean, default: false }, // 是否展示删除按钮 deletable: { type: Boolean, default: true }, // 文件大小限制,单位为byte maxSize: { type: [String, Number], default: Number.MAX_VALUE }, // 显示已上传的文件列表 fileList: { type: Array, default: () => [] }, // 上传区域的提示文字 uploadText: { type: String, default: '' }, // 内部预览图片区域和选择图片按钮的区域宽度 width: { type: [String, Number], default: 80 }, // 内部预览图片区域和选择图片按钮的区域高度 height: { type: [String, Number], default: 80 }, // 是否在上传完成后展示预览图 previewImage: { type: Boolean, default: true }, ...uni.$uv?.props?.upload } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-upload/components/uv-upload/utils.js ================================================ function pickExclude(obj, keys) { // 某些情况下,type可能会为 if (!['[object Object]', '[object File]'].includes(Object.prototype.toString.call(obj))) { return {} } return Object.keys(obj).reduce((prev, key) => { if (!keys.includes(key)) { prev[key] = obj[key] } return prev }, {}) } function formatImage(res) { return res.tempFiles.map((item) => ({ ...pickExclude(item, ['path']), type: 'image', url: item.path, thumb: item.path, size: item.size, // #ifdef H5 name: item.name // #endif })) } function formatVideo(res) { return [ { ...pickExclude(res, ['tempFilePath', 'thumbTempFilePath', 'errMsg']), type: 'video', url: res.tempFilePath, thumb: res.thumbTempFilePath, size: res.size, // #ifdef H5 name: res.name // #endif } ] } function formatMedia(res) { return res.tempFiles.map((item) => ({ ...pickExclude(item, ['fileType', 'thumbTempFilePath', 'tempFilePath']), type: res.type, url: item.tempFilePath, thumb: res.type === 'video' ? item.thumbTempFilePath : item.tempFilePath, size: item.size })) } function formatFile(res) { return res.tempFiles.map((item) => ({ ...pickExclude(item, ['path']), url: item.path, size:item.size, // #ifdef H5 name: item.name, type: item.type // #endif })) } export function chooseFile({ accept, multiple, capture, compressed, maxDuration, sizeType, camera, maxCount }) { return new Promise((resolve, reject) => { switch (accept) { case 'image': uni.chooseImage({ count: multiple ? Math.min(maxCount, 9) : 1, sourceType: capture, sizeType, success: (res) => resolve(formatImage(res)), fail: reject }) break // #ifdef MP-WEIXIN // 只有微信小程序才支持chooseMedia接口 case 'media': wx.chooseMedia({ count: multiple ? Math.min(maxCount, 9) : 1, sourceType: capture, maxDuration, sizeType, camera, success: (res) => resolve(formatMedia(res)), fail: reject }) break // #endif case 'video': uni.chooseVideo({ sourceType: capture, compressed, maxDuration, camera, success: (res) => resolve(formatVideo(res)), fail: reject }) break // #ifdef MP-WEIXIN || H5 // 只有微信小程序才支持chooseMessageFile接口 case 'file': // #ifdef MP-WEIXIN wx.chooseMessageFile({ count: multiple ? maxCount : 1, type: accept, success: (res) => resolve(formatFile(res)), fail: reject }) // #endif // #ifdef H5 // 需要hx2.9.9以上才支持uni.chooseFile uni.chooseFile({ count: multiple ? maxCount : 1, type: accept, success: (res) => resolve(formatFile(res)), fail: reject }) // #endif break // #endif default: // 此为保底选项,在accept不为上面任意一项的时候选取全部文件 // #ifdef MP-WEIXIN wx.chooseMessageFile({ count: multiple ? maxCount : 1, type: 'all', success: (res) => resolve(formatFile(res)), fail: reject }) // #endif // #ifdef H5 // 需要hx2.9.9以上才支持uni.chooseFile uni.chooseFile({ count: multiple ? maxCount : 1, type: 'all', success: (res) => resolve(formatFile(res)), fail: reject }) // #endif } }) } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-upload/components/uv-upload/uv-upload.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-upload/package.json ================================================ { "id": "uv-upload", "displayName": "uv-upload 上传 全面兼容小程序、nvue、vue2、vue3等多端", "version": "1.0.5", "description": "该组件用于上传图片等文件场景。", "keywords": [ "uv-upload", "uvui", "uv-ui", "upload", "上传" ], "repository": "", "engines": { "HBuilderX": "^3.1.0" }, "dcloudext": { "type": "component-vue", "sale": { "regular": { "price": "0.00" }, "sourcecode": { "price": "0.00" } }, "contact": { "qq": "" }, "declaration": { "ads": "无", "data": "插件不采集任何数据", "permissions": "无" }, "npmurl": "" }, "uni_modules": { "dependencies": [ "uv-ui-tools", "uv-icon", "uv-loading-icon", "uv-popup" ], "encrypt": [], "platforms": { "cloud": { "tcb": "y", "aliyun": "y" }, "client": { "Vue": { "vue2": "y", "vue3": "y" }, "App": { "app-vue": "y", "app-nvue": "y" }, "H5-mobile": { "Safari": "y", "Android Browser": "y", "微信浏览器(Android)": "y", "QQ浏览器(Android)": "y" }, "H5-pc": { "Chrome": "y", "IE": "y", "Edge": "y", "Firefox": "y", "Safari": "y" }, "小程序": { "微信": "y", "阿里": "y", "百度": "y", "字节跳动": "y", "QQ": "y", "钉钉": "u", "快手": "u", "飞书": "u", "京东": "u" }, "快应用": { "华为": "u", "联盟": "u" } } } } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-upload/readme.md ================================================ ## Upload 上传 > **组件名:uv-upload** 该组件用于上传图片等文件场景。 ### 查看文档 ### [完整示例项目下载 | 关注更多组件](https://ext.dcloud.net.cn/plugin?name=uv-ui) #### 如使用过程中有任何问题,或者您对uv-ui有一些好的建议,欢迎加入 uv-ui 交流群:uv-ui官方QQ群 ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-vtabs/changelog.md ================================================ ## 1.0.5(2023-06-27) 修复:非联动,内容过多的情况,滚动一段距离,再切换未滚动到顶部的BUG ## 1.0.4(2023-06-13) 1. 增加scrolltolower回调函数 2. 优化 ## 1.0.3(2023-06-13) 1. 优化 ## 1.0.2(2023-06-13) 1. 增加hdHeight参数,避免顶部有内容计算联动不准确的BUG 2. 优化滑动触发频率,避免跳动 ## 1.0.1(2023-06-04) 1. 文档说明 ## 1.0.0(2023-06-04) 1. 新增垂直选项卡组件 ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-vtabs/components/uv-vtabs/props.js ================================================ export default { props: { // 列表数据 list: { type: Array, default: ()=>[] }, // 从list元素对象中读取的键名,默认name keyName: { type: String, default: 'name' }, // 当前选中项 current: { type: [Number, String], default: 0 }, // 头部内容的高度 hdHeight: { type: [Number, String], default: 0 }, // 是否联动,默认开启联动 chain: { type: Boolean, default: true }, // 整个列表的高度,默认auto屏幕高度 height: { type: [Number, String], default: 'auto' }, // 左边列表的宽度,默认200rpx barWidth: { type: [Number, String], default: '180rpx' }, // 左边列表是否允许滚动 barScrollable: { type: Boolean, default: true }, // 背景颜色 默认主题颜色 $bg-color barBgColor: { type: String, default: '' }, // 左边列表的自定义样式 barStyle: { type: Object, default: ()=>{} }, // 左边列表项的自定义样式 barItemStyle: { type: Object, default: ()=>{} }, // 左边选择项激活时的自定义样式 barItemActiveStyle: { type: Object, default: ()=>{} }, // 左边选择项激活时的左边线条自定义样式 barItemActiveLineStyle: { type: Object, default: ()=>{} }, // 菜单项中的徽标自定义样式,比如定位位置 barItemBadgeStyle: { type: Object, default: ()=>{} }, // 右边区域自定义样式 contentStyle: { type: Object, default: ()=>{} } } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-vtabs/components/uv-vtabs/uv-vtabs.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-vtabs/components/uv-vtabs-item/uv-vtabs-item.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-vtabs/package.json ================================================ { "id": "uv-vtabs", "displayName": "uv-vtabs 垂直选项卡 商品分类 灵活配置 多端兼容开箱即用", "version": "1.0.5", "description": "uv-vtabs 垂直分类组件主要用于分类选择,简单配置即可使用,左右自动进行联动,不用自己再去做复杂的计算,组件内部已经完成相关计算。支持联动和不联动,vue3和vue2多端兼容,开箱即用。", "keywords": [ "uv-vtabs", "uvui", "uv-ui", "垂直分类", "垂直选项卡" ], "repository": "", "engines": { }, "dcloudext": { "type": "component-vue", "sale": { "regular": { "price": "0.00" }, "sourcecode": { "price": "0.00" } }, "contact": { "qq": "" }, "declaration": { "ads": "无", "data": "插件不采集任何数据", "permissions": "无" }, "npmurl": "" }, "uni_modules": { "dependencies": [ "uv-ui-tools", "uv-badge" ], "encrypt": [], "platforms": { "cloud": { "tcb": "y", "aliyun": "y" }, "client": { "Vue": { "vue2": "y", "vue3": "y" }, "App": { "app-vue": "y", "app-nvue": "y" }, "H5-mobile": { "Safari": "y", "Android Browser": "y", "微信浏览器(Android)": "y", "QQ浏览器(Android)": "y" }, "H5-pc": { "Chrome": "y", "IE": "y", "Edge": "y", "Firefox": "y", "Safari": "y" }, "小程序": { "微信": "y", "阿里": "y", "百度": "y", "字节跳动": "y", "QQ": "y", "钉钉": "u", "快手": "u", "飞书": "u", "京东": "u" }, "快应用": { "华为": "u", "联盟": "u" } } } } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-vtabs/readme.md ================================================ ## Vtabs 垂直选项卡 > **组件名:uv-vtabs** 该组件主要用于分类选择,开箱即用,简单配置参数即可使用,左右自动进行联动,不用自己再去做复杂的计算,组件内部已经完成相关计算。联动和不联动两种可选方式,联动-左右均可滚动,不联动-右边区域只会在选中时显示。 ### 查看文档 ### [完整示例项目下载 | 关注更多组件](https://ext.dcloud.net.cn/plugin?name=uv-ui) #### 如使用过程中有任何问题,或者您对uv-ui有一些好的建议,欢迎加入 uv-ui 交流群:uv-ui官方QQ群 ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-waterfall/changelog.md ================================================ ## 1.0.8(2023-08-17) 1. 修复只有一条数据切换时可能存在位置错误的BUG ## 1.0.7(2023-07-22) 1. 避免快速切换报错的BUG ## 1.0.6(2023-07-17) 1. 优化文档 2. 优化其他 ## 1.0.5(2023-07-14) 1. 优化changeList未处理数据时,正确返回对应列的数据,避免误导 ## 1.0.4(2023-05-27) 1. 修复在百度小程序中可能存在的BUG 2. 去掉原有的slot方式 ## 1.0.3(2023-05-23) 1. 修复在百度/头条小程序显示异常等BUG 2. 增加changeList回调函数处理数据 3. 更新示例 ## 1.0.2(2023-05-16) 1. 优化组件依赖,修改后无需全局引入,组件导入即可使用 2. 优化部分功能 ## 1.0.1(2023-05-12) 1. 增加clear回调函数 2. 增加remove回调函数 ## 1.0.0(2023-05-10) uv-waterfall 瀑布流 ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-waterfall/components/uv-waterfall/props.js ================================================ export default { props: { // 瀑布流数据 // #ifdef VUE2 value: { type: Array, default: () => [] }, // #endif // #ifdef VUE3 modelValue: { type: Array, default: () => [] }, // #endif // 数据的id值,根据id值对数据执行删除操作 // 如数据为:{id: 1, name: 'uv-ui'},那么该值设置为id idKey: { type: String, default: 'id' }, // 每次插入数据的事件间隔,间隔越长能保证两列高度相近,但是用户体验不好,单位ms addTime: { type: Number, default: 200 }, // 瀑布流的列数,默认2,最高为5 columnCount: { type: [Number, String], default: 2 }, // 列与列的间隙,默认20 columnGap: { type: [Number, String], default: 20 }, // 左边和列表的间隙 leftGap: { type: [Number, String], default: 0 }, // 右边和列表的间隙 rightGap: { type: [Number, String], default: 0 }, // 是否显示滚动条,仅nvue生效 showScrollbar: { type: [Boolean], default: false }, // 列宽,nvue生效 columnWidth: { type: [Number, String], default: 'auto' }, // 瀑布流的宽度,nvue生效 width: { type: [Number, String], default: '' }, // 瀑布流的高度,nvue生效 height: { type: [Number, String], default: '' }, ...uni.$uv?.props?.waterfall } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-waterfall/components/uv-waterfall/uv-waterfall.vue ================================================ ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-waterfall/package.json ================================================ { "id": "uv-waterfall", "displayName": "uv-waterfall 瀑布流 全面兼容vue3+2、app、h5、小程序等多端", "version": "1.0.8", "description": "该组件主要用于瀑布流式布局显示,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部,同时集成nvue的原生瀑布流。", "keywords": [ "uv-waterfall", "uvui", "uv-ui", "waterfall", "瀑布流" ], "repository": "", "engines": { "HBuilderX": "^3.1.0" }, "dcloudext": { "type": "component-vue", "sale": { "regular": { "price": "0.00" }, "sourcecode": { "price": "0.00" } }, "contact": { "qq": "" }, "declaration": { "ads": "无", "data": "插件不采集任何数据", "permissions": "无" }, "npmurl": "" }, "uni_modules": { "dependencies": [ "uv-ui-tools", "uv-image", "uv-loading-icon" ], "encrypt": [], "platforms": { "cloud": { "tcb": "y", "aliyun": "y" }, "client": { "Vue": { "vue2": "y", "vue3": "y" }, "App": { "app-vue": "y", "app-nvue": "y" }, "H5-mobile": { "Safari": "y", "Android Browser": "y", "微信浏览器(Android)": "y", "QQ浏览器(Android)": "y" }, "H5-pc": { "Chrome": "y", "IE": "y", "Edge": "y", "Firefox": "y", "Safari": "y" }, "小程序": { "微信": "y", "阿里": "y", "百度": "y", "字节跳动": "y", "QQ": "y", "钉钉": "u", "快手": "u", "飞书": "u", "京东": "u" }, "快应用": { "华为": "u", "联盟": "u" } } } } } ================================================ FILE: yshop-drink-uniapp-vue3/uni_modules/uv-waterfall/readme.md ================================================ ## Waterfall 瀑布流 > **组件名:uv-waterfall** 该组件主要用于瀑布流式布局显示,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部,同时集成`nvue`的原生瀑布流用于`app-nvue`。常用于一些电商商品展示等,如某宝首页、x红书等。 研究uniapp瀑布流多年,**该方式是目前小程序端最佳方案**,灵活配置,简单易用,开箱即用。 该插件请根据文档耐心查看,`vue`的写法稍微麻烦点,但是效果是很好的,比之前上线的两个版本的瀑布流适用,更有扩展性,我自己的上线项目也是用的此插件。 # 查看文档 ## [下载完整示例项目](https://ext.dcloud.net.cn/plugin?name=uv-ui) ### [更多插件,请关注uv-ui组件库](https://ext.dcloud.net.cn/plugin?name=uv-ui) ![image](https://mp-a667b617-c5f1-4a2d-9a54-683a67cff588.cdn.bspapp.com/uv-ui/banner.png) #### 如使用过程中有任何问题反馈,或者您对uv-ui有一些好的建议,欢迎加入uv-ui官方交流群:官方QQ群 ================================================ FILE: yshop-drink-uniapp-vue3/utils/cookie.js ================================================ const doc = null const CACHE_KEY = 'clear_0.0.1' // const doc = window.document; function get(key) { return uni.getStorageSync(key) } function all() { return uni.getStorageInfoSync() } function set(key, data, time) { console.log("--> % set % key:\n", key) console.log("--> % set % data:\n", data) if (!key) { return } uni.setStorageSync(key, data) } function remove(key) { if (!key || !_has(key)) { return } uni.removeStorageSync(key) } function clearAll() { const res = uni.getStorageInfoSync() res.keys.map(item => { if (item == 'redirect' || item == 'spread' || item == CACHE_KEY) { return } remove(item) }) } function _has(key) { if (!key) { return } let value = uni.getStorageSync(key) if (value) { return true } return false } export default { get, all, set, remove, clearAll, has: _has, CACHE_KEY, } ================================================ FILE: yshop-drink-uniapp-vue3/utils/index.js ================================================ import stringify from '@/utils/querystring' import router from './router' import cookie from './cookie' export const handleLoginFailure = () => { // router.replace({ // path: '/pages/login/login', // }) uni.redirectTo({ url: '/pages/login/login', }) } export function parseUrl(location) { if (typeof location === 'string') return location const { url, query } = location const queryStr = stringify(query) if (!queryStr) { return url } return `${url}?${queryStr}` } const toAuth = () => { uni.showToast({ title: '暂未开放', icon: 'none', duration: 2000, }) } export default { install: (app, options) => { // 在这里编写插件代码 // 注入一个全局可用的 $translate() 方法 app.config.globalProperties.$yrouter = router app.config.globalProperties.$cookie = cookie app.config.globalProperties.$toAuth = toAuth app.config.globalProperties.$onClickLeft = () => { router.back() //uni.navigateBack() //const mypage = getCurrentPages() //console.log('mypage:',mypage) } // #ifdef H5 app.config.globalProperties.$platform = 'h5' // #endif // #ifdef APP-PLUS // app端 app.config.globalProperties.$platform = 'app' // #endif // #ifdef MP-WEIXIN app.config.globalProperties.$platform = 'routine' // #endif }, } ================================================ FILE: yshop-drink-uniapp-vue3/utils/querystring.js ================================================ // Copyright Joyent, Inc. and other Node contributors. // // 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. var stringifyPrimitive = function (v) { switch (typeof v) { case 'string': return v case 'boolean': return v ? 'true' : 'false' case 'number': return isFinite(v) ? v : '' default: return '' } } function stringify(obj, sep, eq, name) { sep = sep || '&' eq = eq || '=' if (obj === null) { obj = undefined } if (typeof obj === 'object') { return Object.keys(obj).map(function (k) { var ks = stringifyPrimitive(k) + eq if (Array.isArray(obj[k])) { return obj[k].map(function (v) { return ks + stringifyPrimitive(v) }).join(sep) } else { return ks + stringifyPrimitive(obj[k]) } }).filter(Boolean).join(sep) } if (!name) return '' return stringifyPrimitive(name) + eq + stringifyPrimitive(obj) } export default stringify ================================================ FILE: yshop-drink-uniapp-vue3/utils/router.js ================================================ import { parseUrl } from '@/utils' export function navigateTo(location, complete, fail, success) { console.log({ url: parseUrl(location), complete, fail, success, }) uni.navigateTo({ url: parseUrl(location), complete, fail, success, }) } export function replace(location, complete, fail, success) { uni.redirectTo({ url: parseUrl(location), complete, fail, success, }) } export function reLaunch(location, complete, fail, success) { uni.reLaunch({ url: parseUrl(location), complete, fail, success, }) } export function go(delta) { uni.navigateBack({ delta, }) } export function back() { const mypage = getCurrentPages() if(mypage.length == 1) { uni.switchTab({ url: '/pages/index/index' }) return } uni.navigateBack({ delta: 1, success: function (e) {}, fail: function (e) { console.log('aaaa:') }, }) } export function switchTab(location, complete, fail, success) { uni.switchTab({ url: parseUrl(location), complete, fail, success, }) } export default { back, navigateTo, replace, reLaunch, switchTab, } ================================================ FILE: yshop-drink-uniapp-vue3/utils/util.js ================================================ export function formatTime(time) { if (typeof time !== 'number' || time < 0) { return time } var hour = parseInt(time / 3600) time = time % 3600 var minute = parseInt(time / 60) time = time % 60 var second = time return ([hour, minute, second]).map(function(n) { n = n.toString() return n[1] ? n : '0' + n }).join(':') } export function formatDateTime(date, fmt = 'yyyy-MM-dd hh:mm:ss') { if(!date) { return '' } if (typeof (date) === 'number') { date = new Date(date) } var o = { "M+": date.getMonth() + 1, //月份 "d+": date.getDate(), //日 "h+": date.getHours(), //小时 "m+": date.getMinutes(), //分 "s+": date.getSeconds(), //秒 "q+": Math.floor((date.getMonth() + 3) / 3), //季度 "S": date.getMilliseconds() //毫秒 } if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length)) for (var k in o) if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))) return fmt } export function formatLocation(longitude, latitude) { if (typeof longitude === 'string' && typeof latitude === 'string') { longitude = parseFloat(longitude) latitude = parseFloat(latitude) } longitude = longitude.toFixed(2) latitude = latitude.toFixed(2) return { longitude: longitude.toString().split('.'), latitude: latitude.toString().split('.') } } var dateUtils = { UNITS: { '年': 31557600000, '月': 2629800000, '天': 86400000, '小时': 3600000, '分钟': 60000, '秒': 1000 }, humanize: function(milliseconds) { var humanize = ''; for (var key in this.UNITS) { if (milliseconds >= this.UNITS[key]) { humanize = Math.floor(milliseconds / this.UNITS[key]) + key + '前'; break; } } return humanize || '刚刚'; }, format: function(dateStr) { var date = this.parse(dateStr) var diff = Date.now() - date.getTime(); if (diff < this.UNITS['天']) { return this.humanize(diff); } var _format = function(number) { return (number < 10 ? ('0' + number) : number); }; return date.getFullYear() + '/' + _format(date.getMonth() + 1) + '/' + _format(date.getDate()) + '-' + _format(date.getHours()) + ':' + _format(date.getMinutes()); }, parse: function(str) { //将"yyyy-mm-dd HH:MM:ss"格式的字符串,转化为一个Date对象 var a = str.split(/[^0-9]/); return new Date(a[0], a[1] - 1, a[2], a[3], a[4], a[5]); } }; // 返回上一页 export function prePage(page = null){ let pages = getCurrentPages(); //console.log('pages:',pages); let prePage = pages[pages.length - 2]; if (page !== null) { prePage = pages[page]; } // #ifdef H5 return prePage; // #endif return prePage.$vm; } export function kmUnit(m){ var v; if(typeof m === 'number' && !isNaN(m)){ if (m >= 1000) { v = (m / 1000).toFixed(2) + 'km' } else { v = m + 'm' } }else{ v = '0m' } return v; } export function isWeixin() { if (navigator && navigator.userAgent && navigator.userAgent.toLowerCase().indexOf('micromessenger') !== -1) { return true } return false } export function parseQuery() { let res = {} // #ifdef H5 const query = (location.href.split('?')[1] || '').trim().replace(/^(\?|#|&)/, '') if (!query) { return res } query.split('&').forEach(param => { const parts = param.replace(/\+/g, ' ').split('=') const key = decodeURIComponent(parts.shift()) const val = parts.length > 0 ? decodeURIComponent(parts.join('=')) : null if (res[key] === undefined) { res[key] = val } else if (Array.isArray(res[key])) { res[key].push(val) } else { res[key] = [res[key], val] } }) // #endif // #ifndef H5 var pages = getCurrentPages() //获取加载的页面 var currentPage = pages[pages.length - 1] //获取当前页面的对象 var url = currentPage.route //当前页面url res = currentPage.options //如果要获取url中所带的参数可以查看options // #endif return res } ================================================ FILE: yshop-drink-uniapp-vue3/vue.config.js ================================================ // module.exports = { // devServer: { // proxy: { // '/app-api': { // target: 'http://yshop.l1.ttut.cc/app-api', // changeOrigin: true, // }, // }, // }, // } ================================================ FILE: yshop-drink-vue3/.editorconfig ================================================ root = true [*.{js,ts,vue}] charset = utf-8 # 设置文件字符集为 utf-8 end_of_line = lf # 控制换行类型(lf | cr | crlf) insert_final_newline = true # 始终在文件末尾插入一个新行 indent_style = space # 缩进风格(tab | space) indent_size = 2 # 缩进大小 max_line_length = 100 # 最大行长度 [*.md] # 仅 md 文件适用以下规则 max_line_length = off # 关闭最大行长度限制 trim_trailing_whitespace = false # 关闭末尾空格修剪 ================================================ FILE: yshop-drink-vue3/.eslintignore ================================================ /build/ /config/ /dist/ /*.js /test/unit/coverage/ /node_modules/* /dist* /src/main.ts ================================================ FILE: yshop-drink-vue3/.eslintrc-auto-import.json ================================================ { "globals": { "EffectScope": true, "ElMessage": true, "ElMessageBox": true, "ElTag": true, "asyncComputed": true, "autoResetRef": true, "computed": true, "computedAsync": true, "computedEager": true, "computedInject": true, "computedWithControl": true, "controlledComputed": true, "controlledRef": true, "createApp": true, "createEventHook": true, "createGlobalState": true, "createInjectionState": true, "createReactiveFn": true, "createSharedComposable": true, "createUnrefFn": true, "customRef": true, "debouncedRef": true, "debouncedWatch": true, "defineAsyncComponent": true, "defineComponent": true, "eagerComputed": true, "effectScope": true, "extendRef": true, "getCurrentInstance": true, "getCurrentScope": true, "h": true, "ignorableWatch": true, "inject": true, "isDefined": true, "isProxy": true, "isReactive": true, "isReadonly": true, "isRef": true, "makeDestructurable": true, "markRaw": true, "nextTick": true, "onActivated": true, "onBeforeMount": true, "onBeforeUnmount": true, "onBeforeUpdate": true, "onClickOutside": true, "onDeactivated": true, "onErrorCaptured": true, "onKeyStroke": true, "onLongPress": true, "onMounted": true, "onRenderTracked": true, "onRenderTriggered": true, "onScopeDispose": true, "onServerPrefetch": true, "onStartTyping": true, "onUnmounted": true, "onUpdated": true, "pausableWatch": true, "provide": true, "reactify": true, "reactifyObject": true, "reactive": true, "reactiveComputed": true, "reactiveOmit": true, "reactivePick": true, "readonly": true, "ref": true, "refAutoReset": true, "refDebounced": true, "refDefault": true, "refThrottled": true, "refWithControl": true, "resolveComponent": true, "resolveRef": true, "resolveUnref": true, "shallowReactive": true, "shallowReadonly": true, "shallowRef": true, "syncRef": true, "syncRefs": true, "templateRef": true, "throttledRef": true, "throttledWatch": true, "toRaw": true, "toReactive": true, "toRef": true, "toRefs": true, "triggerRef": true, "tryOnBeforeMount": true, "tryOnBeforeUnmount": true, "tryOnMounted": true, "tryOnScopeDispose": true, "tryOnUnmounted": true, "unref": true, "unrefElement": true, "until": true, "useActiveElement": true, "useArrayEvery": true, "useArrayFilter": true, "useArrayFind": true, "useArrayFindIndex": true, "useArrayJoin": true, "useArrayMap": true, "useArrayReduce": true, "useArraySome": true, "useAsyncQueue": true, "useAsyncState": true, "useAttrs": true, "useBase64": true, "useBattery": true, "useBluetooth": true, "useBreakpoints": true, "useBroadcastChannel": true, "useBrowserLocation": true, "useCached": true, "useClipboard": true, "useColorMode": true, "useConfirmDialog": true, "useCounter": true, "useCssModule": true, "useCssVar": true, "useCssVars": true, "useCurrentElement": true, "useCycleList": true, "useDark": true, "useDateFormat": true, "useDebounce": true, "useDebounceFn": true, "useDebouncedRefHistory": true, "useDeviceMotion": true, "useDeviceOrientation": true, "useDevicePixelRatio": true, "useDevicesList": true, "useDisplayMedia": true, "useDocumentVisibility": true, "useDraggable": true, "useDropZone": true, "useElementBounding": true, "useElementByPoint": true, "useElementHover": true, "useElementSize": true, "useElementVisibility": true, "useEventBus": true, "useEventListener": true, "useEventSource": true, "useEyeDropper": true, "useFavicon": true, "useFetch": true, "useFileDialog": true, "useFileSystemAccess": true, "useFocus": true, "useFocusWithin": true, "useFps": true, "useFullscreen": true, "useGamepad": true, "useGeolocation": true, "useIdle": true, "useImage": true, "useInfiniteScroll": true, "useIntersectionObserver": true, "useInterval": true, "useIntervalFn": true, "useKeyModifier": true, "useLastChanged": true, "useLocalStorage": true, "useMagicKeys": true, "useManualRefHistory": true, "useMediaControls": true, "useMediaQuery": true, "useMemoize": true, "useMemory": true, "useMounted": true, "useMouse": true, "useMouseInElement": true, "useMousePressed": true, "useMutationObserver": true, "useNavigatorLanguage": true, "useNetwork": true, "useNow": true, "useObjectUrl": true, "useOffsetPagination": true, "useOnline": true, "usePageLeave": true, "useParallax": true, "usePermission": true, "usePointer": true, "usePointerSwipe": true, "usePreferredColorScheme": true, "usePreferredDark": true, "usePreferredLanguages": true, "useRafFn": true, "useRefHistory": true, "useResizeObserver": true, "useRoute": true, "useRouter": true, "useScreenOrientation": true, "useScreenSafeArea": true, "useScriptTag": true, "useScroll": true, "useScrollLock": true, "useSessionStorage": true, "useShare": true, "useSlots": true, "useSpeechRecognition": true, "useSpeechSynthesis": true, "useStepper": true, "useStorage": true, "useStorageAsync": true, "useStyleTag": true, "useSupported": true, "useSwipe": true, "useTemplateRefsList": true, "useTextDirection": true, "useTextSelection": true, "useTextareaAutosize": true, "useThrottle": true, "useThrottleFn": true, "useThrottledRefHistory": true, "useTimeAgo": true, "useTimeout": true, "useTimeoutFn": true, "useTimeoutPoll": true, "useTimestamp": true, "useTitle": true, "useToggle": true, "useTransition": true, "useUrlSearchParams": true, "useUserMedia": true, "useVModel": true, "useVModels": true, "useVibrate": true, "useVirtualList": true, "useWakeLock": true, "useWebNotification": true, "useWebSocket": true, "useWebWorker": true, "useWebWorkerFn": true, "useWindowFocus": true, "useWindowScroll": true, "useWindowSize": true, "watch": true, "watchArray": true, "watchAtMost": true, "watchDebounced": true, "watchEffect": true, "watchIgnorable": true, "watchOnce": true, "watchPausable": true, "watchPostEffect": true, "watchSyncEffect": true, "watchThrottled": true, "watchTriggerable": true, "watchWithFilter": true, "whenever": true } } ================================================ FILE: yshop-drink-vue3/.eslintrc.js ================================================ // @ts-check const { defineConfig } = require('eslint-define-config') module.exports = defineConfig({ root: true, env: { browser: true, node: true, es6: true }, parser: 'vue-eslint-parser', parserOptions: { parser: '@typescript-eslint/parser', ecmaVersion: 2020, sourceType: 'module', jsxPragma: 'React', ecmaFeatures: { jsx: true } }, extends: [ 'plugin:vue/vue3-recommended', 'plugin:@typescript-eslint/recommended', 'prettier', 'plugin:prettier/recommended', '@unocss' ], rules: { 'vue/no-setup-props-destructure': 'off', 'vue/script-setup-uses-vars': 'error', 'vue/no-reserved-component-names': 'off', '@typescript-eslint/ban-ts-ignore': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-empty-function': 'off', 'vue/custom-event-name-casing': 'off', 'no-use-before-define': 'off', '@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/ban-types': 'off', '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-unused-vars': 'off', 'no-unused-vars': 'off', 'space-before-function-paren': 'off', 'vue/attributes-order': 'off', 'vue/one-component-per-file': 'off', 'vue/html-closing-bracket-newline': 'off', 'vue/max-attributes-per-line': 'off', 'vue/multiline-html-element-content-newline': 'off', 'vue/singleline-html-element-content-newline': 'off', 'vue/attribute-hyphenation': 'off', 'vue/require-default-prop': 'off', 'vue/require-explicit-emits': 'off', 'vue/require-toggle-inside-transition': 'off', 'vue/html-self-closing': [ 'error', { html: { void: 'always', normal: 'never', component: 'always' }, svg: 'always', math: 'always' } ], 'vue/multi-word-component-names': 'off', 'vue/no-v-html': 'off', 'prettier/prettier': 'off', // 芋艿:默认关闭 prettier 的 ESLint 校验,因为我们使用的是 IDE 的 Prettier 插件 '@unocss/order': 'off', // 芋艿:禁用 unocss 【css】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐 '@unocss/order-attributify': 'off' // 芋艿:禁用 unocss 【属性】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐 } }) ================================================ FILE: yshop-drink-vue3/.gitignore ================================================ node_modules .DS_Store dist dist-ssr *.local /dist* pnpm-debug auto-*.d.ts .idea .history ================================================ FILE: yshop-drink-vue3/.prettierignore ================================================ /node_modules/** /dist/ /dist* /public/* /docs/* /vite.config.ts /src/types/env.d.ts /src/types/auto-components.d.ts /src/types/auto-imports.d.ts /docs/**/* CHANGELOG ================================================ FILE: yshop-drink-vue3/.stylelintignore ================================================ /dist/* /public/* public/* /dist* /src/types/env.d.ts /docs/**/* ================================================ FILE: yshop-drink-vue3/.vscode/extensions.json ================================================ { "recommendations": [ "christian-kohler.path-intellisense", "vscode-icons-team.vscode-icons", "davidanson.vscode-markdownlint", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "mrmlnc.vscode-less", "lokalise.i18n-ally", "redhat.vscode-yaml", "csstools.postcss", "mikestead.dotenv", "eamodio.gitlens", "antfu.iconify", "antfu.unocss", "Vue.volar" ] } ================================================ FILE: yshop-drink-vue3/.vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "msedge", "request": "launch", "name": "Launch Edge against localhost", "url": "http://localhost", "webRoot": "${workspaceFolder}/src", "sourceMaps": true } ] } ================================================ FILE: yshop-drink-vue3/.vscode/settings.json ================================================ { "typescript.tsdk": "node_modules/typescript/lib", "npm.packageManager": "pnpm", "editor.tabSize": 2, "prettier.printWidth": 100, // 超过最大值换行 "editor.defaultFormatter": "esbenp.prettier-vscode", "files.eol": "\n", "search.exclude": { "**/node_modules": true, "**/*.log": true, "**/*.log*": true, "**/bower_components": true, "**/dist": true, "**/elehukouben": true, "**/.git": true, "**/.gitignore": true, "**/.svn": true, "**/.DS_Store": true, "**/.idea": true, "**/.vscode": false, "**/yarn.lock": true, "**/tmp": true, "out": true, "dist": true, "node_modules": true, "CHANGELOG.md": true, "examples": true, "res": true, "screenshots": true, "yarn-error.log": true, "**/.yarn": true }, "files.exclude": { "**/.cache": true, "**/.editorconfig": true, "**/.eslintcache": true, "**/bower_components": true, "**/.idea": true, "**/tmp": true, "**/.git": true, "**/.svn": true, "**/.hg": true, "**/CVS": true, "**/.DS_Store": true }, "files.watcherExclude": { "**/.git/objects/**": true, "**/.git/subtree-cache/**": true, "**/.vscode/**": true, "**/node_modules/**": true, "**/tmp/**": true, "**/bower_components/**": true, "**/dist/**": true, "**/yarn.lock": true }, "stylelint.enable": true, "stylelint.validate": ["css", "less", "postcss", "scss", "vue", "sass"], "path-intellisense.mappings": { "@/": "${workspaceRoot}/src" }, "[javascriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[typescript]": { "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" }, "[typescriptreact]": { "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" }, "[html]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[css]": { "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" }, "[less]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[scss]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[markdown]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, "[vue]": { "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" }, "i18n-ally.localesPaths": ["src/locales"], "i18n-ally.keystyle": "nested", "i18n-ally.sortKeys": true, "i18n-ally.namespace": false, "i18n-ally.enabledParsers": ["ts"], "i18n-ally.sourceLanguage": "en", "i18n-ally.displayLanguage": "zh-CN", "i18n-ally.enabledFrameworks": ["vue", "react"], "cSpell.words": [ "brotli", "browserslist", "codemirror", "commitlint", "cropperjs", "echart", "echarts", "esnext", "esno", "iconify", "INTLIFY", "lintstagedrc", "logicflow", "nprogress", "pinia", "pnpm", "qrcode", "sider", "sortablejs", "stylelint", "svgs", "unocss", "unplugin", "unref", "videojs", "VITE", "vitejs", "vueuse", "wangeditor", "xingyu", "yudao", "zxcvbn" ], // 控制相关文件嵌套展示 "explorer.fileNesting.enabled": true, "explorer.fileNesting.expand": false, "explorer.fileNesting.patterns": { "*.ts": "$(capture).test.ts, $(capture).test.tsx", "*.tsx": "$(capture).test.ts, $(capture).test.tsx", "*.env": "$(capture).env.*", "package.json": "pnpm-lock.yaml,yarn.lock,LICENSE,README*,CHANGELOG*,CNAME,.gitattributes,.eslintrc-auto-import.json,.gitignore,prettier.config.js,stylelint.config.js,commitlint.config.js,.stylelintignore,.prettierignore,.gitpod.yml,.eslintrc.js,.eslintignore" }, "terminal.integrated.scrollback": 10000, "nuxt.isNuxtApp": false } ================================================ FILE: yshop-drink-vue3/README.md ================================================ 意向订餐系统,类似肯德基点餐小程序模式,支持多门店模式,基础技术Java,uniapp(支持H5、微信小程序) 采用当前流行技术组合的前后端分离点餐系统: SpringBoot3+jdk17+vue3、Spring Security OAuth2、MybatisPlus、SpringSecurity、jwt、redis、Vue3的前后端分离的系统, 包含店铺管理、积分兑换、云小票打印、图片素材库、订单管理、多规格sku、积分、优惠券、充值、多门店、微信公众号等功能,更适合企业或个人二次开发. ================================================ FILE: yshop-drink-vue3/build/vite/index.ts ================================================ import { resolve } from 'path' import Vue from '@vitejs/plugin-vue' import VueJsx from '@vitejs/plugin-vue-jsx' import progress from 'vite-plugin-progress' import EslintPlugin from 'vite-plugin-eslint' import PurgeIcons from 'vite-plugin-purge-icons' import { ViteEjsPlugin } from 'vite-plugin-ejs' // @ts-ignore import ElementPlus from 'unplugin-element-plus/vite' import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' import viteCompression from 'vite-plugin-compression' import topLevelAwait from 'vite-plugin-top-level-await' import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite' import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' import UnoCSS from 'unocss/vite' export function createVitePlugins() { const root = process.cwd() // 路径查找 function pathResolve(dir: string) { return resolve(root, '.', dir) } return [ Vue(), VueJsx(), UnoCSS(), progress(), PurgeIcons(), ElementPlus({}), AutoImport({ include: [ /\.[tj]sx?$/, // .ts, .tsx, .js, .jsx /\.vue$/, /\.vue\?vue/, // .vue /\.md$/ // .md ], imports: [ 'vue', 'vue-router', // 可额外添加需要 autoImport 的组件 { '@/hooks/web/useI18n': ['useI18n'], '@/hooks/web/useMessage': ['useMessage'], '@/hooks/web/useTable': ['useTable'], '@/hooks/web/useCrudSchemas': ['useCrudSchemas'], '@/utils/formRules': ['required'], '@/utils/dict': ['DICT_TYPE'] } ], dts: 'src/types/auto-imports.d.ts', resolvers: [ElementPlusResolver()], eslintrc: { enabled: false, // Default `false` filepath: './.eslintrc-auto-import.json', // Default `./.eslintrc-auto-import.json` globalsPropValue: true // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable') } }), Components({ // 生成自定义 `auto-components.d.ts` 全局声明 dts: 'src/types/auto-components.d.ts', // 自定义组件的解析器 resolvers: [ElementPlusResolver()], globs: ["src/components/**/**.{vue, md}", '!src/components/DiyEditor/components/mobile/**'] }), EslintPlugin({ cache: false, include: ['src/**/*.vue', 'src/**/*.ts', 'src/**/*.tsx'] // 检查的文件 }), VueI18nPlugin({ runtimeOnly: true, compositionOnly: true, include: [resolve(__dirname, 'src/locales/**')] }), createSvgIconsPlugin({ iconDirs: [pathResolve('src/assets/svgs')], symbolId: 'icon-[dir]-[name]', svgoOptions: true }), viteCompression({ verbose: true, // 是否在控制台输出压缩结果 disable: false, // 是否禁用 threshold: 10240, // 体积大于 threshold 才会被压缩,单位 b algorithm: 'gzip', // 压缩算法,可选 [ 'gzip' , 'brotliCompress' ,'deflate' , 'deflateRaw'] ext: '.gz', // 生成的压缩包后缀 deleteOriginFile: false //压缩后是否删除源文件 }), ViteEjsPlugin(), topLevelAwait({ // https://juejin.cn/post/7152191742513512485 // The export name of top-level await promise for each chunk module promiseExportName: '__tla', // The function to generate import names of top-level await promise in each chunk module promiseImportName: (i) => `__tla_${i}` }) ] } ================================================ FILE: yshop-drink-vue3/build/vite/optimize.ts ================================================ const include = [ 'qs', 'url', 'vue', 'sass', 'mitt', 'axios', 'pinia', 'dayjs', 'qrcode', 'unocss', 'vue-router', 'vue-types', 'vue-i18n', 'crypto-js', 'cropperjs', 'lodash-es', 'nprogress', 'web-storage-cache', '@iconify/iconify', '@vueuse/core', '@zxcvbn-ts/core', 'echarts/core', 'echarts/charts', 'echarts/components', 'echarts/renderers', 'echarts-wordcloud', '@wangeditor/editor', '@wangeditor/editor-for-vue', 'element-plus', 'element-plus/es', 'element-plus/es/locale/lang/zh-cn', 'element-plus/es/locale/lang/en', 'element-plus/es/components/avatar/style/css', 'element-plus/es/components/space/style/css', 'element-plus/es/components/backtop/style/css', 'element-plus/es/components/form/style/css', 'element-plus/es/components/radio-group/style/css', 'element-plus/es/components/radio/style/css', 'element-plus/es/components/checkbox/style/css', 'element-plus/es/components/checkbox-group/style/css', 'element-plus/es/components/switch/style/css', 'element-plus/es/components/time-picker/style/css', 'element-plus/es/components/date-picker/style/css', 'element-plus/es/components/descriptions/style/css', 'element-plus/es/components/descriptions-item/style/css', 'element-plus/es/components/link/style/css', 'element-plus/es/components/tooltip/style/css', 'element-plus/es/components/drawer/style/css', 'element-plus/es/components/dialog/style/css', 'element-plus/es/components/checkbox-button/style/css', 'element-plus/es/components/option-group/style/css', 'element-plus/es/components/radio-button/style/css', 'element-plus/es/components/cascader/style/css', 'element-plus/es/components/color-picker/style/css', 'element-plus/es/components/input-number/style/css', 'element-plus/es/components/rate/style/css', 'element-plus/es/components/select-v2/style/css', 'element-plus/es/components/tree-select/style/css', 'element-plus/es/components/slider/style/css', 'element-plus/es/components/time-select/style/css', 'element-plus/es/components/autocomplete/style/css', 'element-plus/es/components/image-viewer/style/css', 'element-plus/es/components/upload/style/css', 'element-plus/es/components/col/style/css', 'element-plus/es/components/form-item/style/css', 'element-plus/es/components/alert/style/css', 'element-plus/es/components/breadcrumb/style/css', 'element-plus/es/components/select/style/css', 'element-plus/es/components/input/style/css', 'element-plus/es/components/breadcrumb-item/style/css', 'element-plus/es/components/tag/style/css', 'element-plus/es/components/pagination/style/css', 'element-plus/es/components/table/style/css', 'element-plus/es/components/table-v2/style/css', 'element-plus/es/components/table-column/style/css', 'element-plus/es/components/card/style/css', 'element-plus/es/components/row/style/css', 'element-plus/es/components/button/style/css', 'element-plus/es/components/menu/style/css', 'element-plus/es/components/sub-menu/style/css', 'element-plus/es/components/menu-item/style/css', 'element-plus/es/components/option/style/css', 'element-plus/es/components/dropdown/style/css', 'element-plus/es/components/dropdown-menu/style/css', 'element-plus/es/components/dropdown-item/style/css', 'element-plus/es/components/skeleton/style/css', 'element-plus/es/components/skeleton/style/css', 'element-plus/es/components/backtop/style/css', 'element-plus/es/components/menu/style/css', 'element-plus/es/components/sub-menu/style/css', 'element-plus/es/components/menu-item/style/css', 'element-plus/es/components/dropdown/style/css', 'element-plus/es/components/tree/style/css', 'element-plus/es/components/dropdown-menu/style/css', 'element-plus/es/components/dropdown-item/style/css', 'element-plus/es/components/badge/style/css', 'element-plus/es/components/breadcrumb/style/css', 'element-plus/es/components/breadcrumb-item/style/css', 'element-plus/es/components/image/style/css', 'element-plus/es/components/collapse-transition/style/css', 'element-plus/es/components/timeline/style/css', 'element-plus/es/components/timeline-item/style/css', 'element-plus/es/components/collapse/style/css', 'element-plus/es/components/collapse-item/style/css', 'element-plus/es/components/button-group/style/css', 'element-plus/es/components/text/style/css' ] const exclude = ['@iconify/json'] export { include, exclude } ================================================ FILE: yshop-drink-vue3/index.html ================================================ %VITE_APP_TITLE%
%VITE_APP_TITLE%
================================================ FILE: yshop-drink-vue3/package.json ================================================ { "name": "yshop-drink-vue3", "version": "3.0.0", "description": "基于vue3、vite4、element-plus、typesScript", "author": "yshop", "private": false, "scripts": { "i": "pnpm install", "dev": "vite", "dev-server": "vite --mode dev", "ts:check": "vue-tsc --noEmit", "build:local": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build", "build:dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode dev", "build:test": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode test", "build:stage": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode stage", "build:prod": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode prod", "serve:dev": "vite preview --mode dev", "serve:prod": "vite preview --mode prod", "preview": "pnpm build:local && vite preview", "clean": "npx rimraf node_modules", "clean:cache": "npx rimraf node_modules/.cache", "lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src", "lint:format": "prettier --write --loglevel warn \"src/**/*.{js,ts,json,tsx,css,less,scss,vue,html,md}\"", "lint:style": "stylelint --fix \"./src/**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/", "lint:lint-staged": "lint-staged -c " }, "dependencies": { "@element-plus/icons-vue": "^2.1.0", "@form-create/designer": "^3.1.3", "@form-create/element-ui": "^3.1.24", "@iconify/iconify": "^3.1.1", "@videojs-player/vue": "^1.0.0", "@vueuse/core": "^10.9.0", "@wangeditor/editor": "^5.1.23", "@wangeditor/editor-for-vue": "^5.1.10", "@zxcvbn-ts/core": "^3.0.4", "animate.css": "^4.1.1", "axios": "^1.6.8", "benz-amr-recorder": "^1.1.5", "bpmn-js-token-simulation": "^0.10.0", "camunda-bpmn-moddle": "^7.0.1", "cropperjs": "^1.6.1", "crypto-js": "^4.2.0", "dayjs": "^1.11.10", "diagram-js": "^12.8.0", "driver.js": "^1.3.1", "echarts": "^5.5.0", "echarts-wordcloud": "^2.1.0", "element-plus": "2.6.1", "fast-xml-parser": "^4.3.2", "highlight.js": "^11.9.0", "jsencrypt": "^3.3.2", "lodash-es": "^4.17.21", "min-dash": "^4.1.1", "mitt": "^3.0.1", "nprogress": "^0.2.0", "pinia": "^2.1.7", "pinia-plugin-persistedstate": "^3.2.1", "qrcode": "^1.5.3", "qs": "^6.12.0", "steady-xml": "^0.1.0", "url": "^0.11.3", "video.js": "^7.21.5", "vue": "3.4.21", "vue-dompurify-html": "^4.1.4", "vue-i18n": "9.10.2", "vue-router": "^4.3.0", "vue-types": "^5.1.1", "vuedraggable": "^4.1.0", "web-storage-cache": "^1.1.1", "xml-js": "^1.6.11", "vue-ueditor-wrap": "^3.0.8", "vue-baidu-map-3x": "^1.0.34" }, "devDependencies": { "@commitlint/cli": "^19.0.1", "@commitlint/config-conventional": "^19.0.0", "@iconify/json": "^2.2.187", "@intlify/unplugin-vue-i18n": "^2.0.0", "@purge-icons/generated": "^0.9.0", "@types/lodash-es": "^4.17.12", "@types/node": "^20.11.21", "@types/nprogress": "^0.2.3", "@types/qrcode": "^1.5.5", "@types/qs": "^6.9.12", "@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/parser": "^7.1.0", "@unocss/transformer-variant-group": "^0.58.5", "@unocss/eslint-config": "^0.57.4", "@vitejs/plugin-legacy": "^5.3.1", "@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue-jsx": "^3.1.0", "autoprefixer": "^10.4.17", "bpmn-js": "8.9.0", "bpmn-js-properties-panel": "0.46.0", "consola": "^3.2.3", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-define-config": "^2.1.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-vue": "^9.22.0", "lint-staged": "^15.2.2", "postcss": "^8.4.35", "postcss-html": "^1.6.0", "postcss-scss": "^4.0.9", "prettier": "^3.2.5", "prettier-eslint": "^16.3.0", "rimraf": "^5.0.5", "rollup": "^4.12.0", "sass": "^1.69.5", "stylelint": "^16.2.1", "stylelint-config-html": "^1.1.0", "stylelint-config-recommended": "^14.0.0", "stylelint-config-standard": "^36.0.0", "stylelint-order": "^6.0.4", "terser": "^5.28.1", "typescript": "5.3.3", "unocss": "^0.58.5", "unplugin-auto-import": "^0.16.7", "unplugin-element-plus": "^0.8.0", "unplugin-vue-components": "^0.25.2", "vite": "5.1.4", "vite-plugin-compression": "^0.5.1", "vite-plugin-ejs": "^1.7.0", "vite-plugin-eslint": "^1.8.1", "vite-plugin-progress": "^0.0.7", "vite-plugin-purge-icons": "^0.10.0", "vite-plugin-svg-icons": "^2.0.1", "vite-plugin-top-level-await": "^1.3.1", "vue-eslint-parser": "^9.3.2", "vue-tsc": "^1.8.27" }, "license": "MIT", "repository": { "type": "git", "url": "git+https://gitee.com/guchengwuyue/yshop-drink" }, "bugs": { "url": "https://gitee.com/guchengwuyue/yshop-drink" }, "homepage": "https://gitee.com/guchengwuyue/yshop-drink", "engines": { "node": ">= 16.0.0", "pnpm": ">=8.6.0" } } ================================================ FILE: yshop-drink-vue3/postcss.config.js ================================================ module.exports = { plugins: { autoprefixer: {} } } ================================================ FILE: yshop-drink-vue3/prettier.config.js ================================================ module.exports = { printWidth: 100, // 每行代码长度(默认80) tabWidth: 2, // 每个tab相当于多少个空格(默认2)ab进行缩进(默认false) useTabs: false, // 是否使用tab semi: false, // 声明结尾使用分号(默认true) vueIndentScriptAndStyle: false, singleQuote: true, // 使用单引号(默认false) quoteProps: 'as-needed', bracketSpacing: true, // 对象字面量的大括号间使用空格(默认true) trailingComma: 'none', // 多行使用拖尾逗号(默认none) jsxSingleQuote: false, // 箭头函数参数括号 默认avoid 可选 avoid| always // avoid 能省略括号的时候就省略 例如x => x // always 总是有括号 arrowParens: 'always', insertPragma: false, requirePragma: false, proseWrap: 'never', htmlWhitespaceSensitivity: 'strict', endOfLine: 'auto', rangeStart: 0 } ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/anchor/anchor.html ================================================
================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/attachment/attachment.css ================================================ @charset "utf-8"; /* dialog样式 */ .wrapper { zoom: 1; width: 630px; *width: 626px; height: 380px; margin: 0 auto; padding: 10px; position: relative; font-family: sans-serif; } /*tab样式框大小*/ .tabhead { float:left; } .tabbody { width: 100%; height: 346px; position: relative; clear: both; } .tabbody .panel { position: absolute; width: 0; height: 0; background: #fff; overflow: hidden; display: none; } .tabbody .panel.focus { width: 100%; height: 346px; display: block; } /* 上传附件 */ .tabbody #upload.panel { width: 0; height: 0; overflow: hidden; position: absolute !important; clip: rect(1px, 1px, 1px, 1px); background: #fff; display: block; } .tabbody #upload.panel.focus { width: 100%; height: 346px; display: block; clip: auto; } #upload .queueList { margin: 0; width: 100%; height: 100%; position: absolute; overflow: hidden; } #upload p { margin: 0; } .element-invisible { width: 0 !important; height: 0 !important; border: 0; padding: 0; margin: 0; overflow: hidden; position: absolute !important; clip: rect(1px, 1px, 1px, 1px); } #upload .placeholder { margin: 10px; border: 2px dashed #e6e6e6; *border: 0px dashed #e6e6e6; height: 172px; padding-top: 150px; text-align: center; background: url(./images/image.png) center 70px no-repeat; color: #cccccc; font-size: 18px; position: relative; top:0; *top: 10px; } #upload .placeholder .webuploader-pick { font-size: 18px; background: #00b7ee; border-radius: 3px; line-height: 44px; padding: 0 30px; *width: 120px; color: #fff; display: inline-block; margin: 0 auto 20px auto; cursor: pointer; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); } #upload .placeholder .webuploader-pick-hover { background: #00a2d4; } #filePickerContainer { text-align: center; } #upload .placeholder .flashTip { color: #666666; font-size: 12px; position: absolute; width: 100%; text-align: center; bottom: 20px; } #upload .placeholder .flashTip a { color: #0785d1; text-decoration: none; } #upload .placeholder .flashTip a:hover { text-decoration: underline; } #upload .placeholder.webuploader-dnd-over { border-color: #999999; } #upload .filelist { list-style: none; margin: 0; padding: 0; overflow-x: hidden; overflow-y: auto; position: relative; height: 300px; } #upload .filelist:after { content: ''; display: block; width: 0; height: 0; overflow: hidden; clear: both; } #upload .filelist li { width: 113px; height: 113px; background: url(./images/bg.png); text-align: center; margin: 9px 0 0 9px; *margin: 6px 0 0 6px; position: relative; display: block; float: left; overflow: hidden; font-size: 12px; } #upload .filelist li p.log { position: relative; top: -45px; } #upload .filelist li p.title { position: absolute; top: 0; left: 0; width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; top: 5px; text-indent: 5px; text-align: left; } #upload .filelist li p.progress { position: absolute; width: 100%; bottom: 0; left: 0; height: 8px; overflow: hidden; z-index: 50; margin: 0; border-radius: 0; background: none; -webkit-box-shadow: 0 0 0; } #upload .filelist li p.progress span { display: none; overflow: hidden; width: 0; height: 100%; background: #1483d8 url(./images/progress.png) repeat-x; -webit-transition: width 200ms linear; -moz-transition: width 200ms linear; -o-transition: width 200ms linear; -ms-transition: width 200ms linear; transition: width 200ms linear; -webkit-animation: progressmove 2s linear infinite; -moz-animation: progressmove 2s linear infinite; -o-animation: progressmove 2s linear infinite; -ms-animation: progressmove 2s linear infinite; animation: progressmove 2s linear infinite; -webkit-transform: translateZ(0); } @-webkit-keyframes progressmove { 0% { background-position: 0 0; } 100% { background-position: 17px 0; } } @-moz-keyframes progressmove { 0% { background-position: 0 0; } 100% { background-position: 17px 0; } } @keyframes progressmove { 0% { background-position: 0 0; } 100% { background-position: 17px 0; } } #upload .filelist li p.imgWrap { position: relative; z-index: 2; line-height: 113px; vertical-align: middle; overflow: hidden; width: 113px; height: 113px; -webkit-transform-origin: 50% 50%; -moz-transform-origin: 50% 50%; -o-transform-origin: 50% 50%; -ms-transform-origin: 50% 50%; transform-origin: 50% 50%; -webit-transition: 200ms ease-out; -moz-transition: 200ms ease-out; -o-transition: 200ms ease-out; -ms-transition: 200ms ease-out; transition: 200ms ease-out; } #upload .filelist li p.imgWrap.notimage { margin-top: 0; width: 111px; height: 111px; border: 1px #eeeeee solid; } #upload .filelist li p.imgWrap.notimage i.file-preview { margin-top: 15px; } #upload .filelist li img { width: 100%; } #upload .filelist li p.error { background: #f43838; color: #fff; position: absolute; bottom: 0; left: 0; height: 28px; line-height: 28px; width: 100%; z-index: 100; display:none; } #upload .filelist li .success { display: block; position: absolute; left: 0; bottom: 0; height: 40px; width: 100%; z-index: 200; background: url(./images/success.png) no-repeat right bottom; background-image: url(./images/success.gif) \9; } #upload .filelist li.filePickerBlock { width: 113px; height: 113px; background: url(./images/image.png) no-repeat center 12px; border: 1px solid #eeeeee; border-radius: 0; } #upload .filelist li.filePickerBlock div.webuploader-pick { width: 100%; height: 100%; margin: 0; padding: 0; opacity: 0; background: none; font-size: 0; } #upload .filelist div.file-panel { position: absolute; height: 0; filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#80000000', endColorstr='#80000000') \0; background: rgba(0, 0, 0, 0.5); width: 100%; top: 0; left: 0; overflow: hidden; z-index: 300; } #upload .filelist div.file-panel span { width: 24px; height: 24px; display: inline; float: right; text-indent: -9999px; overflow: hidden; background: url(./images/icons.png) no-repeat; background: url(./images/icons.gif) no-repeat \9; margin: 5px 1px 1px; cursor: pointer; -webkit-tap-highlight-color: rgba(0,0,0,0); -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } #upload .filelist div.file-panel span.rotateLeft { display:none; background-position: 0 -24px; } #upload .filelist div.file-panel span.rotateLeft:hover { background-position: 0 0; } #upload .filelist div.file-panel span.rotateRight { display:none; background-position: -24px -24px; } #upload .filelist div.file-panel span.rotateRight:hover { background-position: -24px 0; } #upload .filelist div.file-panel span.cancel { background-position: -48px -24px; } #upload .filelist div.file-panel span.cancel:hover { background-position: -48px 0; } #upload .statusBar { height: 45px; border-bottom: 1px solid #dadada; margin: 0 10px; padding: 0; line-height: 45px; vertical-align: middle; position: relative; } #upload .statusBar .progress { border: 1px solid #1483d8; width: 198px; background: #fff; height: 18px; position: absolute; top: 12px; display: none; text-align: center; line-height: 18px; color: #6dbfff; margin: 0 10px 0 0; } #upload .statusBar .progress span.percentage { width: 0; height: 100%; left: 0; top: 0; background: #1483d8; position: absolute; } #upload .statusBar .progress span.text { position: relative; z-index: 10; } #upload .statusBar .info { display: inline-block; font-size: 14px; color: #666666; } #upload .statusBar .btns { position: absolute; top: 7px; right: 0; line-height: 30px; } #filePickerBtn { display: inline-block; float: left; } #upload .statusBar .btns .webuploader-pick, #upload .statusBar .btns .uploadBtn, #upload .statusBar .btns .uploadBtn.state-uploading, #upload .statusBar .btns .uploadBtn.state-paused { background: #ffffff; border: 1px solid #cfcfcf; color: #565656; padding: 0 18px; display: inline-block; border-radius: 3px; margin-left: 10px; cursor: pointer; font-size: 14px; float: left; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } #upload .statusBar .btns .webuploader-pick-hover, #upload .statusBar .btns .uploadBtn:hover, #upload .statusBar .btns .uploadBtn.state-uploading:hover, #upload .statusBar .btns .uploadBtn.state-paused:hover { background: #f0f0f0; } #upload .statusBar .btns .uploadBtn, #upload .statusBar .btns .uploadBtn.state-paused{ background: #00b7ee; color: #fff; border-color: transparent; } #upload .statusBar .btns .uploadBtn:hover, #upload .statusBar .btns .uploadBtn.state-paused:hover{ background: #00a2d4; } #upload .statusBar .btns .uploadBtn.disabled { pointer-events: none; filter:alpha(opacity=60); -moz-opacity:0.6; -khtml-opacity: 0.6; opacity: 0.6; } /* 图片管理样式 */ #online { width: 100%; height: 336px; padding: 10px 0 0 0; } #online #fileList{ width: 100%; height: 100%; overflow-x: hidden; overflow-y: auto; position: relative; } #online ul { display: block; list-style: none; margin: 0; padding: 0; } #online li { float: left; display: block; list-style: none; padding: 0; width: 113px; height: 113px; margin: 0 0 9px 9px; *margin: 0 0 6px 6px; background-color: #eee; overflow: hidden; cursor: pointer; position: relative; } #online li.clearFloat { float: none; clear: both; display: block; width:0; height:0; margin: 0; padding: 0; } #online li img { cursor: pointer; } #online li div.file-wrapper { cursor: pointer; position: absolute; display: block; width: 111px; height: 111px; border: 1px solid #eee; background: url("./images/bg.png") repeat; } #online li div span.file-title{ display: block; padding: 0 3px; margin: 3px 0 0 0; font-size: 12px; height: 13px; color: #555555; text-align: center; width: 107px; white-space: nowrap; word-break: break-all; overflow: hidden; text-overflow: ellipsis; } #online li .icon { cursor: pointer; width: 113px; height: 113px; position: absolute; top: 0; left: 0; z-index: 2; border: 0; background-repeat: no-repeat; } #online li .icon:hover { width: 107px; height: 107px; border: 3px solid #1094fa; } #online li.selected .icon { background-image: url(images/success.png); background-image: url(images/success.gif) \9; background-position: 75px 75px; } #online li.selected .icon:hover { width: 107px; height: 107px; border: 3px solid #1094fa; background-position: 72px 72px; } /* 在线文件的文件预览图标 */ i.file-preview { display: block; margin: 10px auto; width: 70px; height: 70px; background-image: url("./images/file-icons.png"); background-image: url("./images/file-icons.gif") \9; background-position: -140px center; background-repeat: no-repeat; } i.file-preview.file-type-dir{ background-position: 0 center; } i.file-preview.file-type-file{ background-position: -140px center; } i.file-preview.file-type-filelist{ background-position: -210px center; } i.file-preview.file-type-zip, i.file-preview.file-type-rar, i.file-preview.file-type-7z, i.file-preview.file-type-tar, i.file-preview.file-type-gz, i.file-preview.file-type-bz2{ background-position: -280px center; } i.file-preview.file-type-xls, i.file-preview.file-type-xlsx{ background-position: -350px center; } i.file-preview.file-type-doc, i.file-preview.file-type-docx{ background-position: -420px center; } i.file-preview.file-type-ppt, i.file-preview.file-type-pptx{ background-position: -490px center; } i.file-preview.file-type-vsd{ background-position: -560px center; } i.file-preview.file-type-pdf{ background-position: -630px center; } i.file-preview.file-type-txt, i.file-preview.file-type-md, i.file-preview.file-type-json, i.file-preview.file-type-htm, i.file-preview.file-type-xml, i.file-preview.file-type-html, i.file-preview.file-type-js, i.file-preview.file-type-css, i.file-preview.file-type-php, i.file-preview.file-type-jsp, i.file-preview.file-type-asp{ background-position: -700px center; } i.file-preview.file-type-apk{ background-position: -770px center; } i.file-preview.file-type-exe{ background-position: -840px center; } i.file-preview.file-type-ipa{ background-position: -910px center; } i.file-preview.file-type-mp4, i.file-preview.file-type-swf, i.file-preview.file-type-mkv, i.file-preview.file-type-avi, i.file-preview.file-type-flv, i.file-preview.file-type-mov, i.file-preview.file-type-mpg, i.file-preview.file-type-mpeg, i.file-preview.file-type-ogv, i.file-preview.file-type-webm, i.file-preview.file-type-rm, i.file-preview.file-type-rmvb{ background-position: -980px center; } i.file-preview.file-type-ogg, i.file-preview.file-type-wav, i.file-preview.file-type-wmv, i.file-preview.file-type-mid, i.file-preview.file-type-mp3{ background-position: -1050px center; } i.file-preview.file-type-jpg, i.file-preview.file-type-jpeg, i.file-preview.file-type-gif, i.file-preview.file-type-bmp, i.file-preview.file-type-png, i.file-preview.file-type-psd{ background-position: -140px center; } ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/attachment/attachment.html ================================================ ueditor图片对话框
0%
================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/attachment/attachment.js ================================================ /** * User: Jinqn * Date: 14-04-08 * Time: 下午16:34 * 上传图片对话框逻辑代码,包括tab: 远程图片/上传图片/在线图片/搜索图片 */ (function () { var uploadFile, onlineFile; window.onload = function () { initTabs(); initButtons(); }; /* 初始化tab标签 */ function initTabs() { var tabs = $G('tabhead').children; for (var i = 0; i < tabs.length; i++) { domUtils.on(tabs[i], "click", function (e) { var target = e.target || e.srcElement; setTabFocus(target.getAttribute('data-content-id')); }); } setTabFocus('upload'); } /* 初始化tabbody */ function setTabFocus(id) { if(!id) return; var i, bodyId, tabs = $G('tabhead').children; for (i = 0; i < tabs.length; i++) { bodyId = tabs[i].getAttribute('data-content-id') if (bodyId == id) { domUtils.addClass(tabs[i], 'focus'); domUtils.addClass($G(bodyId), 'focus'); } else { domUtils.removeClasses(tabs[i], 'focus'); domUtils.removeClasses($G(bodyId), 'focus'); } } switch (id) { case 'upload': uploadFile = uploadFile || new UploadFile('queueList'); break; case 'online': onlineFile = onlineFile || new OnlineFile('fileList'); break; } } /* 初始化onok事件 */ function initButtons() { dialog.onok = function () { var list = [], id, tabs = $G('tabhead').children; for (var i = 0; i < tabs.length; i++) { if (domUtils.hasClass(tabs[i], 'focus')) { id = tabs[i].getAttribute('data-content-id'); break; } } switch (id) { case 'upload': list = uploadFile.getInsertList(); var count = uploadFile.getQueueCount(); if (count) { $('.info', '#queueList').html('' + '还有2个未上传文件'.replace(/[\d]/, count) + ''); return false; } break; case 'online': list = onlineFile.getInsertList(); break; } editor.execCommand('insertfile', list); }; } /* 上传附件 */ function UploadFile(target) { this.$wrap = target.constructor == String ? $('#' + target) : $(target); this.init(); } UploadFile.prototype = { init: function () { this.fileList = []; this.initContainer(); this.initUploader(); }, initContainer: function () { this.$queue = this.$wrap.find('.filelist'); }, /* 初始化容器 */ initUploader: function () { var _this = this, $ = jQuery, // just in case. Make sure it's not an other libaray. $wrap = _this.$wrap, // 图片容器 $queue = $wrap.find('.filelist'), // 状态栏,包括进度和控制按钮 $statusBar = $wrap.find('.statusBar'), // 文件总体选择信息。 $info = $statusBar.find('.info'), // 上传按钮 $upload = $wrap.find('.uploadBtn'), // 上传按钮 $filePickerBtn = $wrap.find('.filePickerBtn'), // 上传按钮 $filePickerBlock = $wrap.find('.filePickerBlock'), // 没选择文件之前的内容。 $placeHolder = $wrap.find('.placeholder'), // 总体进度条 $progress = $statusBar.find('.progress').hide(), // 添加的文件数量 fileCount = 0, // 添加的文件总大小 fileSize = 0, // 优化retina, 在retina下这个值是2 ratio = window.devicePixelRatio || 1, // 缩略图大小 thumbnailWidth = 113 * ratio, thumbnailHeight = 113 * ratio, // 可能有pedding, ready, uploading, confirm, done. state = '', // 所有文件的进度信息,key为file id percentages = {}, supportTransition = (function () { var s = document.createElement('p').style, r = 'transition' in s || 'WebkitTransition' in s || 'MozTransition' in s || 'msTransition' in s || 'OTransition' in s; s = null; return r; })(), // WebUploader实例 uploader, actionUrl = editor.getActionUrl(editor.getOpt('fileActionName')), fileMaxSize = editor.getOpt('fileMaxSize'), acceptExtensions = (editor.getOpt('fileAllowFiles') || []).join('').replace(/\./g, ',').replace(/^[,]/, '');; if (!WebUploader.Uploader.support()) { $('#filePickerReady').after($('
').html(lang.errorNotSupport)).hide(); return; } else if (!editor.getOpt('fileActionName')) { $('#filePickerReady').after($('
').html(lang.errorLoadConfig)).hide(); return; } uploader = _this.uploader = WebUploader.create({ pick: { id: '#filePickerReady', label: lang.uploadSelectFile }, swf: '../../third-party/webuploader/Uploader.swf', server: actionUrl, fileVal: editor.getOpt('fileFieldName'), duplicate: true, fileSingleSizeLimit: fileMaxSize, compress: false }); uploader.addButton({ id: '#filePickerBlock' }); uploader.addButton({ id: '#filePickerBtn', label: lang.uploadAddFile }); setState('pedding'); // 当有文件添加进来时执行,负责view的创建 function addFile(file) { var $li = $('
  • ' + '

    ' + file.name + '

    ' + '

    ' + '

    ' + '
  • '), $btns = $('
    ' + '' + lang.uploadDelete + '' + '' + lang.uploadTurnRight + '' + '' + lang.uploadTurnLeft + '
    ').appendTo($li), $prgress = $li.find('p.progress span'), $wrap = $li.find('p.imgWrap'), $info = $('

    ').hide().appendTo($li), showError = function (code) { switch (code) { case 'exceed_size': text = lang.errorExceedSize; break; case 'interrupt': text = lang.errorInterrupt; break; case 'http': text = lang.errorHttp; break; case 'not_allow_type': text = lang.errorFileType; break; default: text = lang.errorUploadRetry; break; } $info.text(text).show(); }; if (file.getStatus() === 'invalid') { showError(file.statusText); } else { $wrap.text(lang.uploadPreview); if ('|png|jpg|jpeg|bmp|gif|'.indexOf('|'+file.ext.toLowerCase()+'|') == -1) { $wrap.empty().addClass('notimage').append('' + '' + file.name + ''); } else { if (browser.ie && browser.version <= 7) { $wrap.text(lang.uploadNoPreview); } else { uploader.makeThumb(file, function (error, src) { if (error || !src) { $wrap.text(lang.uploadNoPreview); } else { var $img = $(''); $wrap.empty().append($img); $img.on('error', function () { $wrap.text(lang.uploadNoPreview); }); } }, thumbnailWidth, thumbnailHeight); } } percentages[ file.id ] = [ file.size, 0 ]; file.rotation = 0; /* 检查文件格式 */ if (!file.ext || acceptExtensions.indexOf(file.ext.toLowerCase()) == -1) { showError('not_allow_type'); uploader.removeFile(file); } } file.on('statuschange', function (cur, prev) { if (prev === 'progress') { $prgress.hide().width(0); } else if (prev === 'queued') { $li.off('mouseenter mouseleave'); $btns.remove(); } // 成功 if (cur === 'error' || cur === 'invalid') { showError(file.statusText); percentages[ file.id ][ 1 ] = 1; } else if (cur === 'interrupt') { showError('interrupt'); } else if (cur === 'queued') { percentages[ file.id ][ 1 ] = 0; } else if (cur === 'progress') { $info.hide(); $prgress.css('display', 'block'); } else if (cur === 'complete') { } $li.removeClass('state-' + prev).addClass('state-' + cur); }); $li.on('mouseenter', function () { $btns.stop().animate({height: 30}); }); $li.on('mouseleave', function () { $btns.stop().animate({height: 0}); }); $btns.on('click', 'span', function () { var index = $(this).index(), deg; switch (index) { case 0: uploader.removeFile(file); return; case 1: file.rotation += 90; break; case 2: file.rotation -= 90; break; } if (supportTransition) { deg = 'rotate(' + file.rotation + 'deg)'; $wrap.css({ '-webkit-transform': deg, '-mos-transform': deg, '-o-transform': deg, 'transform': deg }); } else { $wrap.css('filter', 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' + (~~((file.rotation / 90) % 4 + 4) % 4) + ')'); } }); $li.insertBefore($filePickerBlock); } // 负责view的销毁 function removeFile(file) { var $li = $('#' + file.id); delete percentages[ file.id ]; updateTotalProgress(); $li.off().find('.file-panel').off().end().remove(); } function updateTotalProgress() { var loaded = 0, total = 0, spans = $progress.children(), percent; $.each(percentages, function (k, v) { total += v[ 0 ]; loaded += v[ 0 ] * v[ 1 ]; }); percent = total ? loaded / total : 0; spans.eq(0).text(Math.round(percent * 100) + '%'); spans.eq(1).css('width', Math.round(percent * 100) + '%'); updateStatus(); } function setState(val, files) { if (val != state) { var stats = uploader.getStats(); $upload.removeClass('state-' + state); $upload.addClass('state-' + val); switch (val) { /* 未选择文件 */ case 'pedding': $queue.addClass('element-invisible'); $statusBar.addClass('element-invisible'); $placeHolder.removeClass('element-invisible'); $progress.hide(); $info.hide(); uploader.refresh(); break; /* 可以开始上传 */ case 'ready': $placeHolder.addClass('element-invisible'); $queue.removeClass('element-invisible'); $statusBar.removeClass('element-invisible'); $progress.hide(); $info.show(); $upload.text(lang.uploadStart); uploader.refresh(); break; /* 上传中 */ case 'uploading': $progress.show(); $info.hide(); $upload.text(lang.uploadPause); break; /* 暂停上传 */ case 'paused': $progress.show(); $info.hide(); $upload.text(lang.uploadContinue); break; case 'confirm': $progress.show(); $info.hide(); $upload.text(lang.uploadStart); stats = uploader.getStats(); if (stats.successNum && !stats.uploadFailNum) { setState('finish'); return; } break; case 'finish': $progress.hide(); $info.show(); if (stats.uploadFailNum) { $upload.text(lang.uploadRetry); } else { $upload.text(lang.uploadStart); } break; } state = val; updateStatus(); } if (!_this.getQueueCount()) { $upload.addClass('disabled') } else { $upload.removeClass('disabled') } } function updateStatus() { var text = '', stats; if (state === 'ready') { text = lang.updateStatusReady.replace('_', fileCount).replace('_KB', WebUploader.formatSize(fileSize)); } else if (state === 'confirm') { stats = uploader.getStats(); if (stats.uploadFailNum) { text = lang.updateStatusConfirm.replace('_', stats.successNum).replace('_', stats.successNum); } } else { stats = uploader.getStats(); text = lang.updateStatusFinish.replace('_', fileCount). replace('_KB', WebUploader.formatSize(fileSize)). replace('_', stats.successNum); if (stats.uploadFailNum) { text += lang.updateStatusError.replace('_', stats.uploadFailNum); } } $info.html(text); } uploader.on('fileQueued', function (file) { fileCount++; fileSize += file.size; if (fileCount === 1) { $placeHolder.addClass('element-invisible'); $statusBar.show(); } addFile(file); }); uploader.on('fileDequeued', function (file) { fileCount--; fileSize -= file.size; removeFile(file); updateTotalProgress(); }); uploader.on('filesQueued', function (file) { if (!uploader.isInProgress() && (state == 'pedding' || state == 'finish' || state == 'confirm' || state == 'ready')) { setState('ready'); } updateTotalProgress(); }); uploader.on('all', function (type, files) { switch (type) { case 'uploadFinished': setState('confirm', files); break; case 'startUpload': /* 添加额外的GET参数 */ var params = utils.serializeParam(editor.queryCommandValue('serverparam')) || '', url = utils.formatUrl(actionUrl + (actionUrl.indexOf('?') == -1 ? '?':'&') + 'encode=utf-8&' + params); uploader.option('server', url); setState('uploading', files); break; case 'stopUpload': setState('paused', files); break; } }); uploader.on('uploadBeforeSend', function (file, data, header) { //这里可以通过data对象添加POST参数 header['X_Requested_With'] = 'XMLHttpRequest'; // HaoChuan9421 if(editor.options.headers && Object.prototype.toString.apply(editor.options.headers) === "[object Object]"){ for(var key in editor.options.headers){ header[key] = editor.options.headers[key] } } }); uploader.on('uploadProgress', function (file, percentage) { var $li = $('#' + file.id), $percent = $li.find('.progress span'); $percent.css('width', percentage * 100 + '%'); percentages[ file.id ][ 1 ] = percentage; updateTotalProgress(); }); uploader.on('uploadSuccess', function (file, ret) { var $file = $('#' + file.id); try { var responseText = (ret._raw || ret), json = utils.str2json(responseText); if (json.state == 'SUCCESS') { _this.fileList.push(json); $file.append(''); } else { $file.find('.error').text(json.state).show(); } } catch (e) { $file.find('.error').text(lang.errorServerUpload).show(); } }); uploader.on('uploadError', function (file, code) { }); uploader.on('error', function (code, file) { if (code == 'Q_TYPE_DENIED' || code == 'F_EXCEED_SIZE') { addFile(file); } }); uploader.on('uploadComplete', function (file, ret) { }); $upload.on('click', function () { if ($(this).hasClass('disabled')) { return false; } if (state === 'ready') { uploader.upload(); } else if (state === 'paused') { uploader.upload(); } else if (state === 'uploading') { uploader.stop(); } }); $upload.addClass('state-' + state); updateTotalProgress(); }, getQueueCount: function () { var file, i, status, readyFile = 0, files = this.uploader.getFiles(); for (i = 0; file = files[i++]; ) { status = file.getStatus(); if (status == 'queued' || status == 'uploading' || status == 'progress') readyFile++; } return readyFile; }, getInsertList: function () { var i, link, data, list = [], prefix = editor.getOpt('fileUrlPrefix'); for (i = 0; i < this.fileList.length; i++) { data = this.fileList[i]; link = data.url; list.push({ title: data.original || link.substr(link.lastIndexOf('/') + 1), url: prefix + link }); } return list; } }; /* 在线附件 */ function OnlineFile(target) { this.container = utils.isString(target) ? document.getElementById(target) : target; this.init(); } OnlineFile.prototype = { init: function () { this.initContainer(); this.initEvents(); this.initData(); }, /* 初始化容器 */ initContainer: function () { this.container.innerHTML = ''; this.list = document.createElement('ul'); this.clearFloat = document.createElement('li'); domUtils.addClass(this.list, 'list'); domUtils.addClass(this.clearFloat, 'clearFloat'); this.list.appendChild(this.clearFloat); this.container.appendChild(this.list); }, /* 初始化滚动事件,滚动到地步自动拉取数据 */ initEvents: function () { var _this = this; /* 滚动拉取图片 */ domUtils.on($G('fileList'), 'scroll', function(e){ var panel = this; if (panel.scrollHeight - (panel.offsetHeight + panel.scrollTop) < 10) { _this.getFileData(); } }); /* 选中图片 */ domUtils.on(this.list, 'click', function (e) { var target = e.target || e.srcElement, li = target.parentNode; if (li.tagName.toLowerCase() == 'li') { if (domUtils.hasClass(li, 'selected')) { domUtils.removeClasses(li, 'selected'); } else { domUtils.addClass(li, 'selected'); } } }); }, /* 初始化第一次的数据 */ initData: function () { /* 拉取数据需要使用的值 */ this.state = 0; this.listSize = editor.getOpt('fileManagerListSize'); this.listIndex = 0; this.listEnd = false; /* 第一次拉取数据 */ this.getFileData(); }, /* 向后台拉取图片列表数据 */ getFileData: function () { var _this = this; if(!_this.listEnd && !this.isLoadingData) { this.isLoadingData = true; ajax.request(editor.getActionUrl(editor.getOpt('fileManagerActionName')), { timeout: 100000, data: utils.extend({ start: this.listIndex, size: this.listSize }, editor.queryCommandValue('serverparam')), method: 'get', onsuccess: function (r) { try { var json = eval('(' + r.responseText + ')'); if (json.state == 'SUCCESS') { _this.pushData(json.list); _this.listIndex = parseInt(json.start) + parseInt(json.list.length); if(_this.listIndex >= json.total) { _this.listEnd = true; } _this.isLoadingData = false; } } catch (e) { if(r.responseText.indexOf('ue_separate_ue') != -1) { var list = r.responseText.split(r.responseText); _this.pushData(list); _this.listIndex = parseInt(list.length); _this.listEnd = true; _this.isLoadingData = false; } } }, onerror: function () { _this.isLoadingData = false; } }); } }, /* 添加图片到列表界面上 */ pushData: function (list) { var i, item, img, filetype, preview, icon, _this = this, urlPrefix = editor.getOpt('fileManagerUrlPrefix'); for (i = 0; i < list.length; i++) { if(list[i] && list[i].url) { item = document.createElement('li'); icon = document.createElement('span'); filetype = list[i].url.substr(list[i].url.lastIndexOf('.') + 1); if ( "png|jpg|jpeg|gif|bmp".indexOf(filetype) != -1 ) { preview = document.createElement('img'); domUtils.on(preview, 'load', (function(image){ return function(){ _this.scale(image, image.parentNode.offsetWidth, image.parentNode.offsetHeight); }; })(preview)); preview.width = 113; preview.setAttribute('src', urlPrefix + list[i].url + (list[i].url.indexOf('?') == -1 ? '?noCache=':'&noCache=') + (+new Date()).toString(36) ); } else { var ic = document.createElement('i'), textSpan = document.createElement('span'); textSpan.innerHTML = list[i].url.substr(list[i].url.lastIndexOf('/') + 1); preview = document.createElement('div'); preview.appendChild(ic); preview.appendChild(textSpan); domUtils.addClass(preview, 'file-wrapper'); domUtils.addClass(textSpan, 'file-title'); domUtils.addClass(ic, 'file-type-' + filetype); domUtils.addClass(ic, 'file-preview'); } domUtils.addClass(icon, 'icon'); item.setAttribute('data-url', urlPrefix + list[i].url); if (list[i].original) { item.setAttribute('data-title', list[i].original); } item.appendChild(preview); item.appendChild(icon); this.list.insertBefore(item, this.clearFloat); } } }, /* 改变图片大小 */ scale: function (img, w, h, type) { var ow = img.width, oh = img.height; if (type == 'justify') { if (ow >= oh) { img.width = w; img.height = h * oh / ow; img.style.marginLeft = '-' + parseInt((img.width - w) / 2) + 'px'; } else { img.width = w * ow / oh; img.height = h; img.style.marginTop = '-' + parseInt((img.height - h) / 2) + 'px'; } } else { if (ow >= oh) { img.width = w * ow / oh; img.height = h; img.style.marginLeft = '-' + parseInt((img.width - w) / 2) + 'px'; } else { img.width = w; img.height = h * oh / ow; img.style.marginTop = '-' + parseInt((img.height - h) / 2) + 'px'; } } }, getInsertList: function () { var i, lis = this.list.children, list = []; for (i = 0; i < lis.length; i++) { if (domUtils.hasClass(lis[i], 'selected')) { var url = lis[i].getAttribute('data-url'); var title = lis[i].getAttribute('data-title') || url.substr(url.lastIndexOf('/') + 1); list.push({ title: title, url: url }); } } return list; } }; })(); ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/background/background.css ================================================ .wrapper{ width: 424px;margin: 10px auto; zoom:1;position: relative} .tabbody{height:225px;} .tabbody .panel { position: absolute;width:100%; height:100%;background: #fff; display: none;} .tabbody .focus { display: block;} body{font-size: 12px;color: #888;overflow: hidden;} input,label{vertical-align:middle} .clear{clear: both;} .pl{padding-left: 18px;padding-left: 23px\9;} #imageList {width: 420px;height: 215px;margin-top: 10px;overflow: hidden;overflow-y: auto;} #imageList div {float: left;width: 100px;height: 95px;margin: 5px 10px;} #imageList img {cursor: pointer;border: 2px solid white;} .bgarea{margin: 10px;padding: 5px;height: 84%;border: 1px solid #A8A297;} .content div{margin: 10px 0 10px 5px;} .content .iptradio{margin: 0px 5px 5px 0px;} .txt{width:280px;} .wrapcolor{height: 19px;} div.color{float: left;margin: 0;} #colorPicker{width: 17px;height: 17px;border: 1px solid #CCC;display: inline-block;border-radius: 3px;box-shadow: 2px 2px 5px #D3D6DA;margin: 0;float: left;} div.alignment,#custom{margin-left: 23px;margin-left: 28px\9;} #custom input{height: 15px;min-height: 15px;width:20px;} #repeatType{width:100px;} /* 图片管理样式 */ #imgManager { width: 100%; height: 225px; } #imgManager #imageList{ width: 100%; overflow-x: hidden; overflow-y: auto; } #imgManager ul { display: block; list-style: none; margin: 0; padding: 0; } #imgManager li { float: left; display: block; list-style: none; padding: 0; width: 113px; height: 113px; margin: 9px 0 0 19px; background-color: #eee; overflow: hidden; cursor: pointer; position: relative; } #imgManager li.clearFloat { float: none; clear: both; display: block; width:0; height:0; margin: 0; padding: 0; } #imgManager li img { cursor: pointer; } #imgManager li .icon { cursor: pointer; width: 113px; height: 113px; position: absolute; top: 0; left: 0; z-index: 2; border: 0; background-repeat: no-repeat; } #imgManager li .icon:hover { width: 107px; height: 107px; border: 3px solid #1094fa; } #imgManager li.selected .icon { background-image: url(images/success.png); background-position: 75px 75px; } #imgManager li.selected .icon:hover { width: 107px; height: 107px; border: 3px solid #1094fa; background-position: 72px 72px; } ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/background/background.html ================================================
    :
    :
    :x:px  y:px
    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/background/background.js ================================================ (function () { var onlineImage, backupStyle = editor.queryCommandValue('background'); window.onload = function () { initTabs(); initColorSelector(); }; /* 初始化tab标签 */ function initTabs(){ var tabs = $G('tabHeads').children; for (var i = 0; i < tabs.length; i++) { domUtils.on(tabs[i], "click", function (e) { var target = e.target || e.srcElement; for (var j = 0; j < tabs.length; j++) { if(tabs[j] == target){ tabs[j].className = "focus"; var contentId = tabs[j].getAttribute('data-content-id'); $G(contentId).style.display = "block"; if(contentId == 'imgManager') { initImagePanel(); } }else { tabs[j].className = ""; $G(tabs[j].getAttribute('data-content-id')).style.display = "none"; } } }); } } /* 初始化颜色设置 */ function initColorSelector () { var obj = editor.queryCommandValue('background'); if (obj) { var color = obj['background-color'], repeat = obj['background-repeat'] || 'repeat', image = obj['background-image'] || '', position = obj['background-position'] || 'center center', pos = position.split(' '), x = parseInt(pos[0]) || 0, y = parseInt(pos[1]) || 0; if(repeat == 'no-repeat' && (x || y)) repeat = 'self'; image = image.match(/url[\s]*\(([^\)]*)\)/); image = image ? image[1]:''; updateFormState('colored', color, image, repeat, x, y); } else { updateFormState(); } var updateHandler = function () { updateFormState(); updateBackground(); } domUtils.on($G('nocolorRadio'), 'click', updateBackground); domUtils.on($G('coloredRadio'), 'click', updateHandler); domUtils.on($G('url'), 'keyup', function(){ if($G('url').value && $G('alignment').style.display == "none") { utils.each($G('repeatType').children, function(item){ item.selected = ('repeat' == item.getAttribute('value') ? 'selected':false); }); } updateHandler(); }); domUtils.on($G('repeatType'), 'change', updateHandler); domUtils.on($G('x'), 'keyup', updateBackground); domUtils.on($G('y'), 'keyup', updateBackground); initColorPicker(); } /* 初始化颜色选择器 */ function initColorPicker() { var me = editor, cp = $G("colorPicker"); /* 生成颜色选择器ui对象 */ var popup = new UE.ui.Popup({ content: new UE.ui.ColorPicker({ noColorText: me.getLang("clearColor"), editor: me, onpickcolor: function (t, color) { updateFormState('colored', color); updateBackground(); UE.ui.Popup.postHide(); }, onpicknocolor: function (t, color) { updateFormState('colored', 'transparent'); updateBackground(); UE.ui.Popup.postHide(); } }), editor: me, onhide: function () { } }); /* 设置颜色选择器 */ domUtils.on(cp, "click", function () { popup.showAnchor(this); }); domUtils.on(document, 'mousedown', function (evt) { var el = evt.target || evt.srcElement; UE.ui.Popup.postHide(el); }); domUtils.on(window, 'scroll', function () { UE.ui.Popup.postHide(); }); } /* 初始化在线图片列表 */ function initImagePanel() { onlineImage = onlineImage || new OnlineImage('imageList'); } /* 更新背景色设置面板 */ function updateFormState (radio, color, url, align, x, y) { var nocolorRadio = $G('nocolorRadio'), coloredRadio = $G('coloredRadio'); if(radio) { nocolorRadio.checked = (radio == 'colored' ? false:'checked'); coloredRadio.checked = (radio == 'colored' ? 'checked':false); } if(color) { domUtils.setStyle($G("colorPicker"), "background-color", color); } if(url && /^\//.test(url)) { var a = document.createElement('a'); a.href = url; browser.ie && (a.href = a.href); url = browser.ie ? a.href:(a.protocol + '//' + a.host + a.pathname + a.search + a.hash); } if(url || url === '') { $G('url').value = url; } if(align) { utils.each($G('repeatType').children, function(item){ item.selected = (align == item.getAttribute('value') ? 'selected':false); }); } if(x || y) { $G('x').value = parseInt(x) || 0; $G('y').value = parseInt(y) || 0; } $G('alignment').style.display = coloredRadio.checked && $G('url').value ? '':'none'; $G('custom').style.display = coloredRadio.checked && $G('url').value && $G('repeatType').value == 'self' ? '':'none'; } /* 更新背景颜色 */ function updateBackground () { if ($G('coloredRadio').checked) { var color = domUtils.getStyle($G("colorPicker"), "background-color"), bgimg = $G("url").value, align = $G("repeatType").value, backgroundObj = { "background-repeat": "no-repeat", "background-position": "center center" }; if (color) backgroundObj["background-color"] = color; if (bgimg) backgroundObj["background-image"] = 'url(' + bgimg + ')'; if (align == 'self') { backgroundObj["background-position"] = $G("x").value + "px " + $G("y").value + "px"; } else if (align == 'repeat-x' || align == 'repeat-y' || align == 'repeat') { backgroundObj["background-repeat"] = align; } editor.execCommand('background', backgroundObj); } else { editor.execCommand('background', null); } } /* 在线图片 */ function OnlineImage(target) { this.container = utils.isString(target) ? document.getElementById(target) : target; this.init(); } OnlineImage.prototype = { init: function () { this.reset(); this.initEvents(); }, /* 初始化容器 */ initContainer: function () { this.container.innerHTML = ''; this.list = document.createElement('ul'); this.clearFloat = document.createElement('li'); domUtils.addClass(this.list, 'list'); domUtils.addClass(this.clearFloat, 'clearFloat'); this.list.id = 'imageListUl'; this.list.appendChild(this.clearFloat); this.container.appendChild(this.list); }, /* 初始化滚动事件,滚动到地步自动拉取数据 */ initEvents: function () { var _this = this; /* 滚动拉取图片 */ domUtils.on($G('imageList'), 'scroll', function(e){ var panel = this; if (panel.scrollHeight - (panel.offsetHeight + panel.scrollTop) < 10) { _this.getImageData(); } }); /* 选中图片 */ domUtils.on(this.container, 'click', function (e) { var target = e.target || e.srcElement, li = target.parentNode, nodes = $G('imageListUl').childNodes; if (li.tagName.toLowerCase() == 'li') { updateFormState('nocolor', null, ''); for (var i = 0, node; node = nodes[i++];) { if (node == li && !domUtils.hasClass(node, 'selected')) { domUtils.addClass(node, 'selected'); updateFormState('colored', null, li.firstChild.getAttribute("_src"), 'repeat'); } else { domUtils.removeClasses(node, 'selected'); } } updateBackground(); } }); }, /* 初始化第一次的数据 */ initData: function () { /* 拉取数据需要使用的值 */ this.state = 0; this.listSize = editor.getOpt('imageManagerListSize'); this.listIndex = 0; this.listEnd = false; /* 第一次拉取数据 */ this.getImageData(); }, /* 重置界面 */ reset: function() { this.initContainer(); this.initData(); }, /* 向后台拉取图片列表数据 */ getImageData: function () { var _this = this; if(!_this.listEnd && !this.isLoadingData) { this.isLoadingData = true; var url = editor.getActionUrl(editor.getOpt('imageManagerActionName')), isJsonp = utils.isCrossDomainUrl(url); ajax.request(url, { 'timeout': 100000, 'dataType': isJsonp ? 'jsonp':'', 'data': utils.extend({ start: this.listIndex, size: this.listSize }, editor.queryCommandValue('serverparam')), 'method': 'get', 'onsuccess': function (r) { try { var json = isJsonp ? r:eval('(' + r.responseText + ')'); if (json.state == 'SUCCESS') { _this.pushData(json.list); _this.listIndex = parseInt(json.start) + parseInt(json.list.length); if(_this.listIndex >= json.total) { _this.listEnd = true; } _this.isLoadingData = false; } } catch (e) { if(r.responseText.indexOf('ue_separate_ue') != -1) { var list = r.responseText.split(r.responseText); _this.pushData(list); _this.listIndex = parseInt(list.length); _this.listEnd = true; _this.isLoadingData = false; } } }, 'onerror': function () { _this.isLoadingData = false; } }); } }, /* 添加图片到列表界面上 */ pushData: function (list) { var i, item, img, icon, _this = this, urlPrefix = editor.getOpt('imageManagerUrlPrefix'); for (i = 0; i < list.length; i++) { if(list[i] && list[i].url) { item = document.createElement('li'); img = document.createElement('img'); icon = document.createElement('span'); domUtils.on(img, 'load', (function(image){ return function(){ _this.scale(image, image.parentNode.offsetWidth, image.parentNode.offsetHeight); } })(img)); img.width = 113; img.setAttribute('src', urlPrefix + list[i].url + (list[i].url.indexOf('?') == -1 ? '?noCache=':'&noCache=') + (+new Date()).toString(36) ); img.setAttribute('_src', urlPrefix + list[i].url); domUtils.addClass(icon, 'icon'); item.appendChild(img); item.appendChild(icon); this.list.insertBefore(item, this.clearFloat); } } }, /* 改变图片大小 */ scale: function (img, w, h, type) { var ow = img.width, oh = img.height; if (type == 'justify') { if (ow >= oh) { img.width = w; img.height = h * oh / ow; img.style.marginLeft = '-' + parseInt((img.width - w) / 2) + 'px'; } else { img.width = w * ow / oh; img.height = h; img.style.marginTop = '-' + parseInt((img.height - h) / 2) + 'px'; } } else { if (ow >= oh) { img.width = w * ow / oh; img.height = h; img.style.marginLeft = '-' + parseInt((img.width - w) / 2) + 'px'; } else { img.width = w; img.height = h * oh / ow; img.style.marginTop = '-' + parseInt((img.height - h) / 2) + 'px'; } } }, getInsertList: function () { var i, lis = this.list.children, list = [], align = getAlign(); for (i = 0; i < lis.length; i++) { if (domUtils.hasClass(lis[i], 'selected')) { var img = lis[i].firstChild, src = img.getAttribute('_src'); list.push({ src: src, _src: src, floatStyle: align }); } } return list; } }; dialog.onok = function () { updateBackground(); editor.fireEvent('saveScene'); }; dialog.oncancel = function () { editor.execCommand('background', backupStyle); }; })(); ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/charts/chart.config.js ================================================ /* * 图表配置文件 * */ //不同类型的配置 var typeConfig = [ { chart: { type: 'line' }, plotOptions: { line: { dataLabels: { enabled: false }, enableMouseTracking: true } } }, { chart: { type: 'line' }, plotOptions: { line: { dataLabels: { enabled: true }, enableMouseTracking: false } } }, { chart: { type: 'area' } }, { chart: { type: 'bar' } }, { chart: { type: 'column' } }, { chart: { plotBackgroundColor: null, plotBorderWidth: null, plotShadow: false }, plotOptions: { pie: { allowPointSelect: true, cursor: 'pointer', dataLabels: { enabled: true, color: '#000000', connectorColor: '#000000', formatter: function() { return ''+ this.point.name +': '+ ( Math.round( this.point.percentage*100 ) / 100 ) +' %'; } } } } } ]; ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/charts/charts.css ================================================ html, body { width: 100%; height: 100%; margin: 0; padding: 0; overflow-x: hidden; } .main { width: 100%; overflow: hidden; } .table-view { height: 100%; float: left; margin: 20px; width: 40%; } .table-view .table-container { width: 100%; margin-bottom: 50px; overflow: scroll; } .table-view th { padding: 5px 10px; background-color: #F7F7F7; } .table-view td { width: 50px; text-align: center; padding:0; } .table-container input { width: 40px; padding: 5px; border: none; outline: none; } .table-view caption { font-size: 18px; text-align: left; } .charts-view { /*margin-left: 49%!important;*/ width: 50%; margin-left: 49%; height: 400px; } .charts-container { border-left: 1px solid #c3c3c3; } .charts-format fieldset { padding-left: 20px; margin-bottom: 50px; } .charts-format legend { padding-left: 10px; padding-right: 10px; } .format-item-container { padding: 20px; } .format-item-container label { display: block; margin: 10px 0; } .charts-format .data-item { border: 1px solid black; outline: none; padding: 2px 3px; } /* 图表类型 */ .charts-type { margin-top: 50px; height: 300px; } .scroll-view { border: 1px solid #c3c3c3; border-left: none; border-right: none; overflow: hidden; } .scroll-container { margin: 20px; width: 100%; overflow: hidden; } .scroll-bed { width: 10000px; _margin-top: 20px; -webkit-transition: margin-left .5s ease; -moz-transition: margin-left .5s ease; transition: margin-left .5s ease; } .view-box { display: inline-block; *display: inline; *zoom: 1; margin-right: 20px; border: 2px solid white; line-height: 0; overflow: hidden; cursor: pointer; } .view-box img { border: 1px solid #cecece; } .view-box.selected { border-color: #7274A7; } .button-container { margin-bottom: 20px; text-align: center; } .button-container a { display: inline-block; width: 100px; height: 25px; line-height: 25px; border: 1px solid #c2ccd1; margin-right: 30px; text-decoration: none; color: black; -webkit-border-radius: 2px; -moz-border-radius: 2px; border-radius: 2px; } .button-container a:HOVER { background: #fcfcfc; } .button-container a:ACTIVE { border-top-color: #c2ccd1; box-shadow:inset 0 5px 4px -4px rgba(49, 49, 64, 0.1); } .edui-charts-not-data { height: 100px; line-height: 100px; text-align: center; } ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/charts/charts.html ================================================ chart


    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/charts/charts.js ================================================ /* * 图片转换对话框脚本 **/ var tableData = [], //编辑器页面table editorTable = null, chartsConfig = window.typeConfig, resizeTimer = null, //初始默认图表类型 currentChartType = 0; window.onload = function () { editorTable = domUtils.findParentByTagName( editor.selection.getRange().startContainer, 'table', true); //未找到表格, 显示错误页面 if ( !editorTable ) { document.body.innerHTML = "
    未找到数据
    "; return; } //初始化图表类型选择 initChartsTypeView(); renderTable( editorTable ); initEvent(); initUserConfig( editorTable.getAttribute( "data-chart" ) ); $( "#scrollBed .view-box:eq("+ currentChartType +")" ).trigger( "click" ); updateViewType( currentChartType ); dialog.addListener( "resize", function () { if ( resizeTimer != null ) { window.clearTimeout( resizeTimer ); } resizeTimer = window.setTimeout( function () { resizeTimer = null; renderCharts(); }, 500 ); } ); }; function initChartsTypeView () { var contents = []; for ( var i = 0, len = chartsConfig.length; i
    ' ); } $( "#scrollBed" ).html( contents.join( "" ) ); } //渲染table, 以便用户修改数据 function renderTable ( table ) { var tableHtml = []; //构造数据 for ( var i = 0, row; row = table.rows[ i ]; i++ ) { tableData[ i ] = []; tableHtml[ i ] = []; for ( var j = 0, cell; cell = row.cells[ j ]; j++ ) { var value = getCellValue( cell ); if ( i > 0 && j > 0 ) { value = +value; } if ( i === 0 || j === 0 ) { tableHtml[ i ].push( '

    ' ); } else { tableHtml[ i ].push( '' ); } tableData[ i ][ j ] = value; } tableHtml[ i ] = tableHtml[ i ].join( "" ); } //draw 表格 $( "#tableContainer" ).html( '
    '+ value +'
    '+ tableHtml.join( "" ) +'
    ' ); } /* * 根据表格已有的图表属性初始化当前图表属性 */ function initUserConfig ( config ) { var parsedConfig = {}; if ( !config ) { return; } config = config.split( ";" ); $.each( config, function ( index, item ) { item = item.split( ":" ); parsedConfig[ item[ 0 ] ] = item[ 1 ]; } ); setUserConfig( parsedConfig ); } function initEvent () { var cacheValue = null, //图表类型数 typeViewCount = chartsConfig.length- 1, $chartsTypeViewBox = $( '#scrollBed .view-box' ); $( ".charts-format" ).delegate( ".format-ctrl", "change", function () { renderCharts(); } ) $( ".table-view" ).delegate( ".data-item", "focus", function () { cacheValue = this.value; } ).delegate( ".data-item", "blur", function () { if ( this.value !== cacheValue ) { renderCharts(); } cacheValue = null; } ); $( "#buttonContainer" ).delegate( "a", "click", function (e) { e.preventDefault(); if ( this.getAttribute( "data-title" ) === 'prev' ) { if ( currentChartType > 0 ) { currentChartType--; updateViewType( currentChartType ); } } else { if ( currentChartType < typeViewCount ) { currentChartType++; updateViewType( currentChartType ); } } } ); //图表类型变化 $( '#scrollBed' ).delegate( ".view-box", "click", function (e) { var index = $( this ).attr( "data-chart-type" ); $chartsTypeViewBox.removeClass( "selected" ); $( $chartsTypeViewBox[ index ] ).addClass( "selected" ); currentChartType = index | 0; //饼图, 禁用部分配置 if ( currentChartType === chartsConfig.length - 1 ) { disableNotPieConfig(); //启用完整配置 } else { enableNotPieConfig(); } renderCharts(); } ); } function renderCharts () { var data = collectData(); $('#chartsContainer').highcharts( $.extend( {}, chartsConfig[ currentChartType ], { credits: { enabled: false }, exporting: { enabled: false }, title: { text: data.title, x: -20 //center }, subtitle: { text: data.subTitle, x: -20 }, xAxis: { title: { text: data.xTitle }, categories: data.categories }, yAxis: { title: { text: data.yTitle }, plotLines: [{ value: 0, width: 1, color: '#808080' }] }, tooltip: { enabled: true, valueSuffix: data.suffix }, legend: { layout: 'vertical', align: 'right', verticalAlign: 'middle', borderWidth: 1 }, series: data.series } )); } function updateViewType ( index ) { $( "#scrollBed" ).css( 'marginLeft', -index*324+'px' ); } function collectData () { var form = document.forms[ 'data-form' ], data = null; if ( currentChartType !== chartsConfig.length - 1 ) { data = getSeriesAndCategories(); $.extend( data, getUserConfig() ); //饼图数据格式 } else { data = getSeriesForPieChart(); data.title = form[ 'title' ].value; data.suffix = form[ 'unit' ].value; } return data; } /** * 获取用户配置信息 */ function getUserConfig () { var form = document.forms[ 'data-form' ], info = { title: form[ 'title' ].value, subTitle: form[ 'sub-title' ].value, xTitle: form[ 'x-title' ].value, yTitle: form[ 'y-title' ].value, suffix: form[ 'unit' ].value, //数据对齐方式 tableDataFormat: getTableDataFormat (), //饼图提示文字 tip: $( "#tipInput" ).val() }; return info; } function setUserConfig ( config ) { var form = document.forms[ 'data-form' ]; config.title && ( form[ 'title' ].value = config.title ); config.subTitle && ( form[ 'sub-title' ].value = config.subTitle ); config.xTitle && ( form[ 'x-title' ].value = config.xTitle ); config.yTitle && ( form[ 'y-title' ].value = config.yTitle ); config.suffix && ( form[ 'unit' ].value = config.suffix ); config.dataFormat == "-1" && ( form[ 'charts-format' ][ 1 ].checked = true ); config.tip && ( form[ 'tip' ].value = config.tip ); currentChartType = config.chartType || 0; } function getSeriesAndCategories () { var form = document.forms[ 'data-form' ], series = [], categories = [], tmp = [], tableData = getTableData(); //反转数据 if ( getTableDataFormat() === "-1" ) { for ( var i = 0, len = tableData.length; i < len; i++ ) { for ( var j = 0, jlen = tableData[ i ].length; j < jlen; j++ ) { if ( !tmp[ j ] ) { tmp[ j ] = []; } tmp[ j ][ i ] = tableData[ i ][ j ]; } } tableData = tmp; } categories = tableData[0].slice( 1 ); for ( var i = 1, data; data = tableData[ i ]; i++ ) { series.push( { name: data[ 0 ], data: data.slice( 1 ) } ); } return { series: series, categories: categories }; } /* * 获取数据源数据对齐方式 */ function getTableDataFormat () { var form = document.forms[ 'data-form' ], items = form['charts-format']; return items[ 0 ].checked ? items[ 0 ].value : items[ 1 ].value; } /* * 禁用非饼图类型的配置项 */ function disableNotPieConfig() { updateConfigItem( 'disable' ); } /* * 启用非饼图类型的配置项 */ function enableNotPieConfig() { updateConfigItem( 'enable' ); } function updateConfigItem ( value ) { var table = $( "#showTable" )[ 0 ], isDisable = value === 'disable' ? true : false; //table中的input处理 for ( var i = 2 , row; row = table.rows[ i ]; i++ ) { for ( var j = 1, cell; cell = row.cells[ j ]; j++ ) { $( "input", cell ).attr( "disabled", isDisable ); } } //其他项处理 $( "input.not-pie-item" ).attr( "disabled", isDisable ); $( "#tipInput" ).attr( "disabled", !isDisable ) } /* * 获取饼图数据 * 饼图的数据只取第一行的 **/ function getSeriesForPieChart () { var series = { type: 'pie', name: $("#tipInput").val(), data: [] }, tableData = getTableData(); for ( var j = 1, jlen = tableData[ 0 ].length; j < jlen; j++ ) { var title = tableData[ 0 ][ j ], val = tableData[ 1 ][ j ]; series.data.push( [ title, val ] ); } return { series: [ series ] }; } function getTableData () { var table = document.getElementById( "showTable" ), xCount = table.rows[0].cells.length - 1, values = getTableInputValue(); for ( var i = 0, value; value = values[ i ]; i++ ) { tableData[ Math.floor( i / xCount ) + 1 ][ i % xCount + 1 ] = values[ i ]; } return tableData; } function getTableInputValue () { var table = document.getElementById( "showTable" ), inputs = table.getElementsByTagName( "input" ), values = []; for ( var i = 0, input; input = inputs[ i ]; i++ ) { values.push( input.value | 0 ); } return values; } function getCellValue ( cell ) { var value = utils.trim( ( cell.innerText || cell.textContent || '' ) ); return value.replace( new RegExp( UE.dom.domUtils.fillChar, 'g' ), '' ).replace( /^\s+|\s+$/g, '' ); } //dialog确认事件 dialog.onok = function () { //收集信息 var form = document.forms[ 'data-form' ], info = getUserConfig(); //添加图表类型 info.chartType = currentChartType; //同步表格数据到编辑器 syncTableData(); //执行图表命令 editor.execCommand( 'charts', info ); }; /* * 同步图表编辑视图的表格数据到编辑器里的原始表格 */ function syncTableData () { var tableData = getTableData(); for ( var i = 1, row; row = editorTable.rows[ i ]; i++ ) { for ( var j = 1, cell; cell = row.cells[ j ]; j++ ) { cell.innerHTML = tableData[ i ] [ j ]; } } } ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/emotion/emotion.css ================================================ .jd img{ background:transparent url(images/jxface2.gif?v=1.1) no-repeat scroll left top; cursor:pointer;width:35px;height:35px;display:block; } .pp img{ background:transparent url(images/fface.gif?v=1.1) no-repeat scroll left top; cursor:pointer;width:25px;height:25px;display:block; } .ldw img{ background:transparent url(images/wface.gif?v=1.1) no-repeat scroll left top; cursor:pointer;width:35px;height:35px;display:block; } .tsj img{ background:transparent url(images/tface.gif?v=1.1) no-repeat scroll left top; cursor:pointer;width:35px;height:35px;display:block; } .cat img{ background:transparent url(images/cface.gif?v=1.1) no-repeat scroll left top; cursor:pointer;width:35px;height:35px;display:block; } .bb img{ background:transparent url(images/bface.gif?v=1.1) no-repeat scroll left top; cursor:pointer;width:35px;height:35px;display:block; } .youa img{ background:transparent url(images/yface.gif?v=1.1) no-repeat scroll left top; cursor:pointer;width:35px;height:35px;display:block; } .smileytable td {height: 37px;} #tabPanel{margin-left:5px;overflow: hidden;} #tabContent {float:left;background:#FFFFFF;} #tabContent div{display: none;width:480px;overflow:hidden;} #tabIconReview.show{left:17px;display:block;} .menuFocus{background:#ACCD3C;} .menuDefault{background:#FFFFFF;} #tabIconReview{position:absolute;left:406px;left:398px \9;top:41px;z-index:65533;width:90px;height:76px;} img.review{width:90px;height:76px;border:2px solid #9cb945;background:#FFFFFF;background-position:center;background-repeat:no-repeat;} .wrapper .tabbody{position:relative;float:left;clear:both;padding:10px;width: 95%;} .tabbody table{width: 100%;} .tabbody td{border:1px solid #BAC498;} .tabbody td span{display: block;zoom:1;padding:0 4px;} ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/emotion/emotion.html ================================================

    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/emotion/emotion.js ================================================ window.onload = function () { editor.setOpt({ emotionLocalization:false }); emotion.SmileyPath = editor.options.emotionLocalization === true ? 'images/' : "http://img.baidu.com/hi/"; emotion.SmileyBox = createTabList( emotion.tabNum ); emotion.tabExist = createArr( emotion.tabNum ); initImgName(); initEvtHandler( "tabHeads" ); }; function initImgName() { for ( var pro in emotion.SmilmgName ) { var tempName = emotion.SmilmgName[pro], tempBox = emotion.SmileyBox[pro], tempStr = ""; if ( tempBox.length ) return; for ( var i = 1; i <= tempName[1]; i++ ) { tempStr = tempName[0]; if ( i < 10 ) tempStr = tempStr + '0'; tempStr = tempStr + i + '.gif'; tempBox.push( tempStr ); } } } function initEvtHandler( conId ) { var tabHeads = $G( conId ); for ( var i = 0, j = 0; i < tabHeads.childNodes.length; i++ ) { var tabObj = tabHeads.childNodes[i]; if ( tabObj.nodeType == 1 ) { domUtils.on( tabObj, "click", (function ( index ) { return function () { switchTab( index ); }; })( j ) ); j++; } } switchTab( 0 ); $G( "tabIconReview" ).style.display = 'none'; } function InsertSmiley( url, evt ) { var obj = { src:editor.options.emotionLocalization ? editor.options.UEDITOR_HOME_URL + "dialogs/emotion/" + url : url }; obj._src = obj.src; editor.execCommand( 'insertimage', obj ); if ( !evt.ctrlKey ) { dialog.popup.hide(); } } function switchTab( index ) { autoHeight( index ); if ( emotion.tabExist[index] == 0 ) { emotion.tabExist[index] = 1; createTab( 'tab' + index ); } //获取呈现元素句柄数组 var tabHeads = $G( "tabHeads" ).getElementsByTagName( "span" ), tabBodys = $G( "tabBodys" ).getElementsByTagName( "div" ), i = 0, L = tabHeads.length; //隐藏所有呈现元素 for ( ; i < L; i++ ) { tabHeads[i].className = ""; tabBodys[i].style.display = "none"; } //显示对应呈现元素 tabHeads[index].className = "focus"; tabBodys[index].style.display = "block"; } function autoHeight( index ) { var iframe = dialog.getDom( "iframe" ), parent = iframe.parentNode.parentNode; switch ( index ) { case 0: iframe.style.height = "380px"; parent.style.height = "392px"; break; case 1: iframe.style.height = "220px"; parent.style.height = "232px"; break; case 2: iframe.style.height = "260px"; parent.style.height = "272px"; break; case 3: iframe.style.height = "300px"; parent.style.height = "312px"; break; case 4: iframe.style.height = "140px"; parent.style.height = "152px"; break; case 5: iframe.style.height = "260px"; parent.style.height = "272px"; break; case 6: iframe.style.height = "230px"; parent.style.height = "242px"; break; default: } } function createTab( tabName ) { var faceVersion = "?v=1.1", //版本号 tab = $G( tabName ), //获取将要生成的Div句柄 imagePath = emotion.SmileyPath + emotion.imageFolders[tabName], //获取显示表情和预览表情的路径 positionLine = 11 / 2, //中间数 iWidth = iHeight = 35, //图片长宽 iColWidth = 3, //表格剩余空间的显示比例 tableCss = emotion.imageCss[tabName], cssOffset = emotion.imageCssOffset[tabName], textHTML = [''], i = 0, imgNum = emotion.SmileyBox[tabName].length, imgColNum = 11, faceImage, sUrl, realUrl, posflag, offset, infor; for ( ; i < imgNum; ) { textHTML.push( '' ); for ( var j = 0; j < imgColNum; j++, i++ ) { faceImage = emotion.SmileyBox[tabName][i]; if ( faceImage ) { sUrl = imagePath + faceImage + faceVersion; realUrl = imagePath + faceImage; posflag = j < positionLine ? 0 : 1; offset = cssOffset * i * (-1) - 1; infor = emotion.SmileyInfor[tabName][i]; textHTML.push( '' ); } textHTML.push( '' ); } textHTML.push( '
    ' ); textHTML.push( '' ); textHTML.push( '' ); textHTML.push( '' ); } else { textHTML.push( '' ); } textHTML.push( '
    ' ); textHTML = textHTML.join( "" ); tab.innerHTML = textHTML; } function over( td, srcPath, posFlag ) { td.style.backgroundColor = "#ACCD3C"; $G( 'faceReview' ).style.backgroundImage = "url(" + srcPath + ")"; if ( posFlag == 1 ) $G( "tabIconReview" ).className = "show"; $G( "tabIconReview" ).style.display = 'block'; } function out( td ) { td.style.backgroundColor = "transparent"; var tabIconRevew = $G( "tabIconReview" ); tabIconRevew.className = ""; tabIconRevew.style.display = 'none'; } function createTabList( tabNum ) { var obj = {}; for ( var i = 0; i < tabNum; i++ ) { obj["tab" + i] = []; } return obj; } function createArr( tabNum ) { var arr = []; for ( var i = 0; i < tabNum; i++ ) { arr[i] = 0; } return arr; } ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/gmap/gmap.html ================================================
    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/help/help.css ================================================ .wrapper{width: 370px;margin: 10px auto;zoom: 1;} .tabbody{height: 360px;} .tabbody .panel{width:100%;height: 360px;position: absolute;background: #fff;} .tabbody .panel h1{font-size:26px;margin: 5px 0 0 5px;} .tabbody .panel p{font-size:12px;margin: 5px 0 0 5px;} .tabbody table{width:90%;line-height: 20px;margin: 5px 0 0 5px;;} .tabbody table thead{font-weight: bold;line-height: 25px;} ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/help/help.html ================================================ 帮助

    UEditor

    ctrl+b
    ctrl+c
    ctrl+x
    ctrl+v
    ctrl+y
    ctrl+z
    ctrl+i
    ctrl+u
    ctrl+a
    shift+enter
    alt+z
    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/help/help.js ================================================ /** * Created with JetBrains PhpStorm. * User: xuheng * Date: 12-9-26 * Time: 下午1:06 * To change this template use File | Settings | File Templates. */ /** * tab点击处理事件 * @param tabHeads * @param tabBodys * @param obj */ function clickHandler( tabHeads,tabBodys,obj ) { //head样式更改 for ( var k = 0, len = tabHeads.length; k < len; k++ ) { tabHeads[k].className = ""; } obj.className = "focus"; //body显隐 var tabSrc = obj.getAttribute( "tabSrc" ); for ( var j = 0, length = tabBodys.length; j < length; j++ ) { var body = tabBodys[j], id = body.getAttribute( "id" ); body.onclick = function(){ this.style.zoom = 1; }; if ( id != tabSrc ) { body.style.zIndex = 1; } else { body.style.zIndex = 200; } } } /** * TAB切换 * @param tabParentId tab的父节点ID或者对象本身 */ function switchTab( tabParentId ) { var tabElements = $G( tabParentId ).children, tabHeads = tabElements[0].children, tabBodys = tabElements[1].children; for ( var i = 0, length = tabHeads.length; i < length; i++ ) { var head = tabHeads[i]; if ( head.className === "focus" )clickHandler(tabHeads,tabBodys, head ); head.onclick = function () { clickHandler(tabHeads,tabBodys,this); } } } switchTab("helptab"); document.getElementById('version').innerHTML = parent.UE.version; ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/image/image.css ================================================ @charset "utf-8"; /* dialog样式 */ .wrapper { zoom: 1; width: 630px; *width: 626px; height: 380px; margin: 0 auto; padding: 10px; position: relative; font-family: sans-serif; } /*tab样式框大小*/ .tabhead { float:left; } .tabbody { width: 100%; height: 346px; position: relative; clear: both; } .tabbody .panel { position: absolute; width: 0; height: 0; background: #fff; overflow: hidden; display: none; } .tabbody .panel.focus { width: 100%; height: 346px; display: block; } /* 图片对齐方式 */ .alignBar{ float:right; margin-top: 5px; position: relative; } .alignBar .algnLabel{ float:left; height: 20px; line-height: 20px; } .alignBar #alignIcon{ zoom:1; _display: inline; display: inline-block; position: relative; } .alignBar #alignIcon span{ float: left; cursor: pointer; display: block; width: 19px; height: 17px; margin-right: 3px; margin-left: 3px; background-image: url(./images/alignicon.jpg); } .alignBar #alignIcon .none-align{ background-position: 0 -18px; } .alignBar #alignIcon .left-align{ background-position: -20px -18px; } .alignBar #alignIcon .right-align{ background-position: -40px -18px; } .alignBar #alignIcon .center-align{ background-position: -60px -18px; } .alignBar #alignIcon .none-align.focus{ background-position: 0 0; } .alignBar #alignIcon .left-align.focus{ background-position: -20px 0; } .alignBar #alignIcon .right-align.focus{ background-position: -40px 0; } .alignBar #alignIcon .center-align.focus{ background-position: -60px 0; } /* 远程图片样式 */ #remote { z-index: 200; } #remote .top{ width: 100%; margin-top: 25px; } #remote .left{ display: block; float: left; width: 300px; height:10px; } #remote .right{ display: block; float: right; width: 300px; height:10px; } #remote .row{ margin-left: 20px; clear: both; height: 40px; } #remote .row label{ text-align: center; width: 50px; zoom:1; _display: inline; display:inline-block; vertical-align: middle; } #remote .row label.algnLabel{ float: left; } #remote input.text{ width: 150px; padding: 3px 6px; font-size: 14px; line-height: 1.42857143; color: #555; background-color: #fff; background-image: none; border: 1px solid #ccc; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; } #remote input.text:focus { border-color: #66afe9; outline: 0; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6); } #remote #url{ width: 500px; margin-bottom: 2px; } #remote #width, #remote #height{ width: 20px; margin-left: 2px; margin-right: 2px; } #remote #border, #remote #vhSpace, #remote #title{ width: 180px; margin-right: 5px; } #remote #lock{ } #remote #lockicon{ zoom: 1; _display:inline; display: inline-block; width: 20px; height: 20px; background: url("../../themes/default/images/lock.gif") -13px -13px no-repeat; vertical-align: middle; } #remote #preview{ clear: both; width: 260px; height: 240px; z-index: 9999; margin-top: 10px; background-color: #eee; overflow: hidden; } /* 上传图片 */ .tabbody #upload.panel { width: 0; height: 0; overflow: hidden; position: absolute !important; clip: rect(1px, 1px, 1px, 1px); background: #fff; display: block; } .tabbody #upload.panel.focus { width: 100%; height: 346px; display: block; clip: auto; } #upload .queueList { margin: 0; width: 100%; height: 100%; position: absolute; overflow: hidden; } #upload p { margin: 0; } .element-invisible { width: 0 !important; height: 0 !important; border: 0; padding: 0; margin: 0; overflow: hidden; position: absolute !important; clip: rect(1px, 1px, 1px, 1px); } #upload .placeholder { margin: 10px; border: 2px dashed #e6e6e6; *border: 0px dashed #e6e6e6; height: 172px; padding-top: 150px; text-align: center; background: url(./images/image.png) center 70px no-repeat; color: #cccccc; font-size: 18px; position: relative; top:0; *top: 10px; } #upload .placeholder .webuploader-pick { font-size: 18px; background: #00b7ee; border-radius: 3px; line-height: 44px; padding: 0 30px; *width: 120px; color: #fff; display: inline-block; margin: 0 auto 20px auto; cursor: pointer; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); } #upload .placeholder .webuploader-pick-hover { background: #00a2d4; } #filePickerContainer { text-align: center; } #upload .placeholder .flashTip { color: #666666; font-size: 12px; position: absolute; width: 100%; text-align: center; bottom: 20px; } #upload .placeholder .flashTip a { color: #0785d1; text-decoration: none; } #upload .placeholder .flashTip a:hover { text-decoration: underline; } #upload .placeholder.webuploader-dnd-over { border-color: #999999; } #upload .filelist { list-style: none; margin: 0; padding: 0; overflow-x: hidden; overflow-y: auto; position: relative; height: 300px; } #upload .filelist:after { content: ''; display: block; width: 0; height: 0; overflow: hidden; clear: both; position: relative; } #upload .filelist li { width: 113px; height: 113px; background: url(./images/bg.png); text-align: center; margin: 9px 0 0 9px; *margin: 6px 0 0 6px; position: relative; display: block; float: left; overflow: hidden; font-size: 12px; } #upload .filelist li p.log { position: relative; top: -45px; } #upload .filelist li p.title { position: absolute; top: 0; left: 0; width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; top: 5px; text-indent: 5px; text-align: left; } #upload .filelist li p.progress { position: absolute; width: 100%; bottom: 0; left: 0; height: 8px; overflow: hidden; z-index: 50; margin: 0; border-radius: 0; background: none; -webkit-box-shadow: 0 0 0; } #upload .filelist li p.progress span { display: none; overflow: hidden; width: 0; height: 100%; background: #1483d8 url(./images/progress.png) repeat-x; -webit-transition: width 200ms linear; -moz-transition: width 200ms linear; -o-transition: width 200ms linear; -ms-transition: width 200ms linear; transition: width 200ms linear; -webkit-animation: progressmove 2s linear infinite; -moz-animation: progressmove 2s linear infinite; -o-animation: progressmove 2s linear infinite; -ms-animation: progressmove 2s linear infinite; animation: progressmove 2s linear infinite; -webkit-transform: translateZ(0); } @-webkit-keyframes progressmove { 0% { background-position: 0 0; } 100% { background-position: 17px 0; } } @-moz-keyframes progressmove { 0% { background-position: 0 0; } 100% { background-position: 17px 0; } } @keyframes progressmove { 0% { background-position: 0 0; } 100% { background-position: 17px 0; } } #upload .filelist li p.imgWrap { position: relative; z-index: 2; line-height: 113px; vertical-align: middle; overflow: hidden; width: 113px; height: 113px; -webkit-transform-origin: 50% 50%; -moz-transform-origin: 50% 50%; -o-transform-origin: 50% 50%; -ms-transform-origin: 50% 50%; transform-origin: 50% 50%; -webit-transition: 200ms ease-out; -moz-transition: 200ms ease-out; -o-transition: 200ms ease-out; -ms-transition: 200ms ease-out; transition: 200ms ease-out; } #upload .filelist li img { width: 100%; } #upload .filelist li p.error { background: #f43838; color: #fff; position: absolute; bottom: 0; left: 0; height: 28px; line-height: 28px; width: 100%; z-index: 100; display:none; } #upload .filelist li .success { display: block; position: absolute; left: 0; bottom: 0; height: 40px; width: 100%; z-index: 200; background: url(./images/success.png) no-repeat right bottom; background: url(./images/success.gif) no-repeat right bottom \9; } #upload .filelist li.filePickerBlock { width: 113px; height: 113px; background: url(./images/image.png) no-repeat center 12px; border: 1px solid #eeeeee; border-radius: 0; } #upload .filelist li.filePickerBlock div.webuploader-pick { width: 100%; height: 100%; margin: 0; padding: 0; opacity: 0; background: none; font-size: 0; } #upload .filelist div.file-panel { position: absolute; height: 0; filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#80000000', endColorstr='#80000000') \0; background: rgba(0, 0, 0, 0.5); width: 100%; top: 0; left: 0; overflow: hidden; z-index: 300; } #upload .filelist div.file-panel span { width: 24px; height: 24px; display: inline; float: right; text-indent: -9999px; overflow: hidden; background: url(./images/icons.png) no-repeat; background: url(./images/icons.gif) no-repeat \9; margin: 5px 1px 1px; cursor: pointer; -webkit-tap-highlight-color: rgba(0,0,0,0); -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } #upload .filelist div.file-panel span.rotateLeft { display:none; background-position: 0 -24px; } #upload .filelist div.file-panel span.rotateLeft:hover { background-position: 0 0; } #upload .filelist div.file-panel span.rotateRight { display:none; background-position: -24px -24px; } #upload .filelist div.file-panel span.rotateRight:hover { background-position: -24px 0; } #upload .filelist div.file-panel span.cancel { background-position: -48px -24px; } #upload .filelist div.file-panel span.cancel:hover { background-position: -48px 0; } #upload .statusBar { height: 45px; border-bottom: 1px solid #dadada; margin: 0 10px; padding: 0; line-height: 45px; vertical-align: middle; position: relative; } #upload .statusBar .progress { border: 1px solid #1483d8; width: 198px; background: #fff; height: 18px; position: absolute; top: 12px; display: none; text-align: center; line-height: 18px; color: #6dbfff; margin: 0 10px 0 0; } #upload .statusBar .progress span.percentage { width: 0; height: 100%; left: 0; top: 0; background: #1483d8; position: absolute; } #upload .statusBar .progress span.text { position: relative; z-index: 10; } #upload .statusBar .info { display: inline-block; font-size: 14px; color: #666666; } #upload .statusBar .btns { position: absolute; top: 7px; right: 0; line-height: 30px; } #filePickerBtn { display: inline-block; float: left; } #upload .statusBar .btns .webuploader-pick, #upload .statusBar .btns .uploadBtn, #upload .statusBar .btns .uploadBtn.state-uploading, #upload .statusBar .btns .uploadBtn.state-paused { background: #ffffff; border: 1px solid #cfcfcf; color: #565656; padding: 0 18px; display: inline-block; border-radius: 3px; margin-left: 10px; cursor: pointer; font-size: 14px; float: left; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } #upload .statusBar .btns .webuploader-pick-hover, #upload .statusBar .btns .uploadBtn:hover, #upload .statusBar .btns .uploadBtn.state-uploading:hover, #upload .statusBar .btns .uploadBtn.state-paused:hover { background: #f0f0f0; } #upload .statusBar .btns .uploadBtn, #upload .statusBar .btns .uploadBtn.state-paused{ background: #00b7ee; color: #fff; border-color: transparent; } #upload .statusBar .btns .uploadBtn:hover, #upload .statusBar .btns .uploadBtn.state-paused:hover{ background: #00a2d4; } #upload .statusBar .btns .uploadBtn.disabled { pointer-events: none; filter:alpha(opacity=60); -moz-opacity:0.6; -khtml-opacity: 0.6; opacity: 0.6; } /* 图片管理样式 */ #online { width: 100%; height: 336px; padding: 10px 0 0 0; } #online #imageList{ width: 100%; height: 100%; overflow-x: hidden; overflow-y: auto; position: relative; } #online ul { display: block; list-style: none; margin: 0; padding: 0; } #online li { float: left; display: block; list-style: none; padding: 0; width: 113px; height: 113px; margin: 0 0 9px 9px; *margin: 0 0 6px 6px; background-color: #eee; overflow: hidden; cursor: pointer; position: relative; } #online li.clearFloat { float: none; clear: both; display: block; width:0; height:0; margin: 0; padding: 0; } #online li img { cursor: pointer; } #online li .icon { cursor: pointer; width: 113px; height: 113px; position: absolute; top: 0; left: 0; z-index: 2; border: 0; background-repeat: no-repeat; } #online li .icon:hover { width: 107px; height: 107px; border: 3px solid #1094fa; } #online li.selected .icon { background-image: url(images/success.png); background-image: url(images/success.gif)\9; background-position: 75px 75px; } #online li.selected .icon:hover { width: 107px; height: 107px; border: 3px solid #1094fa; background-position: 72px 72px; } /* 图片搜索样式 */ #search .searchBar { width: 100%; height: 30px; margin: 10px 0 5px 0; padding: 0; } #search input.text{ width: 150px; padding: 3px 6px; font-size: 14px; line-height: 1.42857143; color: #555; background-color: #fff; background-image: none; border: 1px solid #ccc; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; } #search input.text:focus { border-color: #66afe9; outline: 0; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6); } #search input.searchTxt { margin-left:5px; padding-left: 5px; background: #FFF; width: 300px; *width: 260px; height: 21px; line-height: 21px; float: left; dislay: block; } #search .searchType { width: 65px; height: 28px; padding:0; line-height: 28px; border: 1px solid #d7d7d7; border-radius: 0; vertical-align: top; margin-left: 5px; float: left; dislay: block; } #search #searchBtn, #search #searchReset { display: inline-block; margin-bottom: 0; margin-right: 5px; padding: 4px 10px; font-weight: 400; text-align: center; vertical-align: middle; cursor: pointer; background-image: none; border: 1px solid transparent; white-space: nowrap; font-size: 14px; border-radius: 4px; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; vertical-align: top; float: right; } #search #searchBtn { color: white; border-color: #285e8e; background-color: #3b97d7; } #search #searchReset { color: #333; border-color: #ccc; background-color: #fff; } #search #searchBtn:hover { background-color: #3276b1; } #search #searchReset:hover { background-color: #eee; } #search .msg { margin-left: 5px; } #search .searchList{ width: 100%; height: 300px; overflow: hidden; clear: both; } #search .searchList ul{ margin:0; padding:0; list-style:none; clear: both; width: 100%; height: 100%; overflow-x: hidden; overflow-y: auto; zoom: 1; position: relative; } #search .searchList li { list-style:none; float: left; display: block; width: 115px; margin: 5px 10px 5px 20px; *margin: 5px 10px 5px 15px; padding:0; font-size: 12px; box-shadow: 0 1px 3px rgba(0, 0, 0, .3); -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, .3); -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, .3); position: relative; vertical-align: top; text-align: center; overflow: hidden; cursor: pointer; filter: alpha(Opacity=100); -moz-opacity: 1; opacity: 1; border: 2px solid #eee; } #search .searchList li.selected { filter: alpha(Opacity=40); -moz-opacity: 0.4; opacity: 0.4; border: 2px solid #00a0e9; } #search .searchList li p { background-color: #eee; margin: 0; padding: 0; position: relative; width:100%; height:115px; overflow: hidden; } #search .searchList li p img { cursor: pointer; border: 0; } #search .searchList li a { color: #999; border-top: 1px solid #F2F2F2; background: #FAFAFA; text-align: center; display: block; padding: 0 5px; width: 105px; height:32px; line-height:32px; white-space:nowrap; text-overflow:ellipsis; text-decoration: none; overflow: hidden; word-break: break-all; } #search .searchList a:hover { text-decoration: underline; color: #333; } #search .searchList .clearFloat{ clear: both; } ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/image/image.html ================================================ ueditor图片对话框
      px   px
    px
    px
    0%
    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/image/image.js ================================================ /** * User: Jinqn * Date: 14-04-08 * Time: 下午16:34 * 上传图片对话框逻辑代码,包括tab: 远程图片/上传图片/在线图片/搜索图片 */ (function () { var remoteImage, uploadImage, onlineImage, searchImage; window.onload = function () { initTabs(); initAlign(); initButtons(); }; /* 初始化tab标签 */ function initTabs() { var tabs = $G('tabhead').children; for (var i = 0; i < tabs.length; i++) { domUtils.on(tabs[i], "click", function (e) { var target = e.target || e.srcElement; setTabFocus(target.getAttribute('data-content-id')); }); } var img = editor.selection.getRange().getClosedNode(); if (img && img.tagName && img.tagName.toLowerCase() == 'img') { setTabFocus('remote'); } else { setTabFocus('upload'); } } /* 初始化tabbody */ function setTabFocus(id) { if(!id) return; var i, bodyId, tabs = $G('tabhead').children; for (i = 0; i < tabs.length; i++) { bodyId = tabs[i].getAttribute('data-content-id'); if (bodyId == id) { domUtils.addClass(tabs[i], 'focus'); domUtils.addClass($G(bodyId), 'focus'); } else { domUtils.removeClasses(tabs[i], 'focus'); domUtils.removeClasses($G(bodyId), 'focus'); } } switch (id) { case 'remote': remoteImage = remoteImage || new RemoteImage(); break; case 'upload': setAlign(editor.getOpt('imageInsertAlign')); uploadImage = uploadImage || new UploadImage('queueList'); break; case 'online': setAlign(editor.getOpt('imageManagerInsertAlign')); onlineImage = onlineImage || new OnlineImage('imageList'); onlineImage.reset(); break; case 'search': setAlign(editor.getOpt('imageManagerInsertAlign')); searchImage = searchImage || new SearchImage(); break; } } /* 初始化onok事件 */ function initButtons() { dialog.onok = function () { var remote = false, list = [], id, tabs = $G('tabhead').children; for (var i = 0; i < tabs.length; i++) { if (domUtils.hasClass(tabs[i], 'focus')) { id = tabs[i].getAttribute('data-content-id'); break; } } switch (id) { case 'remote': list = remoteImage.getInsertList(); break; case 'upload': list = uploadImage.getInsertList(); var count = uploadImage.getQueueCount(); if (count) { $('.info', '#queueList').html('' + '还有2个未上传文件'.replace(/[\d]/, count) + ''); return false; } break; case 'online': list = onlineImage.getInsertList(); break; case 'search': list = searchImage.getInsertList(); remote = true; break; } if(list) { editor.execCommand('insertimage', list); remote && editor.fireEvent("catchRemoteImage"); } }; } /* 初始化对其方式的点击事件 */ function initAlign(){ /* 点击align图标 */ domUtils.on($G("alignIcon"), 'click', function(e){ var target = e.target || e.srcElement; if(target.className && target.className.indexOf('-align') != -1) { setAlign(target.getAttribute('data-align')); } }); } /* 设置对齐方式 */ function setAlign(align){ align = align || 'none'; var aligns = $G("alignIcon").children; for(i = 0; i < aligns.length; i++){ if(aligns[i].getAttribute('data-align') == align) { domUtils.addClass(aligns[i], 'focus'); $G("align").value = aligns[i].getAttribute('data-align'); } else { domUtils.removeClasses(aligns[i], 'focus'); } } } /* 获取对齐方式 */ function getAlign(){ var align = $G("align").value || 'none'; return align == 'none' ? '':align; } /* 在线图片 */ function RemoteImage(target) { this.container = utils.isString(target) ? document.getElementById(target) : target; this.init(); } RemoteImage.prototype = { init: function () { this.initContainer(); this.initEvents(); }, initContainer: function () { this.dom = { 'url': $G('url'), 'width': $G('width'), 'height': $G('height'), 'border': $G('border'), 'vhSpace': $G('vhSpace'), 'title': $G('title'), 'align': $G('align') }; var img = editor.selection.getRange().getClosedNode(); if (img) { this.setImage(img); } }, initEvents: function () { var _this = this, locker = $G('lock'); /* 改变url */ domUtils.on($G("url"), 'keyup', updatePreview); domUtils.on($G("border"), 'keyup', updatePreview); domUtils.on($G("title"), 'keyup', updatePreview); domUtils.on($G("width"), 'keyup', function(){ updatePreview(); if(locker.checked) { var proportion =locker.getAttribute('data-proportion'); $G('height').value = Math.round(this.value / proportion); } else { _this.updateLocker(); } }); domUtils.on($G("height"), 'keyup', function(){ updatePreview(); if(locker.checked) { var proportion =locker.getAttribute('data-proportion'); $G('width').value = Math.round(this.value * proportion); } else { _this.updateLocker(); } }); domUtils.on($G("lock"), 'change', function(){ var proportion = parseInt($G("width").value) /parseInt($G("height").value); locker.setAttribute('data-proportion', proportion); }); function updatePreview(){ _this.setPreview(); } }, updateLocker: function(){ var width = $G('width').value, height = $G('height').value, locker = $G('lock'); if(width && height && width == parseInt(width) && height == parseInt(height)) { locker.disabled = false; locker.title = ''; } else { locker.checked = false; locker.disabled = 'disabled'; locker.title = lang.remoteLockError; } }, setImage: function(img){ /* 不是正常的图片 */ if (!img.tagName || img.tagName.toLowerCase() != 'img' && !img.getAttribute("src") || !img.src) return; var wordImgFlag = img.getAttribute("word_img"), src = wordImgFlag ? wordImgFlag.replace("&", "&") : (img.getAttribute('_src') || img.getAttribute("src", 2).replace("&", "&")), align = editor.queryCommandValue("imageFloat"); /* 防止onchange事件循环调用 */ if (src !== $G("url").value) $G("url").value = src; if(src) { /* 设置表单内容 */ $G("width").value = img.width || ''; $G("height").value = img.height || ''; $G("border").value = img.getAttribute("border") || '0'; $G("vhSpace").value = img.getAttribute("vspace") || '0'; $G("title").value = img.title || img.alt || ''; setAlign(align); this.setPreview(); this.updateLocker(); } }, getData: function(){ var data = {}; for(var k in this.dom){ data[k] = this.dom[k].value; } return data; }, setPreview: function(){ var url = $G('url').value, ow = parseInt($G('width').value, 10) || 0, oh = parseInt($G('height').value, 10) || 0, border = parseInt($G('border').value, 10) || 0, title = $G('title').value, preview = $G('preview'), width, height; url = utils.unhtmlForUrl(url); title = utils.unhtml(title); width = ((!ow || !oh) ? preview.offsetWidth:Math.min(ow, preview.offsetWidth)); width = width+(border*2) > preview.offsetWidth ? width:(preview.offsetWidth - (border*2)); height = (!ow || !oh) ? '':width*oh/ow; if(url) { preview.innerHTML = ''; } }, getInsertList: function () { var data = this.getData(); if(data['url']) { return [{ src: data['url'], _src: data['url'], width: data['width'] || '', height: data['height'] || '', border: data['border'] || '', floatStyle: data['align'] || '', vspace: data['vhSpace'] || '', title: data['title'] || '', alt: data['title'] || '', style: "width:" + data['width'] + "px;height:" + data['height'] + "px;" }]; } else { return []; } } }; /* 上传图片 */ function UploadImage(target) { this.$wrap = target.constructor == String ? $('#' + target) : $(target); this.init(); } UploadImage.prototype = { init: function () { this.imageList = []; this.initContainer(); this.initUploader(); }, initContainer: function () { this.$queue = this.$wrap.find('.filelist'); }, /* 初始化容器 */ initUploader: function () { var _this = this, $ = jQuery, // just in case. Make sure it's not an other libaray. $wrap = _this.$wrap, // 图片容器 $queue = $wrap.find('.filelist'), // 状态栏,包括进度和控制按钮 $statusBar = $wrap.find('.statusBar'), // 文件总体选择信息。 $info = $statusBar.find('.info'), // 上传按钮 $upload = $wrap.find('.uploadBtn'), // 上传按钮 $filePickerBtn = $wrap.find('.filePickerBtn'), // 上传按钮 $filePickerBlock = $wrap.find('.filePickerBlock'), // 没选择文件之前的内容。 $placeHolder = $wrap.find('.placeholder'), // 总体进度条 $progress = $statusBar.find('.progress').hide(), // 添加的文件数量 fileCount = 0, // 添加的文件总大小 fileSize = 0, // 优化retina, 在retina下这个值是2 ratio = window.devicePixelRatio || 1, // 缩略图大小 thumbnailWidth = 113 * ratio, thumbnailHeight = 113 * ratio, // 可能有pedding, ready, uploading, confirm, done. state = '', // 所有文件的进度信息,key为file id percentages = {}, supportTransition = (function () { var s = document.createElement('p').style, r = 'transition' in s || 'WebkitTransition' in s || 'MozTransition' in s || 'msTransition' in s || 'OTransition' in s; s = null; return r; })(), // WebUploader实例 uploader, actionUrl = editor.getActionUrl(editor.getOpt('imageActionName')), acceptExtensions = (editor.getOpt('imageAllowFiles') || []).join('').replace(/\./g, ',').replace(/^[,]/, ''), imageMaxSize = editor.getOpt('imageMaxSize'), imageCompressBorder = editor.getOpt('imageCompressBorder'); if (!WebUploader.Uploader.support()) { $('#filePickerReady').after($('
    ').html(lang.errorNotSupport)).hide(); return; } else if (!editor.getOpt('imageActionName')) { $('#filePickerReady').after($('
    ').html(lang.errorLoadConfig)).hide(); return; } uploader = _this.uploader = WebUploader.create({ pick: { id: '#filePickerReady', label: lang.uploadSelectFile }, accept: { title: 'Images', extensions: acceptExtensions, mimeTypes: 'image/*' }, swf: '../../third-party/webuploader/Uploader.swf', server: actionUrl, fileVal: editor.getOpt('imageFieldName'), duplicate: true, fileSingleSizeLimit: imageMaxSize, // 默认 2 M compress: editor.getOpt('imageCompressEnable') ? { width: imageCompressBorder, height: imageCompressBorder, // 图片质量,只有type为`image/jpeg`的时候才有效。 quality: 90, // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. allowMagnify: false, // 是否允许裁剪。 crop: false, // 是否保留头部meta信息。 preserveHeaders: true }:false }); uploader.addButton({ id: '#filePickerBlock' }); uploader.addButton({ id: '#filePickerBtn', label: lang.uploadAddFile }); setState('pedding'); // 当有文件添加进来时执行,负责view的创建 function addFile(file) { var $li = $('
  • ' + '

    ' + file.name + '

    ' + '

    ' + '

    ' + '
  • '), $btns = $('
    ' + '' + lang.uploadDelete + '' + '' + lang.uploadTurnRight + '' + '' + lang.uploadTurnLeft + '
    ').appendTo($li), $prgress = $li.find('p.progress span'), $wrap = $li.find('p.imgWrap'), $info = $('

    ').hide().appendTo($li), showError = function (code) { switch (code) { case 'exceed_size': text = lang.errorExceedSize; break; case 'interrupt': text = lang.errorInterrupt; break; case 'http': text = lang.errorHttp; break; case 'not_allow_type': text = lang.errorFileType; break; default: text = lang.errorUploadRetry; break; } $info.text(text).show(); }; if (file.getStatus() === 'invalid') { showError(file.statusText); } else { $wrap.text(lang.uploadPreview); if (browser.ie && browser.version <= 7) { $wrap.text(lang.uploadNoPreview); } else { uploader.makeThumb(file, function (error, src) { if (error || !src) { $wrap.text(lang.uploadNoPreview); } else { var $img = $(''); $wrap.empty().append($img); $img.on('error', function () { $wrap.text(lang.uploadNoPreview); }); } }, thumbnailWidth, thumbnailHeight); } percentages[ file.id ] = [ file.size, 0 ]; file.rotation = 0; /* 检查文件格式 */ if (!file.ext || acceptExtensions.indexOf(file.ext.toLowerCase()) == -1) { showError('not_allow_type'); uploader.removeFile(file); } } file.on('statuschange', function (cur, prev) { if (prev === 'progress') { $prgress.hide().width(0); } else if (prev === 'queued') { $li.off('mouseenter mouseleave'); $btns.remove(); } // 成功 if (cur === 'error' || cur === 'invalid') { showError(file.statusText); percentages[ file.id ][ 1 ] = 1; } else if (cur === 'interrupt') { showError('interrupt'); } else if (cur === 'queued') { percentages[ file.id ][ 1 ] = 0; } else if (cur === 'progress') { $info.hide(); $prgress.css('display', 'block'); } else if (cur === 'complete') { } $li.removeClass('state-' + prev).addClass('state-' + cur); }); $li.on('mouseenter', function () { $btns.stop().animate({height: 30}); }); $li.on('mouseleave', function () { $btns.stop().animate({height: 0}); }); $btns.on('click', 'span', function () { var index = $(this).index(), deg; switch (index) { case 0: uploader.removeFile(file); return; case 1: file.rotation += 90; break; case 2: file.rotation -= 90; break; } if (supportTransition) { deg = 'rotate(' + file.rotation + 'deg)'; $wrap.css({ '-webkit-transform': deg, '-mos-transform': deg, '-o-transform': deg, 'transform': deg }); } else { $wrap.css('filter', 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' + (~~((file.rotation / 90) % 4 + 4) % 4) + ')'); } }); $li.insertBefore($filePickerBlock); } // 负责view的销毁 function removeFile(file) { var $li = $('#' + file.id); delete percentages[ file.id ]; updateTotalProgress(); $li.off().find('.file-panel').off().end().remove(); } function updateTotalProgress() { var loaded = 0, total = 0, spans = $progress.children(), percent; $.each(percentages, function (k, v) { total += v[ 0 ]; loaded += v[ 0 ] * v[ 1 ]; }); percent = total ? loaded / total : 0; spans.eq(0).text(Math.round(percent * 100) + '%'); spans.eq(1).css('width', Math.round(percent * 100) + '%'); updateStatus(); } function setState(val, files) { if (val != state) { var stats = uploader.getStats(); $upload.removeClass('state-' + state); $upload.addClass('state-' + val); switch (val) { /* 未选择文件 */ case 'pedding': $queue.addClass('element-invisible'); $statusBar.addClass('element-invisible'); $placeHolder.removeClass('element-invisible'); $progress.hide(); $info.hide(); uploader.refresh(); break; /* 可以开始上传 */ case 'ready': $placeHolder.addClass('element-invisible'); $queue.removeClass('element-invisible'); $statusBar.removeClass('element-invisible'); $progress.hide(); $info.show(); $upload.text(lang.uploadStart); uploader.refresh(); break; /* 上传中 */ case 'uploading': $progress.show(); $info.hide(); $upload.text(lang.uploadPause); break; /* 暂停上传 */ case 'paused': $progress.show(); $info.hide(); $upload.text(lang.uploadContinue); break; case 'confirm': $progress.show(); $info.hide(); $upload.text(lang.uploadStart); stats = uploader.getStats(); if (stats.successNum && !stats.uploadFailNum) { setState('finish'); return; } break; case 'finish': $progress.hide(); $info.show(); if (stats.uploadFailNum) { $upload.text(lang.uploadRetry); } else { $upload.text(lang.uploadStart); } break; } state = val; updateStatus(); } if (!_this.getQueueCount()) { $upload.addClass('disabled') } else { $upload.removeClass('disabled') } } function updateStatus() { var text = '', stats; if (state === 'ready') { text = lang.updateStatusReady.replace('_', fileCount).replace('_KB', WebUploader.formatSize(fileSize)); } else if (state === 'confirm') { stats = uploader.getStats(); if (stats.uploadFailNum) { text = lang.updateStatusConfirm.replace('_', stats.successNum).replace('_', stats.successNum); } } else { stats = uploader.getStats(); text = lang.updateStatusFinish.replace('_', fileCount). replace('_KB', WebUploader.formatSize(fileSize)). replace('_', stats.successNum); if (stats.uploadFailNum) { text += lang.updateStatusError.replace('_', stats.uploadFailNum); } } $info.html(text); } uploader.on('fileQueued', function (file) { fileCount++; fileSize += file.size; if (fileCount === 1) { $placeHolder.addClass('element-invisible'); $statusBar.show(); } addFile(file); }); uploader.on('fileDequeued', function (file) { fileCount--; fileSize -= file.size; removeFile(file); updateTotalProgress(); }); uploader.on('filesQueued', function (file) { if (!uploader.isInProgress() && (state == 'pedding' || state == 'finish' || state == 'confirm' || state == 'ready')) { setState('ready'); } updateTotalProgress(); }); uploader.on('all', function (type, files) { switch (type) { case 'uploadFinished': setState('confirm', files); break; case 'startUpload': /* 添加额外的GET参数 */ var params = utils.serializeParam(editor.queryCommandValue('serverparam')) || '', url = utils.formatUrl(actionUrl + (actionUrl.indexOf('?') == -1 ? '?':'&') + 'encode=utf-8&' + params); uploader.option('server', url); setState('uploading', files); break; case 'stopUpload': setState('paused', files); break; } }); uploader.on('uploadBeforeSend', function (file, data, header) { //这里可以通过data对象添加POST参数 header['X_Requested_With'] = 'XMLHttpRequest'; // HaoChuan9421 if(editor.options.headers && Object.prototype.toString.apply(editor.options.headers) === "[object Object]"){ for(var key in editor.options.headers){ header[key] = editor.options.headers[key] } } }); uploader.on('uploadProgress', function (file, percentage) { var $li = $('#' + file.id), $percent = $li.find('.progress span'); $percent.css('width', percentage * 100 + '%'); percentages[ file.id ][ 1 ] = percentage; updateTotalProgress(); }); uploader.on('uploadSuccess', function (file, ret) { var $file = $('#' + file.id); try { var responseText = (ret._raw || ret), json = utils.str2json(responseText); if (json.state == 'SUCCESS') { _this.imageList.push(json); $file.append(''); } else { $file.find('.error').text(json.state).show(); } } catch (e) { $file.find('.error').text(lang.errorServerUpload).show(); } }); uploader.on('uploadError', function (file, code) { }); uploader.on('error', function (code, file) { if (code == 'Q_TYPE_DENIED' || code == 'F_EXCEED_SIZE') { addFile(file); } }); uploader.on('uploadComplete', function (file, ret) { }); $upload.on('click', function () { if ($(this).hasClass('disabled')) { return false; } if (state === 'ready') { uploader.upload(); } else if (state === 'paused') { uploader.upload(); } else if (state === 'uploading') { uploader.stop(); } }); $upload.addClass('state-' + state); updateTotalProgress(); }, getQueueCount: function () { var file, i, status, readyFile = 0, files = this.uploader.getFiles(); for (i = 0; file = files[i++]; ) { status = file.getStatus(); if (status == 'queued' || status == 'uploading' || status == 'progress') readyFile++; } return readyFile; }, destroy: function () { this.$wrap.remove(); }, getInsertList: function () { var i, data, list = [], align = getAlign(), prefix = editor.getOpt('imageUrlPrefix'); for (i = 0; i < this.imageList.length; i++) { data = this.imageList[i]; list.push({ src: prefix + data.url, _src: prefix + data.url, title: data.title, alt: data.original, floatStyle: align }); } return list; } }; /* 在线图片 */ function OnlineImage(target) { this.container = utils.isString(target) ? document.getElementById(target) : target; this.init(); } OnlineImage.prototype = { init: function () { this.reset(); this.initEvents(); }, /* 初始化容器 */ initContainer: function () { this.container.innerHTML = ''; this.list = document.createElement('ul'); this.clearFloat = document.createElement('li'); domUtils.addClass(this.list, 'list'); domUtils.addClass(this.clearFloat, 'clearFloat'); this.list.appendChild(this.clearFloat); this.container.appendChild(this.list); }, /* 初始化滚动事件,滚动到地步自动拉取数据 */ initEvents: function () { var _this = this; /* 滚动拉取图片 */ domUtils.on($G('imageList'), 'scroll', function(e){ var panel = this; if (panel.scrollHeight - (panel.offsetHeight + panel.scrollTop) < 10) { _this.getImageData(); } }); /* 选中图片 */ domUtils.on(this.container, 'click', function (e) { var target = e.target || e.srcElement, li = target.parentNode; if (li.tagName.toLowerCase() == 'li') { if (domUtils.hasClass(li, 'selected')) { domUtils.removeClasses(li, 'selected'); } else { domUtils.addClass(li, 'selected'); } } }); }, /* 初始化第一次的数据 */ initData: function () { /* 拉取数据需要使用的值 */ this.state = 0; this.listSize = editor.getOpt('imageManagerListSize'); this.listIndex = 0; this.listEnd = false; /* 第一次拉取数据 */ this.getImageData(); }, /* 重置界面 */ reset: function() { this.initContainer(); this.initData(); }, /* 向后台拉取图片列表数据 */ getImageData: function () { var _this = this; if(!_this.listEnd && !this.isLoadingData) { this.isLoadingData = true; var url = editor.getActionUrl(editor.getOpt('imageManagerActionName')), isJsonp = utils.isCrossDomainUrl(url); ajax.request(url, { 'timeout': 100000, 'dataType': isJsonp ? 'jsonp':'', 'data': utils.extend({ start: this.listIndex, size: this.listSize }, editor.queryCommandValue('serverparam')), 'method': 'get', 'onsuccess': function (r) { try { var json = isJsonp ? r:eval('(' + r.responseText + ')'); if (json.state == 'SUCCESS') { _this.pushData(json.list); _this.listIndex = parseInt(json.start) + parseInt(json.list.length); if(_this.listIndex >= json.total) { _this.listEnd = true; } _this.isLoadingData = false; } } catch (e) { if(r.responseText.indexOf('ue_separate_ue') != -1) { var list = r.responseText.split(r.responseText); _this.pushData(list); _this.listIndex = parseInt(list.length); _this.listEnd = true; _this.isLoadingData = false; } } }, 'onerror': function () { _this.isLoadingData = false; } }); } }, /* 添加图片到列表界面上 */ pushData: function (list) { var i, item, img, icon, _this = this, urlPrefix = editor.getOpt('imageManagerUrlPrefix'); for (i = 0; i < list.length; i++) { if(list[i] && list[i].url) { item = document.createElement('li'); img = document.createElement('img'); icon = document.createElement('span'); domUtils.on(img, 'load', (function(image){ return function(){ _this.scale(image, image.parentNode.offsetWidth, image.parentNode.offsetHeight); } })(img)); img.width = 113; img.setAttribute('src', urlPrefix + list[i].url + (list[i].url.indexOf('?') == -1 ? '?noCache=':'&noCache=') + (+new Date()).toString(36) ); img.setAttribute('_src', urlPrefix + list[i].url); domUtils.addClass(icon, 'icon'); item.appendChild(img); item.appendChild(icon); this.list.insertBefore(item, this.clearFloat); } } }, /* 改变图片大小 */ scale: function (img, w, h, type) { var ow = img.width, oh = img.height; if (type == 'justify') { if (ow >= oh) { img.width = w; img.height = h * oh / ow; img.style.marginLeft = '-' + parseInt((img.width - w) / 2) + 'px'; } else { img.width = w * ow / oh; img.height = h; img.style.marginTop = '-' + parseInt((img.height - h) / 2) + 'px'; } } else { if (ow >= oh) { img.width = w * ow / oh; img.height = h; img.style.marginLeft = '-' + parseInt((img.width - w) / 2) + 'px'; } else { img.width = w; img.height = h * oh / ow; img.style.marginTop = '-' + parseInt((img.height - h) / 2) + 'px'; } } }, getInsertList: function () { var i, lis = this.list.children, list = [], align = getAlign(); for (i = 0; i < lis.length; i++) { if (domUtils.hasClass(lis[i], 'selected')) { var img = lis[i].firstChild, src = img.getAttribute('_src'); list.push({ src: src, _src: src, alt: src.substr(src.lastIndexOf('/') + 1), floatStyle: align }); } } return list; } }; /*搜索图片 */ function SearchImage() { this.init(); } SearchImage.prototype = { init: function () { this.initEvents(); }, initEvents: function(){ var _this = this; /* 点击搜索按钮 */ domUtils.on($G('searchBtn'), 'click', function(){ var key = $G('searchTxt').value; if(key && key != lang.searchRemind) { _this.getImageData(); } }); /* 点击清除妞 */ domUtils.on($G('searchReset'), 'click', function(){ $G('searchTxt').value = lang.searchRemind; $G('searchListUl').innerHTML = ''; $G('searchType').selectedIndex = 0; }); /* 搜索框聚焦 */ domUtils.on($G('searchTxt'), 'focus', function(){ var key = $G('searchTxt').value; if(key && key == lang.searchRemind) { $G('searchTxt').value = ''; } }); /* 搜索框回车键搜索 */ domUtils.on($G('searchTxt'), 'keydown', function(e){ var keyCode = e.keyCode || e.which; if (keyCode == 13) { $G('searchBtn').click(); } }); /* 选中图片 */ domUtils.on($G('searchList'), 'click', function(e){ var target = e.target || e.srcElement, li = target.parentNode.parentNode; if (li.tagName.toLowerCase() == 'li') { if (domUtils.hasClass(li, 'selected')) { domUtils.removeClasses(li, 'selected'); } else { domUtils.addClass(li, 'selected'); } } }); }, encodeToGb2312:function (str){ if(!str) return ''; var strOut = "", z = 'D2BBB6A18140C6DF814181428143CDF2D5C9C8FDC9CFCFC2D8A2B2BBD3EB8144D8A4B3F38145D7A8C7D2D8A7CAC08146C7F0B1FBD2B5B4D4B6ABCBBFD8A9814781488149B6AA814AC1BDD1CF814BC9A5D8AD814CB8F6D1BEE3DCD6D0814D814EB7E1814FB4AE8150C1D98151D8BC8152CDE8B5A4CEAAD6F78153C0F6BED9D8AF815481558156C4CB8157BEC38158D8B1C3B4D2E58159D6AECEDAD5A7BAF5B7A6C0D6815AC6B9C5D2C7C7815BB9D4815CB3CBD2D2815D815ED8BFBEC5C6F2D2B2CFB0CFE7815F816081618162CAE981638164D8C081658166816781688169816AC2F2C2D2816BC8E9816C816D816E816F817081718172817381748175C7AC8176817781788179817A817B817CC1CB817DD3E8D5F9817ECAC2B6FED8A1D3DABFF78180D4C6BBA5D8C1CEE5BEAE81818182D8A88183D1C7D0A9818481858186D8BDD9EFCDF6BFBA8187BDBBBAA5D2E0B2FABAE0C4B68188CFEDBEA9CDA4C1C18189818A818BC7D7D9F1818CD9F4818D818E818F8190C8CBD8E9819181928193D2DACAB2C8CAD8ECD8EAD8C6BDF6C6CDB3F08194D8EBBDF1BDE98195C8D4B4D381968197C2D88198B2D6D7D0CACBCBFBD5CCB8B6CFC98199819A819BD9DAD8F0C7AA819CD8EE819DB4FAC1EED2D4819E819FD8ED81A0D2C7D8EFC3C781A181A281A3D1F681A4D6D9D8F281A5D8F5BCFEBCDB81A681A781A8C8CE81A9B7DD81AAB7C281ABC6F381AC81AD81AE81AF81B081B181B2D8F8D2C181B381B4CEE9BCBFB7FCB7A5D0DD81B581B681B781B881B9D6DAD3C5BBEFBBE1D8F181BA81BBC9A1CEB0B4AB81BCD8F381BDC9CBD8F6C2D7D8F781BE81BFCEB1D8F981C081C181C2B2AEB9C081C3D9A381C4B0E981C5C1E681C6C9EC81C7CBC581C8CBC6D9A481C981CA81CB81CC81CDB5E881CE81CFB5AB81D081D181D281D381D481D5CEBBB5CDD7A1D7F4D3D381D6CCE581D7BACE81D8D9A2D9DCD3E0D8FDB7F0D7F7D8FED8FAD9A1C4E381D981DAD3B6D8F4D9DD81DBD8FB81DCC5E581DD81DEC0D081DF81E0D1F0B0DB81E181E2BCD1D9A681E3D9A581E481E581E681E7D9ACD9AE81E8D9ABCAB981E981EA81EBD9A9D6B681EC81ED81EEB3DED9A881EFC0FD81F0CACC81F1D9AA81F2D9A781F381F4D9B081F581F6B6B181F781F881F9B9A981FAD2C081FB81FCCFC081FD81FEC2C28240BDC4D5ECB2E0C7C8BFEBD9AD8241D9AF8242CEEABAEE82438244824582468247C7D682488249824A824B824C824D824E824F8250B1E3825182528253B4D9B6EDD9B48254825582568257BFA182588259825AD9DEC7CEC0FED9B8825B825C825D825E825FCBD7B7FD8260D9B58261D9B7B1A3D3E1D9B98262D0C58263D9B682648265D9B18266D9B2C1A9D9B382678268BCF3D0DEB8A98269BEE3826AD9BD826B826C826D826ED9BA826FB0B3827082718272D9C28273827482758276827782788279827A827B827C827D827E8280D9C4B1B68281D9BF82828283B5B98284BEF3828582868287CCC8BAF2D2D08288D9C38289828ABDE8828BB3AB828C828D828ED9C5BEEB828FD9C6D9BBC4DF8290D9BED9C1D9C0829182928293829482958296829782988299829A829BD5AE829CD6B5829DC7E3829E829F82A082A1D9C882A282A382A4BCD9D9CA82A582A682A7D9BC82A8D9CBC6AB82A982AA82AB82AC82ADD9C982AE82AF82B082B1D7F682B2CDA382B382B482B582B682B782B882B982BABDA182BB82BC82BD82BE82BF82C0D9CC82C182C282C382C482C582C682C782C882C9C5BCCDB582CA82CB82CCD9CD82CD82CED9C7B3A5BFFE82CF82D082D182D2B8B582D382D4C0FC82D582D682D782D8B0F882D982DA82DB82DC82DD82DE82DF82E082E182E282E382E482E582E682E782E882E982EA82EB82EC82EDB4F682EED9CE82EFD9CFB4A2D9D082F082F1B4DF82F282F382F482F582F6B0C182F782F882F982FA82FB82FC82FDD9D1C9B582FE8340834183428343834483458346834783488349834A834B834C834D834E834F83508351CFF1835283538354835583568357D9D283588359835AC1C5835B835C835D835E835F836083618362836383648365D9D6C9AE8366836783688369D9D5D9D4D9D7836A836B836C836DCBDB836EBDA9836F8370837183728373C6A7837483758376837783788379837A837B837C837DD9D3D9D8837E83808381D9D9838283838384838583868387C8E583888389838A838B838C838D838E838F839083918392839383948395C0DC8396839783988399839A839B839C839D839E839F83A083A183A283A383A483A583A683A783A883A983AA83AB83AC83AD83AE83AF83B083B183B2B6F9D8A3D4CA83B3D4AAD0D6B3E4D5D783B4CFC8B9E283B5BFCB83B6C3E283B783B883B9B6D283BA83BBCDC3D9EED9F083BC83BD83BEB5B383BFB6B583C083C183C283C383C4BEA483C583C6C8EB83C783C8C8AB83C983CAB0CBB9ABC1F9D9E283CBC0BCB9B283CCB9D8D0CBB1F8C6E4BEDFB5E4D7C883CDD1F8BCE6CADE83CE83CFBCBDD9E6D8E783D083D1C4DA83D283D3B8D4C8BD83D483D5B2E1D4D983D683D783D883D9C3B083DA83DBC3E1DAA2C8DF83DCD0B483DDBEFCC5A983DE83DF83E0B9DA83E1DAA383E2D4A9DAA483E383E483E583E683E7D9FBB6AC83E883E9B7EBB1F9D9FCB3E5BEF683EABFF6D2B1C0E483EB83EC83EDB6B3D9FED9FD83EE83EFBEBB83F083F183F2C6E083F3D7BCDAA183F4C1B983F5B5F2C1E883F683F7BCF583F8B4D583F983FA83FB83FC83FD83FE844084418442C1DD8443C4FD84448445BCB8B7B284468447B7EF84488449844A844B844C844DD9EC844EC6BE844FBFADBBCB84508451B5CA8452DBC9D0D78453CDB9B0BCB3F6BBF7DBCABAAF8454D4E4B5B6B5F3D8D6C8D084558456B7D6C7D0D8D78457BFAF84588459DBBBD8D8845A845BD0CCBBAE845C845D845EEBBEC1D0C1F5D4F2B8D5B4B4845FB3F584608461C9BE846284638464C5D0846584668467C5D9C0FB8468B1F08469D8D9B9CE846AB5BD846B846CD8DA846D846ED6C6CBA2C8AFC9B2B4CCBFCC846FB9F48470D8DBD8DCB6E7BCC1CCEA847184728473847484758476CFF78477D8DDC7B084788479B9D0BDA3847A847BCCDE847CC6CA847D847E848084818482D8E08483D8DE84848485D8DF848684878488B0FE8489BEE7848ACAA3BCF4848B848C848D848EB8B1848F8490B8EE849184928493849484958496849784988499849AD8E2849BBDCB849CD8E4D8E3849D849E849F84A084A1C5FC84A284A384A484A584A684A784A8D8E584A984AAD8E684AB84AC84AD84AE84AF84B084B1C1A684B2C8B0B0ECB9A6BCD3CEF1DBBDC1D384B384B484B584B6B6AFD6FAC5ACBDD9DBBEDBBF84B784B884B9C0F8BEA2C0CD84BA84BB84BC84BD84BE84BF84C084C184C284C3DBC0CAC684C484C584C6B2AA84C784C884C9D3C284CAC3E384CBD1AB84CC84CD84CE84CFDBC284D0C0D584D184D284D3DBC384D4BFB184D584D684D784D884D984DAC4BC84DB84DC84DD84DEC7DA84DF84E084E184E284E384E484E584E684E784E884E9DBC484EA84EB84EC84ED84EE84EF84F084F1D9E8C9D784F284F384F4B9B4CEF0D4C884F584F684F784F8B0FCB4D284F9D0D984FA84FB84FC84FDD9E984FEDECBD9EB8540854185428543D8B0BBAFB1B18544B3D7D8CE85458546D4D185478548BDB3BFEF8549CFBB854A854BD8D0854C854D854EB7CB854F85508551D8D185528553855485558556855785588559855A855BC6A5C7F8D2BD855C855DD8D2C4E4855ECAAE855FC7A78560D8A68561C9FDCEE7BBDCB0EB856285638564BBAAD0AD8565B1B0D7E4D7BF8566B5A5C2F4C4CF85678568B2A98569B2B7856AB1E5DFB2D5BCBFA8C2ACD8D5C2B1856BD8D4CED4856CDAE0856DCEC0856E856FD8B4C3AED3A1CEA38570BCB4C8B4C2D18571BEEDD0B68572DAE18573857485758576C7E485778578B3A78579B6F2CCFCC0FA857A857BC0F7857CD1B9D1E1D8C7857D857E85808581858285838584B2DE85858586C0E58587BAF185888589D8C8858AD4AD858B858CCFE1D8C9858DD8CACFC3858EB3F8BEC7858F859085918592D8CB8593859485958596859785988599DBCC859A859B859C859DC8A5859E859F85A0CFD885A1C8FEB2CE85A285A385A485A585A6D3D6B2E6BCB0D3D1CBABB7B485A785A885A9B7A285AA85ABCAE585ACC8A1CADCB1E4D0F085ADC5D185AE85AF85B0DBC5B5FE85B185B2BFDAB9C5BEE4C1ED85B3DFB6DFB5D6BBBDD0D5D9B0C8B6A3BFC9CCA8DFB3CAB7D3D285B4D8CFD2B6BAC5CBBECCBE85B5DFB7B5F0DFB485B685B785B8D3F585B9B3D4B8F785BADFBA85BBBACFBCAAB5F585BCCDACC3FBBAF3C0F4CDC2CFF2DFB8CFC585BDC2C0DFB9C2F085BE85BF85C0BEFD85C1C1DFCDCCD2F7B7CDDFC185C2DFC485C385C4B7F1B0C9B6D6B7D485C5BAACCCFDBFD4CBB1C6F485C6D6A8DFC585C7CEE2B3B385C885C9CEFCB4B585CACEC7BAF085CBCEE185CCD1BD85CD85CEDFC085CF85D0B4F485D1B3CA85D2B8E6DFBB85D385D485D585D6C4C585D7DFBCDFBDDFBEC5BBDFBFDFC2D4B1DFC385D8C7BACED885D985DA85DB85DC85DDC4D885DEDFCA85DFDFCF85E0D6DC85E185E285E385E485E585E685E785E8DFC9DFDACEB685E9BAC7DFCEDFC8C5DE85EA85EBC9EBBAF4C3FC85EC85EDBED785EEDFC685EFDFCD85F0C5D885F185F285F385F4D5A6BACD85F5BECCD3BDB8C085F6D6E485F7DFC7B9BEBFA785F885F9C1FCDFCBDFCC85FADFD085FB85FC85FD85FE8640DFDBDFE58641DFD7DFD6D7C9DFE3DFE4E5EBD2A7DFD28642BFA98643D4DB8644BFC8DFD4864586468647CFCC86488649DFDD864AD1CA864BDFDEB0A7C6B7DFD3864CBAE5864DB6DFCDDBB9FED4D5864E864FDFDFCFECB0A5DFE7DFD1D1C6DFD5DFD8DFD9DFDC8650BBA98651DFE0DFE18652DFE2DFE6DFE8D3B486538654865586568657B8E7C5B6DFEAC9DAC1A8C4C486588659BFDECFF8865A865B865CD5DCDFEE865D865E865F866086618662B2B88663BADFDFEC8664DBC18665D1E48666866786688669CBF4B4BD866AB0A6866B866C866D866E866FDFF1CCC6DFF286708671DFED867286738674867586768677DFE986788679867A867BDFEB867CDFEFDFF0BBBD867D867EDFF386808681DFF48682BBA38683CADBCEA8E0A7B3AA8684E0A6868586868687E0A186888689868A868BDFFE868CCDD9DFFC868DDFFA868EBFD0D7C4868FC9CC86908691DFF8B0A186928693869486958696DFFD869786988699869ADFFBE0A2869B869C869D869E869FE0A886A086A186A286A3B7C886A486A5C6A1C9B6C0B2DFF586A686A7C5BE86A8D8C4DFF9C4F686A986AA86AB86AC86AD86AEE0A3E0A4E0A5D0A586AF86B0E0B4CCE486B1E0B186B2BFA6E0AFCEB9E0ABC9C686B386B4C0AEE0AEBAEDBAB0E0A986B586B686B7DFF686B8E0B386B986BAE0B886BB86BC86BDB4ADE0B986BE86BFCFB2BAC886C0E0B086C186C286C386C486C586C686C7D0FA86C886C986CA86CB86CC86CD86CE86CF86D0E0AC86D1D4FB86D2DFF786D3C5E786D4E0AD86D5D3F786D6E0B6E0B786D786D886D986DA86DBE0C4D0E186DC86DD86DEE0BC86DF86E0E0C9E0CA86E186E286E3E0BEE0AAC9A4E0C186E4E0B286E586E686E786E886E9CAC8E0C386EAE0B586EBCECB86ECCBC3E0CDE0C6E0C286EDE0CB86EEE0BAE0BFE0C086EF86F0E0C586F186F2E0C7E0C886F3E0CC86F4E0BB86F586F686F786F886F9CBD4E0D586FAE0D6E0D286FB86FC86FD86FE87408741E0D0BCCE87428743E0D18744B8C2D8C587458746874787488749874A874B874CD0EA874D874EC2EF874F8750E0CFE0BD875187528753E0D4E0D387548755E0D78756875787588759E0DCE0D8875A875B875CD6F6B3B0875DD7EC875ECBBB875F8760E0DA8761CEFB876287638764BAD987658766876787688769876A876B876C876D876E876F8770E0E1E0DDD2AD87718772877387748775E0E287768777E0DBE0D9E0DF87788779E0E0877A877B877C877D877EE0DE8780E0E4878187828783C6F7D8ACD4EBE0E6CAC98784878587868787E0E587888789878A878BB8C1878C878D878E878FE0E7E0E887908791879287938794879587968797E0E9E0E387988799879A879B879C879D879EBABFCCE7879F87A087A1E0EA87A287A387A487A587A687A787A887A987AA87AB87AC87AD87AE87AF87B0CFF987B187B287B387B487B587B687B787B887B987BA87BBE0EB87BC87BD87BE87BF87C087C187C2C8C287C387C487C587C6BDC087C787C887C987CA87CB87CC87CD87CE87CF87D087D187D287D3C4D287D487D587D687D787D887D987DA87DB87DCE0EC87DD87DEE0ED87DF87E0C7F4CBC487E1E0EEBBD8D8B6D2F2E0EFCDC587E2B6DA87E387E487E587E687E787E8E0F187E9D4B087EA87EBC0A7B4D187EC87EDCEA7E0F087EE87EF87F0E0F2B9CC87F187F2B9FACDBCE0F387F387F487F5C6D4E0F487F6D4B287F7C8A6E0F6E0F587F887F987FA87FB87FC87FD87FE8840884188428843884488458846884788488849E0F7884A884BCDC1884C884D884ECAA5884F885088518852D4DADBD7DBD98853DBD8B9E7DBDCDBDDB5D888548855DBDA8856885788588859885ADBDBB3A1DBDF885B885CBBF8885DD6B7885EDBE0885F886088618862BEF988638864B7BB8865DBD0CCAEBFB2BBB5D7F8BFD38866886788688869886ABFE9886B886CBCE1CCB3DBDEB0D3CEEBB7D8D7B9C6C2886D886EC0A4886FCCB98870DBE7DBE1C6BADBE38871DBE88872C5F7887388748875DBEA88768877DBE9BFC088788879887ADBE6DBE5887B887C887D887E8880B4B9C0ACC2A2DBE2DBE48881888288838884D0CDDBED88858886888788888889C0DDDBF2888A888B888C888D888E888F8890B6E28891889288938894DBF3DBD2B9B8D4ABDBEC8895BFD1DBF08896DBD18897B5E68898DBEBBFE58899889A889BDBEE889CDBF1889D889E889FDBF988A088A188A288A388A488A588A688A788A8B9A1B0A388A988AA88AB88AC88AD88AE88AFC2F188B088B1B3C7DBEF88B288B3DBF888B4C6D2DBF488B588B6DBF5DBF7DBF688B788B8DBFE88B9D3F2B2BA88BA88BB88BCDBFD88BD88BE88BF88C088C188C288C388C4DCA488C5DBFB88C688C788C888C9DBFA88CA88CB88CCDBFCC5E0BBF988CD88CEDCA388CF88D0DCA588D1CCC388D288D388D4B6D1DDC088D588D688D7DCA188D8DCA288D988DA88DBC7B588DC88DD88DEB6E988DF88E088E1DCA788E288E388E488E5DCA688E6DCA9B1A488E788E8B5CC88E988EA88EB88EC88EDBFB088EE88EF88F088F188F2D1DF88F388F488F588F6B6C288F788F888F988FA88FB88FC88FD88FE894089418942894389448945DCA88946894789488949894A894B894CCBFAEBF3894D894E894FCBDC89508951CBFE895289538954CCC189558956895789588959C8FB895A895B895C895D895E895FDCAA89608961896289638964CCEEDCAB89658966896789688969896A896B896C896D896E896F897089718972897389748975DBD38976DCAFDCAC8977BEB38978CAFB8979897A897BDCAD897C897D897E89808981898289838984C9CAC4B989858986898789888989C7BDDCAE898A898B898CD4F6D0E6898D898E898F89908991899289938994C4ABB6D589958996899789988999899A899B899C899D899E899F89A089A189A289A389A489A589A6DBD489A789A889A989AAB1DA89AB89AC89ADDBD589AE89AF89B089B189B289B389B489B589B689B789B8DBD689B989BA89BBBABE89BC89BD89BE89BF89C089C189C289C389C489C589C689C789C889C9C8C089CA89CB89CC89CD89CE89CFCABFC8C989D0D7B389D1C9F989D289D3BFC789D489D5BAF889D689D7D2BC89D889D989DA89DB89DC89DD89DE89DFE2BA89E0B4A689E189E2B1B889E389E489E589E689E7B8B489E8CFC489E989EA89EB89ECD9E7CFA6CDE289ED89EED9EDB6E089EFD2B989F089F1B9BB89F289F389F489F5E2B9E2B789F6B4F389F7CCECCCABB7F289F8D8B2D1EBBABB89F9CAA789FA89FBCDB789FC89FDD2C4BFE4BCD0B6E189FEDEC58A408A418A428A43DEC6DBBC8A44D1D98A458A46C6E6C4CEB7EE8A47B7DC8A488A49BFFCD7E08A4AC6F58A4B8A4CB1BCDEC8BDB1CCD7DECA8A4DDEC98A4E8A4F8A508A518A52B5EC8A53C9DD8A548A55B0C28A568A578A588A598A5A8A5B8A5C8A5D8A5E8A5F8A608A618A62C5AEC5AB8A63C4CC8A64BCE9CBFD8A658A668A67BAC38A688A698A6AE5F9C8E7E5FACDFD8A6BD7B1B8BEC2E88A6CC8D18A6D8A6EE5FB8A6F8A708A718A72B6CABCCB8A738A74D1FDE6A18A75C3EE8A768A778A788A79E6A48A7A8A7B8A7C8A7DE5FEE6A5CDD78A7E8A80B7C1E5FCE5FDE6A38A818A82C4DDE6A88A838A84E6A78A858A868A878A888A898A8AC3C38A8BC6DE8A8C8A8DE6AA8A8E8A8F8A908A918A928A938A94C4B78A958A968A97E6A2CABC8A988A998A9A8A9BBDE3B9C3E6A6D0D5CEAF8A9C8A9DE6A9E6B08A9ED2A68A9FBDAAE6AD8AA08AA18AA28AA38AA4E6AF8AA5C0D18AA68AA7D2CC8AA88AA98AAABCA78AAB8AAC8AAD8AAE8AAF8AB08AB18AB28AB38AB48AB58AB6E6B18AB7D2F68AB88AB98ABAD7CB8ABBCDFE8ABCCDDEC2A6E6ABE6ACBDBFE6AEE6B38ABD8ABEE6B28ABF8AC08AC18AC2E6B68AC3E6B88AC48AC58AC68AC7C4EF8AC88AC98ACAC4C88ACB8ACCBEEAC9EF8ACD8ACEE6B78ACFB6F08AD08AD18AD2C3E48AD38AD48AD58AD68AD78AD88AD9D3E9E6B48ADAE6B58ADBC8A28ADC8ADD8ADE8ADF8AE0E6BD8AE18AE28AE3E6B98AE48AE58AE68AE78AE8C6C58AE98AEACDF1E6BB8AEB8AEC8AED8AEE8AEF8AF08AF18AF28AF38AF4E6BC8AF58AF68AF78AF8BBE98AF98AFA8AFB8AFC8AFD8AFE8B40E6BE8B418B428B438B44E6BA8B458B46C0B78B478B488B498B4A8B4B8B4C8B4D8B4E8B4FD3A4E6BFC9F4E6C38B508B51E6C48B528B538B548B55D0F68B568B578B588B598B5A8B5B8B5C8B5D8B5E8B5F8B608B618B628B638B648B658B668B67C3BD8B688B698B6A8B6B8B6C8B6D8B6EC3C4E6C28B6F8B708B718B728B738B748B758B768B778B788B798B7A8B7B8B7CE6C18B7D8B7E8B808B818B828B838B84E6C7CFB18B85EBF48B868B87E6CA8B888B898B8A8B8B8B8CE6C58B8D8B8EBCDEC9A98B8F8B908B918B928B938B94BCB58B958B96CFD38B978B988B998B9A8B9BE6C88B9CE6C98B9DE6CE8B9EE6D08B9F8BA08BA1E6D18BA28BA38BA4E6CBB5D58BA5E6CC8BA68BA7E6CF8BA88BA9C4DB8BAAE6C68BAB8BAC8BAD8BAE8BAFE6CD8BB08BB18BB28BB38BB48BB58BB68BB78BB88BB98BBA8BBB8BBC8BBD8BBE8BBF8BC08BC18BC28BC38BC48BC58BC6E6D28BC78BC88BC98BCA8BCB8BCC8BCD8BCE8BCF8BD08BD18BD2E6D4E6D38BD38BD48BD58BD68BD78BD88BD98BDA8BDB8BDC8BDD8BDE8BDF8BE08BE18BE28BE38BE48BE58BE68BE78BE88BE98BEA8BEB8BECE6D58BEDD9F88BEE8BEFE6D68BF08BF18BF28BF38BF48BF58BF68BF7E6D78BF88BF98BFA8BFB8BFC8BFD8BFE8C408C418C428C438C448C458C468C47D7D3E6DD8C48E6DEBFD7D4D08C49D7D6B4E6CBEFE6DAD8C3D7CED0A28C4AC3CF8C4B8C4CE6DFBCBEB9C2E6DBD1A78C4D8C4EBAA2C2CF8C4FD8AB8C508C518C52CAEBE5EE8C53E6DC8C54B7F58C558C568C578C58C8E68C598C5AC4F58C5B8C5CE5B2C4FE8C5DCBFCE5B3D5AC8C5ED3EECAD8B0B28C5FCBCECDEA8C608C61BAEA8C628C638C64E5B58C65E5B48C66D7DAB9D9D6E6B6A8CDF0D2CBB1A6CAB58C67B3E8C9F3BFCDD0FBCAD2E5B6BBC28C688C698C6ACFDCB9AC8C6B8C6C8C6D8C6ED4D78C6F8C70BAA6D1E7CFFCBCD28C71E5B7C8DD8C728C738C74BFEDB1F6CBDE8C758C76BCC58C77BCC4D2FAC3DCBFDC8C788C798C7A8C7BB8BB8C7C8C7D8C7EC3C28C80BAAED4A28C818C828C838C848C858C868C878C888C89C7DEC4AFB2EC8C8AB9D18C8B8C8CE5BBC1C88C8D8C8ED5AF8C8F8C908C918C928C93E5BC8C94E5BE8C958C968C978C988C998C9A8C9BB4E7B6D4CBC2D1B0B5BC8C9C8C9DCAD98C9EB7E28C9F8CA0C9E48CA1BDAB8CA28CA3CEBED7F08CA48CA58CA68CA7D0A18CA8C9D98CA98CAAB6FBE6D8BCE28CABB3BE8CACC9D08CADE6D9B3A28CAE8CAF8CB08CB1DECC8CB2D3C8DECD8CB3D2A28CB48CB58CB68CB7DECE8CB88CB98CBA8CBBBECD8CBC8CBDDECF8CBE8CBF8CC0CAACD2FCB3DFE5EAC4E1BEA1CEB2C4F2BED6C6A8B2E38CC18CC2BED38CC38CC4C7FCCCEBBDECCEDD8CC58CC6CABAC6C1E5ECD0BC8CC78CC88CC9D5B98CCA8CCB8CCCE5ED8CCD8CCE8CCF8CD0CAF48CD1CDC0C2C58CD2E5EF8CD3C2C4E5F08CD48CD58CD68CD78CD88CD98CDAE5F8CDCD8CDBC9BD8CDC8CDD8CDE8CDF8CE08CE18CE2D2D9E1A88CE38CE48CE58CE6D3EC8CE7CBEAC6F18CE88CE98CEA8CEB8CECE1AC8CED8CEE8CEFE1A7E1A98CF08CF1E1AAE1AF8CF28CF3B2ED8CF4E1ABB8DAE1ADE1AEE1B0B5BAE1B18CF58CF68CF78CF88CF9E1B3E1B88CFA8CFB8CFC8CFD8CFED1D28D40E1B6E1B5C1EB8D418D428D43E1B78D44D4C08D45E1B28D46E1BAB0B68D478D488D498D4AE1B48D4BBFF98D4CE1B98D4D8D4EE1BB8D4F8D508D518D528D538D54E1BE8D558D568D578D588D598D5AE1BC8D5B8D5C8D5D8D5E8D5F8D60D6C58D618D628D638D648D658D668D67CFBF8D688D69E1BDE1BFC2CD8D6AB6EB8D6BD3F88D6C8D6DC7CD8D6E8D6FB7E58D708D718D728D738D748D758D768D778D788D79BEFE8D7A8D7B8D7C8D7D8D7E8D80E1C0E1C18D818D82E1C7B3E78D838D848D858D868D878D88C6E98D898D8A8D8B8D8C8D8DB4DE8D8ED1C28D8F8D908D918D92E1C88D938D94E1C68D958D968D978D988D99E1C58D9AE1C3E1C28D9BB1C08D9C8D9D8D9ED5B8E1C48D9F8DA08DA18DA28DA3E1CB8DA48DA58DA68DA78DA88DA98DAA8DABE1CCE1CA8DAC8DAD8DAE8DAF8DB08DB18DB28DB3EFFA8DB48DB5E1D3E1D2C7B68DB68DB78DB88DB98DBA8DBB8DBC8DBD8DBE8DBF8DC0E1C98DC18DC2E1CE8DC3E1D08DC48DC58DC68DC78DC88DC98DCA8DCB8DCC8DCD8DCEE1D48DCFE1D1E1CD8DD08DD1E1CF8DD28DD38DD48DD5E1D58DD68DD78DD88DD98DDA8DDB8DDC8DDD8DDE8DDF8DE08DE18DE2E1D68DE38DE48DE58DE68DE78DE88DE98DEA8DEB8DEC8DED8DEE8DEF8DF08DF18DF28DF38DF48DF58DF68DF78DF8E1D78DF98DFA8DFBE1D88DFC8DFD8DFE8E408E418E428E438E448E458E468E478E488E498E4A8E4B8E4C8E4D8E4E8E4F8E508E518E528E538E548E55E1DA8E568E578E588E598E5A8E5B8E5C8E5D8E5E8E5F8E608E618E62E1DB8E638E648E658E668E678E688E69CEA18E6A8E6B8E6C8E6D8E6E8E6F8E708E718E728E738E748E758E76E7DD8E77B4A8D6DD8E788E79D1B2B3B28E7A8E7BB9A4D7F3C7C9BEDEB9AE8E7CCED78E7D8E7EB2EEDBCF8E80BCBAD2D1CBC8B0CD8E818E82CFEF8E838E848E858E868E87D9E3BDED8E888E89B1D2CAD0B2BC8E8ACBA7B7AB8E8BCAA68E8C8E8D8E8ECFA38E8F8E90E0F8D5CAE0FB8E918E92E0FAC5C1CCFB8E93C1B1E0F9D6E3B2AFD6C4B5DB8E948E958E968E978E988E998E9A8E9BB4F8D6A18E9C8E9D8E9E8E9F8EA0CFAFB0EF8EA18EA2E0FC8EA38EA48EA58EA68EA7E1A1B3A38EA88EA9E0FDE0FEC3B18EAA8EAB8EAC8EADC3DD8EAEE1A2B7F98EAF8EB08EB18EB28EB38EB4BBCF8EB58EB68EB78EB88EB98EBA8EBBE1A3C4BB8EBC8EBD8EBE8EBF8EC0E1A48EC18EC2E1A58EC38EC4E1A6B4B18EC58EC68EC78EC88EC98ECA8ECB8ECC8ECD8ECE8ECF8ED08ED18ED28ED3B8C9C6BDC4EA8ED4B2A28ED5D0D28ED6E7DBBBC3D3D7D3C48ED7B9E3E2CF8ED88ED98EDAD7AF8EDBC7ECB1D38EDC8EDDB4B2E2D18EDE8EDF8EE0D0F2C2AEE2D08EE1BFE2D3A6B5D7E2D2B5EA8EE2C3EDB8FD8EE3B8AE8EE4C5D3B7CFE2D48EE58EE68EE78EE8E2D3B6C8D7F98EE98EEA8EEB8EEC8EEDCDA58EEE8EEF8EF08EF18EF2E2D88EF3E2D6CAFCBFB5D3B9E2D58EF48EF58EF68EF7E2D78EF88EF98EFA8EFB8EFC8EFD8EFE8F408F418F42C1AEC0C88F438F448F458F468F478F48E2DBE2DAC0AA8F498F4AC1CE8F4B8F4C8F4D8F4EE2DC8F4F8F508F518F528F538F548F558F568F578F588F598F5AE2DD8F5BE2DE8F5C8F5D8F5E8F5F8F608F618F628F638F64DBC88F65D1D3CDA28F668F67BDA88F688F698F6ADEC3D8A5BFAADBCDD2ECC6FAC5AA8F6B8F6C8F6DDEC48F6EB1D7DFAE8F6F8F708F71CABD8F72DFB18F73B9AD8F74D2FD8F75B8A5BAEB8F768F77B3DA8F788F798F7AB5DCD5C58F7B8F7C8F7D8F7EC3D6CFD2BBA18F80E5F3E5F28F818F82E5F48F83CDE48F84C8F58F858F868F878F888F898F8A8F8BB5AFC7BF8F8CE5F68F8D8F8E8F8FECB08F908F918F928F938F948F958F968F978F988F998F9A8F9B8F9C8F9D8F9EE5E68F9FB9E9B5B18FA0C2BCE5E8E5E7E5E98FA18FA28FA38FA4D2CD8FA58FA68FA7E1EAD0CE8FA8CDAE8FA9D1E58FAA8FABB2CAB1EB8FACB1F2C5ED8FAD8FAED5C3D3B08FAFE1DC8FB08FB18FB2E1DD8FB3D2DB8FB4B3B9B1CB8FB58FB68FB7CDF9D5F7E1DE8FB8BEB6B4FD8FB9E1DFBADCE1E0BBB2C2C9E1E18FBA8FBB8FBCD0EC8FBDCDBD8FBE8FBFE1E28FC0B5C3C5C7E1E38FC18FC2E1E48FC38FC48FC58FC6D3F98FC78FC88FC98FCA8FCB8FCCE1E58FCDD1AD8FCE8FCFE1E6CEA28FD08FD18FD28FD38FD48FD5E1E78FD6B5C28FD78FD88FD98FDAE1E8BBD58FDB8FDC8FDD8FDE8FDFD0C4E2E0B1D8D2E48FE08FE1E2E18FE28FE3BCC9C8CC8FE4E2E3ECFEECFDDFAF8FE58FE68FE7E2E2D6BECDFCC3A68FE88FE98FEAE3C38FEB8FECD6D2E2E78FED8FEEE2E88FEF8FF0D3C78FF18FF2E2ECBFEC8FF3E2EDE2E58FF48FF5B3C08FF68FF78FF8C4EE8FF98FFAE2EE8FFB8FFCD0C38FFDBAF6E2E9B7DEBBB3CCACCBCBE2E4E2E6E2EAE2EB8FFE90409041E2F790429043E2F4D4F5E2F390449045C5AD9046D5FAC5C2B2C090479048E2EF9049E2F2C1AFCBBC904A904BB5A1E2F9904C904D904EBCB1E2F1D0D4D4B9E2F5B9D6E2F6904F90509051C7D390529053905490559056E2F0905790589059905A905BD7DCEDA1905C905DE2F8905EEDA5E2FECAD1905F906090619062906390649065C1B59066BBD090679068BFD69069BAE3906A906BCBA1906C906D906EEDA6EDA3906F9070EDA29071907290739074BBD6EDA7D0F490759076EDA4BADEB6F7E3A1B6B2CCF1B9A79077CFA2C7A190789079BFD2907A907BB6F1907CE2FAE2FBE2FDE2FCC4D5E3A2907DD3C1907E90809081E3A7C7C49082908390849085CFA490869087E3A9BAB790889089908A908BE3A8908CBBDA908DE3A3908E908F9090E3A4E3AA9091E3A69092CEF2D3C690939094BBBC90959096D4C39097C4FA90989099EDA8D0FCE3A5909AC3F5909BE3ADB1AF909CE3B2909D909E909FBCC290A090A1E3ACB5BF90A290A390A490A590A690A790A890A9C7E9E3B090AA90AB90ACBEAACDEF90AD90AE90AF90B090B1BBF390B290B390B4CCE890B590B6E3AF90B7E3B190B8CFA7E3AE90B9CEA9BBDD90BA90BB90BC90BD90BEB5EBBEE5B2D2B3CD90BFB1B9E3ABB2D1B5ACB9DFB6E890C090C1CFEBE3B790C2BBCC90C390C4C8C7D0CA90C590C690C790C890C9E3B8B3EE90CA90CB90CC90CDEDA990CED3FAD3E490CF90D090D1EDAAE3B9D2E290D290D390D490D590D6E3B590D790D890D990DAD3DE90DB90DC90DD90DEB8D0E3B390DF90E0E3B6B7DF90E1E3B4C0A290E290E390E4E3BA90E590E690E790E890E990EA90EB90EC90ED90EE90EF90F090F190F290F390F490F590F690F7D4B890F890F990FA90FB90FC90FD90FE9140B4C89141E3BB9142BBC59143C9F791449145C9E5914691479148C4BD9149914A914B914C914D914E914FEDAB9150915191529153C2FD9154915591569157BBDBBFAE91589159915A915B915C915D915ECEBF915F916091619162E3BC9163BFB6916491659166916791689169916A916B916C916D916E916F9170917191729173917491759176B1EF91779178D4F79179917A917B917C917DE3BE917E9180918191829183918491859186EDAD918791889189918A918B918C918D918E918FE3BFBAA9EDAC91909191E3BD91929193919491959196919791989199919A919BE3C0919C919D919E919F91A091A1BAB691A291A391A4B6AE91A591A691A791A891A9D0B891AAB0C3EDAE91AB91AC91AD91AE91AFEDAFC0C191B0E3C191B191B291B391B491B591B691B791B891B991BA91BB91BC91BD91BE91BF91C091C1C5B391C291C391C491C591C691C791C891C991CA91CB91CC91CD91CE91CFE3C291D091D191D291D391D491D591D691D791D8DCB291D991DA91DB91DC91DD91DEEDB091DFB8EA91E0CEECEAA7D0E7CAF9C8D6CFB7B3C9CED2BDE491E191E2E3DEBBF2EAA8D5BD91E3C6DDEAA991E491E591E6EAAA91E7EAACEAAB91E8EAAEEAAD91E991EA91EB91ECBDD891EDEAAF91EEC2BE91EF91F091F191F2B4C1B4F791F391F4BBA791F591F691F791F891F9ECE6ECE5B7BFCBF9B1E291FAECE791FB91FC91FDC9C8ECE8ECE991FECAD6DED0B2C5D4FA92409241C6CBB0C7B4F2C8D3924292439244CDD092459246BFB8924792489249924A924B924C924DBFDB924E924FC7A4D6B49250C0A9DED1C9A8D1EFC5A4B0E7B3B6C8C592519252B0E292539254B7F692559256C5FA92579258B6F39259D5D2B3D0BCBC925A925B925CB3AD925D925E925F9260BEF1B0D1926192629263926492659266D2D6CAE3D7A59267CDB6B6B6BFB9D5DB9268B8A7C5D79269926A926BDED2BFD9C2D5C7C0926CBBA4B1A8926D926EC5EA926F9270C5FBCCA79271927292739274B1A7927592769277B5D692789279927AC4A8927BDED3D1BAB3E9927CC3F2927D927EB7F79280D6F4B5A3B2F0C4B4C4E9C0ADDED49281B0E8C5C4C1E09282B9D59283BEDCCDD8B0CE9284CDCFDED6BED0D7BEDED5D5D0B0DD92859286C4E292879288C2A3BCF09289D3B5C0B9C5A1B2A6D4F1928A928BC0A8CAC3DED7D5FC928CB9B0928DC8ADCBA9928EDED9BFBD928F929092919292C6B4D7A7CAB0C4C39293B3D6B9D29294929592969297D6B8EAFCB0B492989299929A929BBFE6929C929DCCF4929E929F92A092A1CDDA92A292A392A4D6BFC2CE92A5CECECCA2D0AEC4D3B5B2DED8D5F5BCB7BBD392A692A7B0A492A8C5B2B4EC92A992AA92ABD5F192AC92ADEAFD92AE92AF92B092B192B292B3DEDACDA692B492B5CDEC92B692B792B892B9CEE6DEDC92BACDB1C0A692BB92BCD7BD92BDDEDBB0C6BAB4C9D3C4F3BEE892BE92BF92C092C1B2B692C292C392C492C592C692C792C892C9C0CCCBF092CABCF1BBBBB5B792CB92CC92CDC5F592CEDEE692CF92D092D1DEE3BEDD92D292D3DEDF92D492D592D692D7B4B7BDDD92D892D9DEE0C4ED92DA92DB92DC92DDCFC692DEB5E092DF92E092E192E2B6DECADAB5F4DEE592E3D5C692E4DEE1CCCDC6FE92E5C5C592E692E792E8D2B492E9BEF292EA92EB92EC92ED92EE92EF92F0C2D392F1CCBDB3B892F2BDD392F3BFD8CDC6D1DAB4EB92F4DEE4DEDDDEE792F5EAFE92F692F7C2B0DEE292F892F9D6C0B5A792FAB2F492FBDEE892FCDEF292FD92FE934093419342DEED9343DEF193449345C8E0934693479348D7E1DEEFC3E8CCE19349B2E5934A934B934CD2BE934D934E934F9350935193529353DEEE9354DEEBCED59355B4A79356935793589359935ABFABBEBE935B935CBDD2935D935E935F9360DEE99361D4AE9362DEDE9363DEEA9364936593669367C0BF9368DEECB2F3B8E9C2A79369936ABDC1936B936C936D936E936FDEF5DEF893709371B2ABB4A493729373B4EAC9A6937493759376937793789379DEF6CBD1937AB8E3937BDEF7DEFA937C937D937E9380DEF9938193829383CCC29384B0E1B4EE93859386938793889389938AE5BA938B938C938D938E938FD0AF93909391B2EB9392EBA19393DEF493949395C9E3DEF3B0DAD2A1B1F79396CCAF939793989399939A939B939C939DDEF0939ECBA4939F93A093A1D5AA93A293A393A493A593A6DEFB93A793A893A993AA93AB93AC93AD93AEB4DD93AFC4A693B093B193B2DEFD93B393B493B593B693B793B893B993BA93BB93BCC3FEC4A1DFA193BD93BE93BF93C093C193C293C3C1CC93C4DEFCBEEF93C5C6B293C693C793C893C993CA93CB93CC93CD93CEB3C5C8F693CF93D0CBBADEFE93D193D2DFA493D393D493D593D6D7B293D793D893D993DA93DBB3B793DC93DD93DE93DFC1C393E093E1C7CBB2A5B4E993E2D7AB93E393E493E593E6C4EC93E7DFA2DFA393E8DFA593E9BAB393EA93EB93ECDFA693EDC0DE93EE93EFC9C393F093F193F293F393F493F593F6B2D9C7E693F7DFA793F8C7DC93F993FA93FB93FCDFA8EBA293FD93FE944094419442CBD3944394449445DFAA9446DFA99447B2C194489449944A944B944C944D944E944F9450945194529453945494559456945794589459945A945B945C945D945E945F9460C5CA94619462946394649465946694679468DFAB9469946A946B946C946D946E946F9470D4DC94719472947394749475C8C19476947794789479947A947B947C947D947E948094819482DFAC94839484948594869487BEF094889489DFADD6A7948A948B948C948DEAB7EBB6CAD5948ED8FCB8C4948FB9A594909491B7C5D5FE94929493949494959496B9CA94979498D0A7F4CD9499949AB5D0949B949CC3F4949DBEC8949E949F94A0EBB7B0BD94A194A2BDCC94A3C1B294A4B1D6B3A894A594A694A7B8D2C9A294A894A9B6D894AA94AB94AC94ADEBB8BEB494AE94AF94B0CAFD94B1C7C394B2D5FB94B394B4B7F394B594B694B794B894B994BA94BB94BC94BD94BE94BF94C094C194C294C3CEC494C494C594C6D5ABB1F394C794C894C9ECB3B0DF94CAECB594CB94CC94CDB6B794CEC1CF94CFF5FAD0B194D094D1D5E594D2CED394D394D4BDEFB3E294D5B8AB94D6D5B694D7EDBD94D8B6CF94D9CBB9D0C294DA94DB94DC94DD94DE94DF94E094E1B7BD94E294E3ECB6CAA994E494E594E6C5D494E7ECB9ECB8C2C3ECB794E894E994EA94EBD0FDECBA94ECECBBD7E594ED94EEECBC94EF94F094F1ECBDC6EC94F294F394F494F594F694F794F894F9CEDE94FABCC894FB94FCC8D5B5A9BEC9D6BCD4E794FD94FED1AED0F1EAB8EAB9EABABAB59540954195429543CAB1BFF595449545CDFA9546954795489549954AEAC0954BB0BAEABE954C954DC0A5954E954F9550EABB9551B2FD9552C3F7BBE8955395549555D2D7CEF4EABF955695579558EABC9559955A955BEAC3955CD0C7D3B3955D955E955F9560B4BA9561C3C1D7F29562956395649565D5D19566CAC79567EAC595689569EAC4EAC7EAC6956A956B956C956D956ED6E7956FCFD495709571EACB9572BBCE9573957495759576957795789579BDFAC9CE957A957BEACC957C957DC9B9CFFEEACAD4CEEACDEACF957E9580CDED9581958295839584EAC99585EACE95869587CEEE9588BBDE9589B3BF958A958B958C958D958EC6D5BEB0CEFA958F95909591C7E79592BEA7EAD095939594D6C7959595969597C1C095989599959AD4DD959BEAD1959C959DCFBE959E959F95A095A1EAD295A295A395A495A5CAEE95A695A795A895A9C5AFB0B595AA95AB95AC95AD95AEEAD495AF95B095B195B295B395B495B595B695B7EAD3F4DF95B895B995BA95BB95BCC4BA95BD95BE95BF95C095C1B1A995C295C395C495C5E5DF95C695C795C895C9EAD595CA95CB95CC95CD95CE95CF95D095D195D295D395D495D595D695D795D895D995DA95DB95DC95DD95DE95DF95E095E195E295E3CAEF95E4EAD6EAD7C6D895E595E695E795E895E995EA95EB95ECEAD895ED95EEEAD995EF95F095F195F295F395F4D4BB95F5C7FAD2B7B8FC95F695F7EAC295F8B2DC95F995FAC2FC95FBD4F8CCE6D7EE95FC95FD95FE9640964196429643D4C2D3D0EBC3C5F39644B7FE96459646EBD4964796489649CBB7EBDE964AC0CA964B964C964DCDFB964EB3AF964FC6DA965096519652965396549655EBFC9656C4BE9657CEB4C4A9B1BED4FD9658CAF59659D6EC965A965BC6D3B6E4965C965D965E965FBBFA96609661D0E096629663C9B19664D4D3C8A896659666B8CB9667E8BEC9BC96689669E8BB966AC0EED0D3B2C4B4E5966BE8BC966C966DD5C8966E966F967096719672B6C59673E8BDCAF8B8DCCCF5967496759676C0B496779678D1EEE8BFE8C29679967ABABC967BB1ADBDDC967CEABDE8C3967DE8C6967EE8CB9680968196829683E8CC9684CBC9B0E59685BCAB96869687B9B996889689E8C1968ACDF7968BE8CA968C968D968E968FCEF69690969196929693D5ED9694C1D6E8C49695C3B69696B9FBD6A6E8C8969796989699CAE0D4E6969AE8C0969BE8C5E8C7969CC7B9B7E3969DE8C9969EBFDDE8D2969F96A0E8D796A1E8D5BCDCBCCFE8DB96A296A396A496A596A696A796A896A9E8DE96AAE8DAB1FA96AB96AC96AD96AE96AF96B096B196B296B396B4B0D8C4B3B8CCC6E2C8BEC8E196B596B696B7E8CFE8D4E8D696B8B9F1E8D8D7F596B9C4FB96BAE8DC96BB96BCB2E996BD96BE96BFE8D196C096C1BCED96C296C3BFC2E8CDD6F996C4C1F8B2F196C596C696C796C896C996CA96CB96CCE8DF96CDCAC1E8D996CE96CF96D096D1D5A496D2B1EAD5BBE8CEE8D0B6B0E8D396D3E8DDC0B896D4CAF796D5CBA896D696D7C6DCC0F596D896D996DA96DB96DCE8E996DD96DE96DFD0A396E096E196E296E396E496E596E6E8F2D6EA96E796E896E996EA96EB96EC96EDE8E0E8E196EE96EF96F0D1F9BACBB8F996F196F2B8F1D4D4E8EF96F3E8EEE8ECB9F0CCD2E8E6CEA6BFF296F4B0B8E8F1E8F096F5D7C096F6E8E496F7CDA9C9A396F8BBB8BDDBE8EA96F996FA96FB96FC96FD96FE9740974197429743E8E2E8E3E8E5B5B5E8E7C7C5E8EBE8EDBDB0D7AE9744E8F897459746974797489749974A974B974CE8F5974DCDB0E8F6974E974F9750975197529753975497559756C1BA9757E8E89758C3B7B0F09759975A975B975C975D975E975F9760E8F4976197629763E8F7976497659766B9A3976797689769976A976B976C976D976E976F9770C9D2977197729773C3CECEE0C0E69774977597769777CBF39778CCDDD0B59779977ACAE1977BE8F3977C977D977E9780978197829783978497859786BCEC9787E8F997889789978A978B978C978DC3DE978EC6E5978FB9F79790979197929793B0F497949795D7D897969797BCAC9798C5EF9799979A979B979C979DCCC4979E979FE9A697A097A197A297A397A497A597A697A797A897A9C9AD97AAE9A2C0E297AB97AC97ADBFC397AE97AF97B0E8FEB9D797B1E8FB97B297B397B497B5E9A497B697B797B8D2CE97B997BA97BB97BC97BDE9A397BED6B2D7B597BFE9A797C0BDB797C197C297C397C497C597C697C797C897C997CA97CB97CCE8FCE8FD97CD97CE97CFE9A197D097D197D297D397D497D597D697D7CDD697D897D9D2AC97DA97DB97DCE9B297DD97DE97DF97E0E9A997E197E297E3B4AA97E4B4BB97E597E6E9AB97E797E897E997EA97EB97EC97ED97EE97EF97F097F197F297F397F497F597F697F7D0A897F897F9E9A597FA97FBB3FE97FC97FDE9ACC0E397FEE9AA98409841E9B998429843E9B89844984598469847E9AE98489849E8FA984A984BE9A8984C984D984E984F9850BFACE9B1E9BA98519852C2A5985398549855E9AF9856B8C59857E9AD9858D3DCE9B4E9B5E9B79859985A985BE9C7985C985D985E985F98609861C0C6E9C598629863E9B098649865E9BBB0F19866986798689869986A986B986C986D986E986FE9BCD5A598709871E9BE9872E9BF987398749875E9C198769877C1F198789879C8B6987A987B987CE9BD987D987E988098819882E9C29883988498859886988798889889988AE9C3988BE9B3988CE9B6988DBBB1988E988F9890E9C0989198929893989498959896BCF7989798989899E9C4E9C6989A989B989C989D989E989F98A098A198A298A398A498A5E9CA98A698A798A898A9E9CE98AA98AB98AC98AD98AE98AF98B098B198B298B3B2DB98B4E9C898B598B698B798B898B998BA98BB98BC98BD98BEB7AE98BF98C098C198C298C398C498C598C698C798C898C998CAE9CBE9CC98CB98CC98CD98CE98CF98D0D5C198D1C4A398D298D398D498D598D698D7E9D898D8BAE198D998DA98DB98DCE9C998DDD3A398DE98DF98E0E9D498E198E298E398E498E598E698E7E9D7E9D098E898E998EA98EB98ECE9CF98ED98EEC7C198EF98F098F198F298F398F498F598F6E9D298F798F898F998FA98FB98FC98FDE9D9B3C898FEE9D399409941994299439944CFF0994599469947E9CD99489949994A994B994C994D994E994F995099519952B3F79953995499559956995799589959E9D6995A995BE9DA995C995D995ECCB4995F99609961CFAD99629963996499659966996799689969996AE9D5996BE9DCE9DB996C996D996E996F9970E9DE99719972997399749975997699779978E9D19979997A997B997C997D997E99809981E9DD9982E9DFC3CA9983998499859986998799889989998A998B998C998D998E998F9990999199929993999499959996999799989999999A999B999C999D999E999F99A099A199A299A399A499A599A699A799A899A999AA99AB99AC99AD99AE99AF99B099B199B299B399B499B599B699B799B899B999BA99BB99BC99BD99BE99BF99C099C199C299C399C499C599C699C799C899C999CA99CB99CC99CD99CE99CF99D099D199D299D399D499D599D699D799D899D999DA99DB99DC99DD99DE99DF99E099E199E299E399E499E599E699E799E899E999EA99EB99EC99ED99EE99EF99F099F199F299F399F499F5C7B7B4CEBBB6D0C0ECA399F699F7C5B799F899F999FA99FB99FC99FD99FE9A409A419A42D3FB9A439A449A459A46ECA49A47ECA5C6DB9A489A499A4ABFEE9A4B9A4C9A4D9A4EECA69A4F9A50ECA7D0AA9A51C7B89A529A53B8E89A549A559A569A579A589A599A5A9A5B9A5C9A5D9A5E9A5FECA89A609A619A629A639A649A659A669A67D6B9D5FDB4CBB2BDCEE4C6E79A689A69CDE19A6A9A6B9A6C9A6D9A6E9A6F9A709A719A729A739A749A759A769A77B4F59A78CBC0BCDF9A799A7A9A7B9A7CE9E2E9E3D1EAE9E59A7DB4F9E9E49A7ED1B3CAE2B2D09A80E9E89A819A829A839A84E9E6E9E79A859A86D6B39A879A889A89E9E9E9EA9A8A9A8B9A8C9A8D9A8EE9EB9A8F9A909A919A929A939A949A959A96E9EC9A979A989A999A9A9A9B9A9C9A9D9A9EECAFC5B9B6CE9A9FD2F39AA09AA19AA29AA39AA49AA59AA6B5EE9AA7BBD9ECB19AA89AA9D2E39AAA9AAB9AAC9AAD9AAECEE39AAFC4B89AB0C3BF9AB19AB2B6BED8B9B1C8B1CFB1D1C5FE9AB3B1D09AB4C3AB9AB59AB69AB79AB89AB9D5B19ABA9ABB9ABC9ABD9ABE9ABF9AC09AC1EBA4BAC19AC29AC39AC4CCBA9AC59AC69AC7EBA59AC8EBA79AC99ACA9ACBEBA89ACC9ACD9ACEEBA69ACF9AD09AD19AD29AD39AD49AD5EBA9EBABEBAA9AD69AD79AD89AD99ADAEBAC9ADBCACFD8B5C3F19ADCC3A5C6F8EBADC4CA9ADDEBAEEBAFEBB0B7D59ADE9ADF9AE0B7FA9AE1EBB1C7E29AE2EBB39AE3BAA4D1F5B0B1EBB2EBB49AE49AE59AE6B5AAC2C8C7E89AE7EBB59AE8CBAEE3DF9AE99AEAD3C09AEB9AEC9AED9AEED9DB9AEF9AF0CDA1D6ADC7F39AF19AF29AF3D9E0BBE39AF4BABAE3E29AF59AF69AF79AF89AF9CFAB9AFA9AFB9AFCE3E0C9C79AFDBAB99AFE9B409B41D1B4E3E1C8EAB9AFBDADB3D8CEDB9B429B43CCC09B449B459B46E3E8E3E9CDF49B479B489B499B4A9B4BCCAD9B4CBCB39B4DE3EA9B4EE3EB9B4F9B50D0DA9B519B529B53C6FBB7DA9B549B55C7DFD2CACED69B56E3E4E3EC9B57C9F2B3C19B589B59E3E79B5A9B5BC6E3E3E59B5C9B5DEDB3E3E69B5E9B5F9B609B61C9B39B62C5E69B639B649B65B9B59B66C3BB9B67E3E3C5BDC1A4C2D9B2D79B68E3EDBBA6C4AD9B69E3F0BEDA9B6A9B6BE3FBE3F5BAD39B6C9B6D9B6E9B6FB7D0D3CD9B70D6CED5D3B9C1D5B4D1D89B719B729B739B74D0B9C7F69B759B769B77C8AAB2B49B78C3DA9B799B7A9B7BE3EE9B7C9B7DE3FCE3EFB7A8E3F7E3F49B7E9B809B81B7BA9B829B83C5A29B84E3F6C5DDB2A8C6FC9B85C4E09B869B87D7A29B88C0E1E3F99B899B8AE3FAE3FDCCA9E3F39B8BD3BE9B8CB1C3EDB4E3F1E3F29B8DE3F8D0BAC6C3D4F3E3FE9B8E9B8FBDE09B909B91E4A79B929B93E4A69B949B959B96D1F3E4A39B97E4A99B989B999B9AC8F79B9B9B9C9B9D9B9ECFB49B9FE4A8E4AEC2E59BA09BA1B6B49BA29BA39BA49BA59BA69BA7BDF29BA8E4A29BA99BAABAE9E4AA9BAB9BACE4AC9BAD9BAEB6FDD6DEE4B29BAFE4AD9BB09BB19BB2E4A19BB3BBEECDDDC7A2C5C99BB49BB5C1F79BB6E4A49BB7C7B3BDACBDBDE4A59BB8D7C7B2E29BB9E4ABBCC3E4AF9BBABBEBE4B0C5A8E4B19BBB9BBC9BBD9BBED5E3BFA39BBFE4BA9BC0E4B79BC1E4BB9BC29BC3E4BD9BC49BC5C6D69BC69BC7BAC6C0CB9BC89BC99BCAB8A1E4B49BCB9BCC9BCD9BCED4A19BCF9BD0BAA3BDFE9BD19BD29BD3E4BC9BD49BD59BD69BD79BD8CDBF9BD99BDAC4F99BDB9BDCCFFBC9E69BDD9BDED3BF9BDFCFD19BE09BE1E4B39BE2E4B8E4B9CCE99BE39BE49BE59BE69BE7CCCE9BE8C0D4E4B5C1B0E4B6CED09BE9BBC1B5D39BEAC8F3BDA7D5C7C9ACB8A2E4CA9BEB9BECE4CCD1C49BED9BEED2BA9BEF9BF0BAAD9BF19BF2BAD49BF39BF49BF59BF69BF79BF8E4C3B5ED9BF99BFA9BFBD7CDE4C0CFFDE4BF9BFC9BFD9BFEC1DCCCCA9C409C419C429C43CAE79C449C459C469C47C4D79C48CCD4E4C89C499C4A9C4BE4C7E4C19C4CE4C4B5AD9C4D9C4ED3D99C4FE4C69C509C519C529C53D2F9B4E39C54BBB49C559C56C9EE9C57B4BE9C589C599C5ABBEC9C5BD1CD9C5CCCEDEDB59C5D9C5E9C5F9C609C619C629C639C64C7E59C659C669C679C68D4A89C69E4CBD7D5E4C29C6ABDA5E4C59C6B9C6CD3E69C6DE4C9C9F89C6E9C6FE4BE9C709C71D3E59C729C73C7FEB6C99C74D4FCB2B3E4D79C759C769C77CEC29C78E4CD9C79CEBC9C7AB8DB9C7B9C7CE4D69C7DBFCA9C7E9C809C81D3CE9C82C3EC9C839C849C859C869C879C889C899C8AC5C8E4D89C8B9C8C9C8D9C8E9C8F9C909C919C92CDC4E4CF9C939C949C959C96E4D4E4D59C97BAFE9C98CFE69C999C9AD5BF9C9B9C9C9C9DE4D29C9E9C9F9CA09CA19CA29CA39CA49CA59CA69CA79CA8E4D09CA99CAAE4CE9CAB9CAC9CAD9CAE9CAF9CB09CB19CB29CB39CB49CB59CB69CB79CB89CB9CDE5CAAA9CBA9CBB9CBCC0A39CBDBDA6E4D39CBE9CBFB8C89CC09CC19CC29CC39CC4E4E7D4B49CC59CC69CC79CC89CC99CCA9CCBE4DB9CCC9CCD9CCEC1EF9CCF9CD0E4E99CD19CD2D2E79CD39CD4E4DF9CD5E4E09CD69CD7CFAA9CD89CD99CDA9CDBCBDD9CDCE4DAE4D19CDDE4E59CDEC8DCE4E39CDF9CE0C4E7E4E29CE1E4E19CE29CE39CE4B3FCE4E89CE59CE69CE79CE8B5E19CE99CEA9CEBD7CC9CEC9CED9CEEE4E69CEFBBAC9CF0D7D2CCCFEBF89CF1E4E49CF29CF3B9F69CF49CF59CF6D6CDE4D9E4DCC2FAE4DE9CF7C2CBC0C4C2D09CF8B1F5CCB29CF99CFA9CFB9CFC9CFD9CFE9D409D419D429D43B5CE9D449D459D469D47E4EF9D489D499D4A9D4B9D4C9D4D9D4E9D4FC6AF9D509D519D52C6E19D539D54E4F59D559D569D579D589D59C2A99D5A9D5B9D5CC0ECD1DDE4EE9D5D9D5E9D5F9D609D619D629D639D649D659D66C4AE9D679D689D69E4ED9D6A9D6B9D6C9D6DE4F6E4F4C2FE9D6EE4DD9D6FE4F09D70CAFE9D71D5C49D729D73E4F19D749D759D769D779D789D799D7AD1FA9D7B9D7C9D7D9D7E9D809D819D82E4EBE4EC9D839D849D85E4F29D86CEAB9D879D889D899D8A9D8B9D8C9D8D9D8E9D8F9D90C5CB9D919D929D93C7B19D94C2BA9D959D969D97E4EA9D989D999D9AC1CA9D9B9D9C9D9D9D9E9D9F9DA0CCB6B3B19DA19DA29DA3E4FB9DA4E4F39DA59DA69DA7E4FA9DA8E4FD9DA9E4FC9DAA9DAB9DAC9DAD9DAE9DAF9DB0B3CE9DB19DB29DB3B3BAE4F79DB49DB5E4F9E4F8C5EC9DB69DB79DB89DB99DBA9DBB9DBC9DBD9DBE9DBF9DC09DC19DC2C0BD9DC39DC49DC59DC6D4E89DC79DC89DC99DCA9DCBE5A29DCC9DCD9DCE9DCF9DD09DD19DD29DD39DD49DD59DD6B0C49DD79DD8E5A49DD99DDAE5A39DDB9DDC9DDD9DDE9DDF9DE0BCA49DE1E5A59DE29DE39DE49DE59DE69DE7E5A19DE89DE99DEA9DEB9DEC9DED9DEEE4FEB1F49DEF9DF09DF19DF29DF39DF49DF59DF69DF79DF89DF9E5A89DFAE5A9E5A69DFB9DFC9DFD9DFE9E409E419E429E439E449E459E469E47E5A7E5AA9E489E499E4A9E4B9E4C9E4D9E4E9E4F9E509E519E529E539E549E559E569E579E589E599E5A9E5B9E5C9E5D9E5E9E5F9E609E619E629E639E649E659E669E679E68C6D99E699E6A9E6B9E6C9E6D9E6E9E6F9E70E5ABE5AD9E719E729E739E749E759E769E77E5AC9E789E799E7A9E7B9E7C9E7D9E7E9E809E819E829E839E849E859E869E879E889E89E5AF9E8A9E8B9E8CE5AE9E8D9E8E9E8F9E909E919E929E939E949E959E969E979E989E999E9A9E9B9E9C9E9D9E9EB9E09E9F9EA0E5B09EA19EA29EA39EA49EA59EA69EA79EA89EA99EAA9EAB9EAC9EAD9EAEE5B19EAF9EB09EB19EB29EB39EB49EB59EB69EB79EB89EB99EBABBF0ECE1C3F09EBBB5C6BBD29EBC9EBD9EBE9EBFC1E9D4EE9EC0BEC49EC19EC29EC3D7C69EC4D4D6B2D3ECBE9EC59EC69EC79EC8EAC19EC99ECA9ECBC2AFB4B69ECC9ECD9ECED1D79ECF9ED09ED1B3B49ED2C8B2BFBBECC09ED39ED4D6CB9ED59ED6ECBFECC19ED79ED89ED99EDA9EDB9EDC9EDD9EDE9EDF9EE09EE19EE29EE3ECC5BEE6CCBFC5DABEBC9EE4ECC69EE5B1FE9EE69EE79EE8ECC4D5A8B5E39EE9ECC2C1B6B3E39EEA9EEBECC3CBB8C0C3CCFE9EEC9EED9EEE9EEFC1D29EF0ECC89EF19EF29EF39EF49EF59EF69EF79EF89EF99EFA9EFB9EFC9EFDBAE6C0D39EFED6F29F409F419F42D1CC9F439F449F459F46BFBE9F47B7B3C9D5ECC7BBE29F48CCCCBDFDC8C89F49CFA99F4A9F4B9F4C9F4D9F4E9F4F9F50CDE99F51C5EB9F529F539F54B7E99F559F569F579F589F599F5A9F5B9F5C9F5D9F5E9F5FD1C9BAB89F609F619F629F639F64ECC99F659F66ECCA9F67BBC0ECCB9F68ECE2B1BAB7D99F699F6A9F6B9F6C9F6D9F6E9F6F9F709F719F729F73BDB99F749F759F769F779F789F799F7A9F7BECCCD1E6ECCD9F7C9F7D9F7E9F80C8BB9F819F829F839F849F859F869F879F889F899F8A9F8B9F8C9F8D9F8EECD19F8F9F909F919F92ECD39F93BBCD9F94BCE59F959F969F979F989F999F9A9F9B9F9C9F9D9F9E9F9F9FA09FA1ECCF9FA2C9B79FA39FA49FA59FA69FA7C3BA9FA8ECE3D5D5ECD09FA99FAA9FAB9FAC9FADD6F39FAE9FAF9FB0ECD2ECCE9FB19FB29FB39FB4ECD49FB5ECD59FB69FB7C9BF9FB89FB99FBA9FBB9FBC9FBDCFA89FBE9FBF9FC09FC19FC2D0DC9FC39FC49FC59FC6D1AC9FC79FC89FC99FCAC8DB9FCB9FCC9FCDECD6CEF59FCE9FCF9FD09FD19FD2CAECECDA9FD39FD49FD59FD69FD79FD89FD9ECD99FDA9FDB9FDCB0BE9FDD9FDE9FDF9FE09FE19FE2ECD79FE3ECD89FE49FE59FE6ECE49FE79FE89FE99FEA9FEB9FEC9FED9FEE9FEFC8BC9FF09FF19FF29FF39FF49FF59FF69FF79FF89FF9C1C79FFA9FFB9FFC9FFD9FFEECDCD1E0A040A041A042A043A044A045A046A047A048A049ECDBA04AA04BA04CA04DD4EFA04EECDDA04FA050A051A052A053A054DBC6A055A056A057A058A059A05AA05BA05CA05DA05EECDEA05FA060A061A062A063A064A065A066A067A068A069A06AB1ACA06BA06CA06DA06EA06FA070A071A072A073A074A075A076A077A078A079A07AA07BA07CA07DA07EA080A081ECDFA082A083A084A085A086A087A088A089A08AA08BECE0A08CD7A6A08DC5C0A08EA08FA090EBBCB0AEA091A092A093BEF4B8B8D2AFB0D6B5F9A094D8B3A095CBACA096E3DDA097A098A099A09AA09BA09CA09DC6ACB0E6A09EA09FA0A0C5C6EBB9A0A1A0A2A0A3A0A4EBBAA0A5A0A6A0A7EBBBA0A8A0A9D1C0A0AAC5A3A0ABEAF2A0ACC4B2A0ADC4B5C0CEA0AEA0AFA0B0EAF3C4C1A0B1CEEFA0B2A0B3A0B4A0B5EAF0EAF4A0B6A0B7C9FCA0B8A0B9C7A3A0BAA0BBA0BCCCD8CEFEA0BDA0BEA0BFEAF5EAF6CFACC0E7A0C0A0C1EAF7A0C2A0C3A0C4A0C5A0C6B6BFEAF8A0C7EAF9A0C8EAFAA0C9A0CAEAFBA0CBA0CCA0CDA0CEA0CFA0D0A0D1A0D2A0D3A0D4A0D5A0D6EAF1A0D7A0D8A0D9A0DAA0DBA0DCA0DDA0DEA0DFA0E0A0E1A0E2C8AEE1EBA0E3B7B8E1ECA0E4A0E5A0E6E1EDA0E7D7B4E1EEE1EFD3CCA0E8A0E9A0EAA0EBA0ECA0EDA0EEE1F1BFF1E1F0B5D2A0EFA0F0A0F1B1B7A0F2A0F3A0F4A0F5E1F3E1F2A0F6BAFCA0F7E1F4A0F8A0F9A0FAA0FBB9B7A0FCBED1A0FDA0FEAA40AA41C4FCAA42BADDBDC6AA43AA44AA45AA46AA47AA48E1F5E1F7AA49AA4AB6C0CFC1CAA8E1F6D5F8D3FCE1F8E1FCE1F9AA4BAA4CE1FAC0EAAA4DE1FEE2A1C0C7AA4EAA4FAA50AA51E1FBAA52E1FDAA53AA54AA55AA56AA57AA58E2A5AA59AA5AAA5BC1D4AA5CAA5DAA5EAA5FE2A3AA60E2A8B2FEE2A2AA61AA62AA63C3CDB2C2E2A7E2A6AA64AA65E2A4E2A9AA66AA67E2ABAA68AA69AA6AD0C9D6EDC3A8E2ACAA6BCFD7AA6CAA6DE2AEAA6EAA6FBAEFAA70AA71E9E0E2ADE2AAAA72AA73AA74AA75BBABD4B3AA76AA77AA78AA79AA7AAA7BAA7CAA7DAA7EAA80AA81AA82AA83E2B0AA84AA85E2AFAA86E9E1AA87AA88AA89AA8AE2B1AA8BAA8CAA8DAA8EAA8FAA90AA91AA92E2B2AA93AA94AA95AA96AA97AA98AA99AA9AAA9BAA9CAA9DE2B3CCA1AA9EE2B4AA9FAAA0AB40AB41AB42AB43AB44AB45AB46AB47AB48AB49AB4AAB4BE2B5AB4CAB4DAB4EAB4FAB50D0FEAB51AB52C2CAAB53D3F1AB54CDF5AB55AB56E7E0AB57AB58E7E1AB59AB5AAB5BAB5CBEC1AB5DAB5EAB5FAB60C2EAAB61AB62AB63E7E4AB64AB65E7E3AB66AB67AB68AB69AB6AAB6BCDE6AB6CC3B5AB6DAB6EE7E2BBB7CFD6AB6FC1E1E7E9AB70AB71AB72E7E8AB73AB74E7F4B2A3AB75AB76AB77AB78E7EAAB79E7E6AB7AAB7BAB7CAB7DAB7EE7ECE7EBC9BAAB80AB81D5E4AB82E7E5B7A9E7E7AB83AB84AB85AB86AB87AB88AB89E7EEAB8AAB8BAB8CAB8DE7F3AB8ED6E9AB8FAB90AB91AB92E7EDAB93E7F2AB94E7F1AB95AB96AB97B0E0AB98AB99AB9AAB9BE7F5AB9CAB9DAB9EAB9FABA0AC40AC41AC42AC43AC44AC45AC46AC47AC48AC49AC4AC7F2AC4BC0C5C0EDAC4CAC4DC1F0E7F0AC4EAC4FAC50AC51E7F6CBF6AC52AC53AC54AC55AC56AC57AC58AC59AC5AE8A2E8A1AC5BAC5CAC5DAC5EAC5FAC60D7C1AC61AC62E7FAE7F9AC63E7FBAC64E7F7AC65E7FEAC66E7FDAC67E7FCAC68AC69C1D5C7D9C5FDC5C3AC6AAC6BAC6CAC6DAC6EC7EDAC6FAC70AC71AC72E8A3AC73AC74AC75AC76AC77AC78AC79AC7AAC7BAC7CAC7DAC7EAC80AC81AC82AC83AC84AC85AC86E8A6AC87E8A5AC88E8A7BAF7E7F8E8A4AC89C8F0C9AAAC8AAC8BAC8CAC8DAC8EAC8FAC90AC91AC92AC93AC94AC95AC96E8A9AC97AC98B9E5AC99AC9AAC9BAC9CAC9DD1FEE8A8AC9EAC9FACA0AD40AD41AD42E8AAAD43E8ADE8AEAD44C1A7AD45AD46AD47E8AFAD48AD49AD4AE8B0AD4BAD4CE8ACAD4DE8B4AD4EAD4FAD50AD51AD52AD53AD54AD55AD56AD57AD58E8ABAD59E8B1AD5AAD5BAD5CAD5DAD5EAD5FAD60AD61E8B5E8B2E8B3AD62AD63AD64AD65AD66AD67AD68AD69AD6AAD6BAD6CAD6DAD6EAD6FAD70AD71E8B7AD72AD73AD74AD75AD76AD77AD78AD79AD7AAD7BAD7CAD7DAD7EAD80AD81AD82AD83AD84AD85AD86AD87AD88AD89E8B6AD8AAD8BAD8CAD8DAD8EAD8FAD90AD91AD92B9CFAD93F0ACAD94F0ADAD95C6B0B0EAC8BFAD96CDDFAD97AD98AD99AD9AAD9BAD9CAD9DCECDEAB1AD9EAD9FADA0AE40EAB2AE41C6BFB4C9AE42AE43AE44AE45AE46AE47AE48EAB3AE49AE4AAE4BAE4CD5E7AE4DAE4EAE4FAE50AE51AE52AE53AE54DDF9AE55EAB4AE56EAB5AE57EAB6AE58AE59AE5AAE5BB8CADFB0C9F5AE5CCCF0AE5DAE5EC9FAAE5FAE60AE61AE62AE63C9FBAE64AE65D3C3CBA6AE66B8A6F0AEB1C2AE67E5B8CCEFD3C9BCD7C9EAAE68B5E7AE69C4D0B5E9AE6AEEAEBBADAE6BAE6CE7DEAE6DEEAFAE6EAE6FAE70AE71B3A9AE72AE73EEB2AE74AE75EEB1BDE7AE76EEB0CEB7AE77AE78AE79AE7AC5CFAE7BAE7CAE7DAE7EC1F4DBCEEEB3D0F3AE80AE81AE82AE83AE84AE85AE86AE87C2D4C6E8AE88AE89AE8AB7ACAE8BAE8CAE8DAE8EAE8FAE90AE91EEB4AE92B3EBAE93AE94AE95BBFBEEB5AE96AE97AE98AE99AE9AE7DCAE9BAE9CAE9DEEB6AE9EAE9FBDAEAEA0AF40AF41AF42F1E2AF43AF44AF45CAE8AF46D2C9F0DAAF47F0DBAF48F0DCC1C6AF49B8EDBECEAF4AAF4BF0DEAF4CC5B1F0DDD1F1AF4DF0E0B0CCBDEAAF4EAF4FAF50AF51AF52D2DFF0DFAF53B4AFB7E8F0E6F0E5C6A3F0E1F0E2B4C3AF54AF55F0E3D5EEAF56AF57CCDBBED2BCB2AF58AF59AF5AF0E8F0E7F0E4B2A1AF5BD6A2D3B8BEB7C8ACAF5CAF5DF0EAAF5EAF5FAF60AF61D1F7AF62D6CCBADBF0E9AF63B6BBAF64AF65CDB4AF66AF67C6A6AF68AF69AF6AC1A1F0EBF0EEAF6BF0EDF0F0F0ECAF6CBBBEF0EFAF6DAF6EAF6FAF70CCB5F0F2AF71AF72B3D5AF73AF74AF75AF76B1D4AF77AF78F0F3AF79AF7AF0F4F0F6B4E1AF7BF0F1AF7CF0F7AF7DAF7EAF80AF81F0FAAF82F0F8AF83AF84AF85F0F5AF86AF87AF88AF89F0FDAF8AF0F9F0FCF0FEAF8BF1A1AF8CAF8DAF8ECEC1F1A4AF8FF1A3AF90C1F6F0FBCADDAF91AF92B4F1B1F1CCB1AF93F1A6AF94AF95F1A7AF96AF97F1ACD5CEF1A9AF98AF99C8B3AF9AAF9BAF9CF1A2AF9DF1ABF1A8F1A5AF9EAF9FF1AAAFA0B040B041B042B043B044B045B046B0A9F1ADB047B048B049B04AB04BB04CF1AFB04DF1B1B04EB04FB050B051B052F1B0B053F1AEB054B055B056B057D1A2B058B059B05AB05BB05CB05DB05EF1B2B05FB060B061F1B3B062B063B064B065B066B067B068B069B9EFB06AB06BB5C7B06CB0D7B0D9B06DB06EB06FD4EDB070B5C4B071BDD4BBCAF0A7B072B073B8DEB074B075F0A8B076B077B0A8B078F0A9B079B07ACDEEB07BB07CF0AAB07DB07EB080B081B082B083B084B085B086B087F0ABB088B089B08AB08BB08CB08DB08EB08FB090C6A4B091B092D6E5F1E4B093F1E5B094B095B096B097B098B099B09AB09BB09CB09DC3F3B09EB09FD3DBB0A0B140D6D1C5E8B141D3AFB142D2E6B143B144EEC1B0BBD5B5D1CEBCE0BAD0B145BFF8B146B8C7B5C1C5CCB147B148CAA2B149B14AB14BC3CBB14CB14DB14EB14FB150EEC2B151B152B153B154B155B156B157B158C4BFB6A2B159EDECC3A4B15AD6B1B15BB15CB15DCFE0EDEFB15EB15FC5CEB160B6DCB161B162CAA1B163B164EDEDB165B166EDF0EDF1C3BCB167BFB4B168EDEEB169B16AB16BB16CB16DB16EB16FB170B171B172B173EDF4EDF2B174B175B176B177D5E6C3DFB178EDF3B179B17AB17BEDF6B17CD5A3D1A3B17DB17EB180EDF5B181C3D0B182B183B184B185B186EDF7BFF4BEECEDF8B187CCF7B188D1DBB189B18AB18BD7C5D5F6B18CEDFCB18DB18EB18FEDFBB190B191B192B193B194B195B196B197EDF9EDFAB198B199B19AB19BB19CB19DB19EB19FEDFDBEA6B1A0B240B241B242B243CBAFEEA1B6BDB244EEA2C4C0B245EDFEB246B247BDDEB2C7B248B249B24AB24BB24CB24DB24EB24FB250B251B252B253B6C3B254B255B256EEA5D8BAEEA3EEA6B257B258B259C3E9B3F2B25AB25BB25CB25DB25EB25FEEA7EEA4CFB9B260B261EEA8C2F7B262B263B264B265B266B267B268B269B26AB26BB26CB26DEEA9EEAAB26EDEABB26FB270C6B3B271C7C6B272D6F5B5C9B273CBB2B274B275B276EEABB277B278CDABB279EEACB27AB27BB27CB27DB27ED5B0B280EEADB281F6C4B282B283B284B285B286B287B288B289B28AB28BB28CB28DB28EDBC7B28FB290B291B292B293B294B295B296B297B4A3B298B299B29AC3ACF1E6B29BB29CB29DB29EB29FCAB8D2D3B2A0D6AAB340EFF2B341BED8B342BDC3EFF3B6CCB0ABB343B344B345B346CAAFB347B348EDB6B349EDB7B34AB34BB34CB34DCEF9B7AFBFF3EDB8C2EBC9B0B34EB34FB350B351B352B353EDB9B354B355C6F6BFB3B356B357B358EDBCC5F8B359D1D0B35AD7A9EDBAEDBBB35BD1E2B35CEDBFEDC0B35DEDC4B35EB35FB360EDC8B361EDC6EDCED5E8B362EDC9B363B364EDC7EDBEB365B366C5E9B367B368B369C6C6B36AB36BC9E9D4D2EDC1EDC2EDC3EDC5B36CC0F9B36DB4A1B36EB36FB370B371B9E8B372EDD0B373B374B375B376EDD1B377EDCAB378EDCFB379CEF8B37AB37BCBB6EDCCEDCDB37CB37DB37EB380B381CFF5B382B383B384B385B386B387B388B389B38AB38BB38CB38DEDD2C1F2D3B2EDCBC8B7B38EB38FB390B391B392B393B394B395BCEFB396B397B398B399C5F0B39AB39BB39CB39DB39EB39FB3A0B440B441B442EDD6B443B5EFB444B445C2B5B0ADCBE9B446B447B1AEB448EDD4B449B44AB44BCDEBB5E2B44CEDD5EDD3EDD7B44DB44EB5FAB44FEDD8B450EDD9B451EDDCB452B1CCB453B454B455B456B457B458B459B45AC5F6BCEEEDDACCBCB2EAB45BB45CB45DB45EEDDBB45FB460B461B462C4EBB463B464B4C5B465B466B467B0F5B468B469B46AEDDFC0DAB4E8B46BB46CB46DB46EC5CDB46FB470B471EDDDBFC4B472B473B474EDDEB475B476B477B478B479B47AB47BB47CB47DB47EB480B481B482B483C4A5B484B485B486EDE0B487B488B489B48AB48BEDE1B48CEDE3B48DB48EC1D7B48FB490BBC7B491B492B493B494B495B496BDB8B497B498B499EDE2B49AB49BB49CB49DB49EB49FB4A0B540B541B542B543B544B545EDE4B546B547B548B549B54AB54BB54CB54DB54EB54FEDE6B550B551B552B553B554EDE5B555B556B557B558B559B55AB55BB55CB55DB55EB55FB560B561B562B563EDE7B564B565B566B567B568CABEECEAC0F1B569C9E7B56AECEBC6EEB56BB56CB56DB56EECECB56FC6EDECEDB570B571B572B573B574B575B576B577B578ECF0B579B57AD7E6ECF3B57BB57CECF1ECEEECEFD7A3C9F1CBEEECF4B57DECF2B57EB580CFE9B581ECF6C6B1B582B583B584B585BCC0B586ECF5B587B588B589B58AB58BB58CB58DB5BBBBF6B58EECF7B58FB590B591B592B593D9F7BDFBB594B595C2BBECF8B596B597B598B599ECF9B59AB59BB59CB59DB8A3B59EB59FB5A0B640B641B642B643B644B645B646ECFAB647B648B649B64AB64BB64CB64DB64EB64FB650B651B652ECFBB653B654B655B656B657B658B659B65AB65BB65CB65DECFCB65EB65FB660B661B662D3EDD8AEC0EBB663C7DDBACCB664D0E3CBBDB665CDBAB666B667B8D1B668B669B1FCB66AC7EFB66BD6D6B66CB66DB66EBFC6C3EBB66FB670EFF5B671B672C3D8B673B674B675B676B677B678D7E2B679B67AB67BEFF7B3D3B67CC7D8D1EDB67DD6C8B67EEFF8B680EFF6B681BBFDB3C6B682B683B684B685B686B687B688BDD5B689B68AD2C6B68BBBE0B68CB68DCFA1B68EEFFCEFFBB68FB690EFF9B691B692B693B694B3CCB695C9D4CBB0B696B697B698B699B69AEFFEB69BB69CB0DEB69DB69ED6C9B69FB6A0B740EFFDB741B3EDB742B743F6D5B744B745B746B747B748B749B74AB74BB74CB74DB74EB74FB750B751B752CEC8B753B754B755F0A2B756F0A1B757B5BEBCDABBFCB758B8E5B759B75AB75BB75CB75DB75EC4C2B75FB760B761B762B763B764B765B766B767B768F0A3B769B76AB76BB76CB76DCBEBB76EB76FB770B771B772B773B774B775B776B777B778B779B77AB77BB77CB77DB77EB780B781B782B783B784B785B786F0A6B787B788B789D1A8B78ABEBFC7EEF1B6F1B7BFD5B78BB78CB78DB78EB4A9F1B8CDBBB78FC7D4D5ADB790F1B9B791F1BAB792B793B794B795C7CFB796B797B798D2A4D6CFB799B79AF1BBBDD1B4B0BEBDB79BB79CB79DB4DCCED1B79EBFDFF1BDB79FB7A0B840B841BFFAF1BCB842F1BFB843B844B845F1BEF1C0B846B847B848B849B84AF1C1B84BB84CB84DB84EB84FB850B851B852B853B854B855C1FEB856B857B858B859B85AB85BB85CB85DB85EB85FB860C1A2B861B862B863B864B865B866B867B868B869B86ACAFAB86BB86CD5BEB86DB86EB86FB870BEBABEB9D5C2B871B872BFA2B873CDAFF1B5B874B875B876B877B878B879BDDFB87AB6CBB87BB87CB87DB87EB880B881B882B883B884D6F1F3C3B885B886F3C4B887B8CDB888B889B88AF3C6F3C7B88BB0CAB88CF3C5B88DF3C9CBF1B88EB88FB890F3CBB891D0A6B892B893B1CAF3C8B894B895B896F3CFB897B5D1B898B899F3D7B89AF3D2B89BB89CB89DF3D4F3D3B7FBB89EB1BFB89FF3CEF3CAB5DAB8A0F3D0B940B941F3D1B942F3D5B943B944B945B946F3CDB947BCE3B948C1FDB949F3D6B94AB94BB94CB94DB94EB94FF3DAB950F3CCB951B5C8B952BDEEF3DCB953B954B7A4BFF0D6FECDB2B955B4F0B956B2DFB957F3D8B958F3D9C9B8B959F3DDB95AB95BF3DEB95CF3E1B95DB95EB95FB960B961B962B963B964B965B966B967F3DFB968B969F3E3F3E2B96AB96BF3DBB96CBFEAB96DB3EFB96EF3E0B96FB970C7A9B971BCF2B972B973B974B975F3EBB976B977B978B979B97AB97BB97CB9BFB97DB97EF3E4B980B981B982B2ADBBFEB983CBE3B984B985B986B987F3EDF3E9B988B989B98AB9DCF3EEB98BB98CB98DF3E5F3E6F3EAC2E1F3ECF3EFF3E8BCFDB98EB98FB990CFE4B991B992F3F0B993B994B995F3E7B996B997B998B999B99AB99BB99CB99DF3F2B99EB99FB9A0BA40D7ADC6AABA41BA42BA43BA44F3F3BA45BA46BA47BA48F3F1BA49C2A8BA4ABA4BBA4CBA4DBA4EB8DDF3F5BA4FBA50F3F4BA51BA52BA53B4DBBA54BA55BA56F3F6F3F7BA57BA58BA59F3F8BA5ABA5BBA5CC0BABA5DBA5EC0E9BA5FBA60BA61BA62BA63C5F1BA64BA65BA66BA67F3FBBA68F3FABA69BA6ABA6BBA6CBA6DBA6EBA6FBA70B4D8BA71BA72BA73F3FEF3F9BA74BA75F3FCBA76BA77BA78BA79BA7ABA7BF3FDBA7CBA7DBA7EBA80BA81BA82BA83BA84F4A1BA85BA86BA87BA88BA89BA8AF4A3BBC9BA8BBA8CF4A2BA8DBA8EBA8FBA90BA91BA92BA93BA94BA95BA96BA97BA98BA99F4A4BA9ABA9BBA9CBA9DBA9EBA9FB2BEF4A6F4A5BAA0BB40BB41BB42BB43BB44BB45BB46BB47BB48BB49BCAEBB4ABB4BBB4CBB4DBB4EBB4FBB50BB51BB52BB53BB54BB55BB56BB57BB58BB59BB5ABB5BBB5CBB5DBB5EBB5FBB60BB61BB62BB63BB64BB65BB66BB67BB68BB69BB6ABB6BBB6CBB6DBB6EC3D7D9E1BB6FBB70BB71BB72BB73BB74C0E0F4CCD7D1BB75BB76BB77BB78BB79BB7ABB7BBB7CBB7DBB7EBB80B7DBBB81BB82BB83BB84BB85BB86BB87F4CEC1A3BB88BB89C6C9BB8AB4D6D5B3BB8BBB8CBB8DF4D0F4CFF4D1CBDABB8EBB8FF4D2BB90D4C1D6E0BB91BB92BB93BB94B7E0BB95BB96BB97C1B8BB98BB99C1BBF4D3BEACBB9ABB9BBB9CBB9DBB9EB4E2BB9FBBA0F4D4F4D5BEABBC40BC41F4D6BC42BC43BC44F4DBBC45F4D7F4DABC46BAFDBC47F4D8F4D9BC48BC49BC4ABC4BBC4CBC4DBC4EB8E2CCC7F4DCBC4FB2DABC50BC51C3D3BC52BC53D4E3BFB7BC54BC55BC56BC57BC58BC59BC5AF4DDBC5BBC5CBC5DBC5EBC5FBC60C5B4BC61BC62BC63BC64BC65BC66BC67BC68F4E9BC69BC6ACFB5BC6BBC6CBC6DBC6EBC6FBC70BC71BC72BC73BC74BC75BC76BC77BC78CEC9BC79BC7ABC7BBC7CBC7DBC7EBC80BC81BC82BC83BC84BC85BC86BC87BC88BC89BC8ABC8BBC8CBC8DBC8ECBD8BC8FCBF7BC90BC91BC92BC93BDF4BC94BC95BC96D7CFBC97BC98BC99C0DBBC9ABC9BBC9CBC9DBC9EBC9FBCA0BD40BD41BD42BD43BD44BD45BD46BD47BD48BD49BD4ABD4BBD4CBD4DBD4EBD4FBD50BD51BD52BD53BD54BD55BD56BD57BD58BD59BD5ABD5BBD5CBD5DBD5EBD5FBD60BD61BD62BD63BD64BD65BD66BD67BD68BD69BD6ABD6BBD6CBD6DBD6EBD6FBD70BD71BD72BD73BD74BD75BD76D0F5BD77BD78BD79BD7ABD7BBD7CBD7DBD7EF4EABD80BD81BD82BD83BD84BD85BD86BD87BD88BD89BD8ABD8BBD8CBD8DBD8EBD8FBD90BD91BD92BD93BD94BD95BD96BD97BD98BD99BD9ABD9BBD9CBD9DBD9EBD9FBDA0BE40BE41BE42BE43BE44BE45BE46BE47BE48BE49BE4ABE4BBE4CF4EBBE4DBE4EBE4FBE50BE51BE52BE53F4ECBE54BE55BE56BE57BE58BE59BE5ABE5BBE5CBE5DBE5EBE5FBE60BE61BE62BE63BE64BE65BE66BE67BE68BE69BE6ABE6BBE6CBE6DBE6EBE6FBE70BE71BE72BE73BE74BE75BE76BE77BE78BE79BE7ABE7BBE7CBE7DBE7EBE80BE81BE82BE83BE84BE85BE86BE87BE88BE89BE8ABE8BBE8CBE8DBE8EBE8FBE90BE91BE92BE93BE94BE95BE96BE97BE98BE99BE9ABE9BBE9CBE9DBE9EBE9FBEA0BF40BF41BF42BF43BF44BF45BF46BF47BF48BF49BF4ABF4BBF4CBF4DBF4EBF4FBF50BF51BF52BF53BF54BF55BF56BF57BF58BF59BF5ABF5BBF5CBF5DBF5EBF5FBF60BF61BF62BF63BF64BF65BF66BF67BF68BF69BF6ABF6BBF6CBF6DBF6EBF6FBF70BF71BF72BF73BF74BF75BF76BF77BF78BF79BF7ABF7BBF7CBF7DBF7EBF80F7E3BF81BF82BF83BF84BF85B7B1BF86BF87BF88BF89BF8AF4EDBF8BBF8CBF8DBF8EBF8FBF90BF91BF92BF93BF94BF95BF96BF97BF98BF99BF9ABF9BBF9CBF9DBF9EBF9FBFA0C040C041C042C043C044C045C046C047C048C049C04AC04BC04CC04DC04EC04FC050C051C052C053C054C055C056C057C058C059C05AC05BC05CC05DC05EC05FC060C061C062C063D7EBC064C065C066C067C068C069C06AC06BC06CC06DC06EC06FC070C071C072C073C074C075C076C077C078C079C07AC07BF4EEC07CC07DC07EE6F9BEC0E6FABAECE6FBCFCBE6FCD4BCBCB6E6FDE6FEBCCDC8D2CEB3E7A1C080B4BFE7A2C9B4B8D9C4C9C081D7DDC2DAB7D7D6BDCEC6B7C4C082C083C5A6E7A3CFDFE7A4E7A5E7A6C1B7D7E9C9F0CFB8D6AFD6D5E7A7B0EDE7A8E7A9C9DCD2EFBEADE7AAB0F3C8DEBDE1E7ABC8C6C084E7ACBBE6B8F8D1A4E7ADC2E7BEF8BDCACDB3E7AEE7AFBEEED0E5C085CBE7CCD0BCCCE7B0BCA8D0F7E7B1C086D0F8E7B2E7B3B4C2E7B4E7B5C9FECEACC3E0E7B7B1C1B3F1C087E7B8E7B9D7DBD5C0E7BAC2CCD7BAE7BBE7BCE7BDBCEAC3E5C0C2E7BEE7BFBCA9C088E7C0E7C1E7B6B6D0E7C2C089E7C3E7C4BBBAB5DEC2C6B1E0E7C5D4B5E7C6B8BFE7C8E7C7B7ECC08AE7C9B2F8E7CAE7CBE7CCE7CDE7CEE7CFE7D0D3A7CBF5E7D1E7D2E7D3E7D4C9C9E7D5E7D6E7D7E7D8E7D9BDC9E7DAF3BEC08BB8D7C08CC8B1C08DC08EC08FC090C091C092C093F3BFC094F3C0F3C1C095C096C097C098C099C09AC09BC09CC09DC09EB9DECDF8C09FC0A0D8E8BAB1C140C2DEEEB7C141B7A3C142C143C144C145EEB9C146EEB8B0D5C147C148C149C14AC14BEEBBD5D6D7EFC14CC14DC14ED6C3C14FC150EEBDCAF0C151EEBCC152C153C154C155EEBEC156C157C158C159EEC0C15AC15BEEBFC15CC15DC15EC15FC160C161C162C163D1F2C164C7BCC165C3C0C166C167C168C169C16AB8E1C16BC16CC16DC16EC16FC1E7C170C171F4C6D0DFF4C7C172CFDBC173C174C8BAC175C176F4C8C177C178C179C17AC17BC17CC17DF4C9F4CAC17EF4CBC180C181C182C183C184D9FAB8FEC185C186E5F1D3F0C187F4E0C188CECCC189C18AC18BB3E1C18CC18DC18EC18FF1B4C190D2EEC191F4E1C192C193C194C195C196CFE8F4E2C197C198C7CCC199C19AC19BC19CC19DC19EB5D4B4E4F4E4C19FC1A0C240F4E3F4E5C241C242F4E6C243C244C245C246F4E7C247BAB2B0BFC248F4E8C249C24AC24BC24CC24DC24EC24FB7ADD2EDC250C251C252D2ABC0CFC253BFBCEBA3D5DFEAC8C254C255C256C257F1F3B6F8CBA3C258C259C4CDC25AF1E7C25BF1E8B8FBF1E9BAC4D4C5B0D2C25CC25DF1EAC25EC25FC260F1EBC261F1ECC262C263F1EDF1EEF1EFF1F1F1F0C5D5C264C265C266C267C268C269F1F2C26AB6FAC26BF1F4D2AEDEC7CBCAC26CC26DB3DCC26EB5A2C26FB9A2C270C271C4F4F1F5C272C273F1F6C274C275C276C1C4C1FBD6B0F1F7C277C278C279C27AF1F8C27BC1AAC27CC27DC27EC6B8C280BEDBC281C282C283C284C285C286C287C288C289C28AC28BC28CC28DC28EF1F9B4CFC28FC290C291C292C293C294F1FAC295C296C297C298C299C29AC29BC29CC29DC29EC29FC2A0C340EDB2EDB1C341C342CBE0D2DEC343CBC1D5D8C344C8E2C345C0DFBCA1C346C347C348C349C34AC34BEBC1C34CC34DD0A4C34ED6E2C34FB6C7B8D8EBC0B8CEC350EBBFB3A6B9C9D6ABC351B7F4B7CAC352C353C354BCE7B7BEEBC6C355EBC7B0B9BFCFC356EBC5D3FDC357EBC8C358C359EBC9C35AC35BB7CEC35CEBC2EBC4C9F6D6D7D5CDD0B2EBCFCEB8EBD0C35DB5A8C35EC35FC360C361C362B1B3EBD2CCA5C363C364C365C366C367C368C369C5D6EBD3C36AEBD1C5DFEBCECAA4EBD5B0FBC36BC36CBAFAC36DC36ED8B7F1E3C36FEBCAEBCBEBCCEBCDEBD6E6C0EBD9C370BFE8D2C8EBD7EBDCB8ECEBD8C371BDBAC372D0D8C373B0B7C374EBDDC4DCC375C376C377C378D6ACC379C37AC37BB4E0C37CC37DC2F6BCB9C37EC380EBDAEBDBD4E0C6EAC4D4EBDFC5A7D9F5C381B2B1C382EBE4C383BDC5C384C385C386EBE2C387C388C389C38AC38BC38CC38DC38EC38FC390C391C392C393EBE3C394C395B8ACC396CDD1EBE5C397C398C399EBE1C39AC1B3C39BC39CC39DC39EC39FC6A2C3A0C440C441C442C443C444C445CCF3C446EBE6C447C0B0D2B8EBE7C448C449C44AB8AFB8ADC44BEBE8C7BBCDF3C44CC44DC44EEBEAEBEBC44FC450C451C452C453EBEDC454C455C456C457D0C8C458EBF2C459EBEEC45AC45BC45CEBF1C8F9C45DD1FCEBECC45EC45FEBE9C460C461C462C463B8B9CFD9C4E5EBEFEBF0CCDACDC8B0F2C464EBF6C465C466C467C468C469EBF5C46AB2B2C46BC46CC46DC46EB8E0C46FEBF7C470C471C472C473C474C475B1ECC476C477CCC5C4A4CFA5C478C479C47AC47BC47CEBF9C47DC47EECA2C480C5F2C481EBFAC482C483C484C485C486C487C488C489C9C5C48AC48BC48CC48DC48EC48FE2DFEBFEC490C491C492C493CDCEECA1B1DBD3B7C494C495D2DCC496C497C498EBFDC499EBFBC49AC49BC49CC49DC49EC49FC4A0C540C541C542C543C544C545C546C547C548C549C54AC54BC54CC54DC54EB3BCC54FC550C551EAB0C552C553D7D4C554F4ABB3F4C555C556C557C558C559D6C1D6C2C55AC55BC55CC55DC55EC55FD5E9BECAC560F4A7C561D2A8F4A8F4A9C562F4AABECBD3DFC563C564C565C566C567C9E0C9E1C568C569F3C2C56ACAE6C56BCCF2C56CC56DC56EC56FC570C571E2B6CBB4C572CEE8D6DBC573F4ADF4AEF4AFC574C575C576C577F4B2C578BABDF4B3B0E3F4B0C579F4B1BDA2B2D5C57AF4B6F4B7B6E6B2B0CFCFF4B4B4ACC57BF4B5C57CC57DF4B8C57EC580C581C582C583F4B9C584C585CDA7C586F4BAC587F4BBC588C589C58AF4BCC58BC58CC58DC58EC58FC590C591C592CBD2C593F4BDC594C595C596C597F4BEC598C599C59AC59BC59CC59DC59EC59FF4BFC5A0C640C641C642C643F4DEC1BCBCE8C644C9ABD1DEE5F5C645C646C647C648DCB3D2D5C649C64ADCB4B0ACDCB5C64BC64CBDDAC64DDCB9C64EC64FC650D8C2C651DCB7D3F3C652C9D6DCBADCB6C653DCBBC3A2C654C655C656C657DCBCDCC5DCBDC658C659CEDFD6A5C65ADCCFC65BDCCDC65CC65DDCD2BDE6C2ABC65EDCB8DCCBDCCEDCBEB7D2B0C5DCC7D0BEDCC1BBA8C65FB7BCDCCCC660C661DCC6DCBFC7DBC662C663C664D1BFDCC0C665C666DCCAC667C668DCD0C669C66ACEADDCC2C66BDCC3DCC8DCC9B2D4DCD1CBD5C66CD4B7DCDBDCDFCCA6DCE6C66DC3E7DCDCC66EC66FBFC1DCD9C670B0FAB9B6DCE5DCD3C671DCC4DCD6C8F4BFE0C672C673C674C675C9BBC676C677C678B1BDC679D3A2C67AC67BDCDAC67CC67DDCD5C67EC6BBC680DCDEC681C682C683C684C685D7C2C3AFB7B6C7D1C3A9DCE2DCD8DCEBDCD4C686C687DCDDC688BEA5DCD7C689DCE0C68AC68BDCE3DCE4C68CDCF8C68DC68EDCE1DDA2DCE7C68FC690C691C692C693C694C695C696C697C698BCEBB4C4C699C69AC3A3B2E7DCFAC69BDCF2C69CDCEFC69DDCFCDCEED2F0B2E8C69EC8D7C8E3DCFBC69FDCEDC6A0C740C741DCF7C742C743DCF5C744C745BEA3DCF4C746B2DDC747C748C749C74AC74BDCF3BCF6DCE8BBC4C74CC0F3C74DC74EC74FC750C751BCD4DCE9DCEAC752DCF1DCF6DCF9B5B4C753C8D9BBE7DCFEDCFDD3ABDDA1DDA3DDA5D2F1DDA4DDA6DDA7D2A9C754C755C756C757C758C759C75ABAC9DDA9C75BC75CDDB6DDB1DDB4C75DC75EC75FC760C761C762C763DDB0C6CEC764C765C0F2C766C767C768C769C9AFC76AC76BC76CDCECDDAEC76DC76EC76FC770DDB7C771C772DCF0DDAFC773DDB8C774DDACC775C776C777C778C779C77AC77BDDB9DDB3DDADC4AAC77CC77DC77EC780DDA8C0B3C1ABDDAADDABC781DDB2BBF1DDB5D3A8DDBAC782DDBBC3A7C783C784DDD2DDBCC785C786C787DDD1C788B9BDC789C78ABED5C78BBEFAC78CC78DBACAC78EC78FC790C791DDCAC792DDC5C793DDBFC794C795C796B2CBDDC3C797DDCBB2A4DDD5C798C799C79ADDBEC79BC79CC79DC6D0DDD0C79EC79FC7A0C840C841DDD4C1E2B7C6C842C843C844C845C846DDCEDDCFC847C848C849DDC4C84AC84BC84CDDBDC84DDDCDCCD1C84EDDC9C84FC850C851C852DDC2C3C8C6BCCEAEDDCCC853DDC8C854C855C856C857C858C859DDC1C85AC85BC85CDDC6C2DCC85DC85EC85FC860C861C862D3A9D3AADDD3CFF4C8F8C863C864C865C866C867C868C869C86ADDE6C86BC86CC86DC86EC86FC870DDC7C871C872C873DDE0C2E4C874C875C876C877C878C879C87AC87BDDE1C87CC87DC87EC880C881C882C883C884C885C886DDD7C887C888C889C88AC88BD6F8C88CDDD9DDD8B8F0DDD6C88DC88EC88FC890C6CFC891B6ADC892C893C894C895C896DDE2C897BAF9D4E1DDE7C898C899C89AB4D0C89BDDDAC89CBFFBDDE3C89DDDDFC89EDDDDC89FC8A0C940C941C942C943C944B5D9C945C946C947C948DDDBDDDCDDDEC949BDAFDDE4C94ADDE5C94BC94CC94DC94EC94FC950C951C952DDF5C953C3C9C954C955CBE2C956C957C958C959DDF2C95AC95BC95CC95DC95EC95FC960C961C962C963C964C965C966D8E1C967C968C6D1C969DDF4C96AC96BC96CD5F4DDF3DDF0C96DC96EDDECC96FDDEFC970DDE8C971C972D0EEC973C974C975C976C8D8DDEEC977C978DDE9C979C97ADDEACBF2C97BDDEDC97CC97DB1CDC97EC980C981C982C983C984C0B6C985BCBBDDF1C986C987DDF7C988DDF6DDEBC989C98AC98BC98CC98DC5EEC98EC98FC990DDFBC991C992C993C994C995C996C997C998C999C99AC99BDEA4C99CC99DDEA3C99EC99FC9A0CA40CA41CA42CA43CA44CA45CA46CA47CA48DDF8CA49CA4ACA4BCA4CC3EFCA4DC2FBCA4ECA4FCA50D5E1CA51CA52CEB5CA53CA54CA55CA56DDFDCA57B2CCCA58CA59CA5ACA5BCA5CCA5DCA5ECA5FCA60C4E8CADFCA61CA62CA63CA64CA65CA66CA67CA68CA69CA6AC7BEDDFADDFCDDFEDEA2B0AAB1CECA6BCA6CCA6DCA6ECA6FDEACCA70CA71CA72CA73DEA6BDB6C8EFCA74CA75CA76CA77CA78CA79CA7ACA7BCA7CCA7DCA7EDEA1CA80CA81DEA5CA82CA83CA84CA85DEA9CA86CA87CA88CA89CA8ADEA8CA8BCA8CCA8DDEA7CA8ECA8FCA90CA91CA92CA93CA94CA95CA96DEADCA97D4CCCA98CA99CA9ACA9BDEB3DEAADEAECA9CCA9DC0D9CA9ECA9FCAA0CB40CB41B1A1DEB6CB42DEB1CB43CB44CB45CB46CB47CB48CB49DEB2CB4ACB4BCB4CCB4DCB4ECB4FCB50CB51CB52CB53CB54D1A6DEB5CB55CB56CB57CB58CB59CB5ACB5BDEAFCB5CCB5DCB5EDEB0CB5FD0BDCB60CB61CB62DEB4CAEDDEB9CB63CB64CB65CB66CB67CB68DEB8CB69DEB7CB6ACB6BCB6CCB6DCB6ECB6FCB70DEBBCB71CB72CB73CB74CB75CB76CB77BDE5CB78CB79CB7ACB7BCB7CB2D8C3EACB7DCB7EDEBACB80C5BACB81CB82CB83CB84CB85CB86DEBCCB87CB88CB89CB8ACB8BCB8CCB8DCCD9CB8ECB8FCB90CB91B7AACB92CB93CB94CB95CB96CB97CB98CB99CB9ACB9BCB9CCB9DCB9ECB9FCBA0CC40CC41D4E5CC42CC43CC44DEBDCC45CC46CC47CC48CC49DEBFCC4ACC4BCC4CCC4DCC4ECC4FCC50CC51CC52CC53CC54C4A2CC55CC56CC57CC58DEC1CC59CC5ACC5BCC5CCC5DCC5ECC5FCC60CC61CC62CC63CC64CC65CC66CC67CC68DEBECC69DEC0CC6ACC6BCC6CCC6DCC6ECC6FCC70CC71CC72CC73CC74CC75CC76CC77D5BACC78CC79CC7ADEC2CC7BCC7CCC7DCC7ECC80CC81CC82CC83CC84CC85CC86CC87CC88CC89CC8ACC8BF2AEBBA2C2B2C5B0C2C7CC8CCC8DF2AFCC8ECC8FCC90CC91CC92D0E9CC93CC94CC95D3DDCC96CC97CC98EBBDCC99CC9ACC9BCC9CCC9DCC9ECC9FCCA0B3E6F2B0CD40F2B1CD41CD42CAADCD43CD44CD45CD46CD47CD48CD49BAE7F2B3F2B5F2B4CBE4CFBAF2B2CAB4D2CFC2ECCD4ACD4BCD4CCD4DCD4ECD4FCD50CEC3F2B8B0F6F2B7CD51CD52CD53CD54CD55F2BECD56B2CFCD57CD58CD59CD5ACD5BCD5CD1C1F2BACD5DCD5ECD5FCD60CD61F2BCD4E9CD62CD63F2BBF2B6F2BFF2BDCD64F2B9CD65CD66F2C7F2C4F2C6CD67CD68F2CAF2C2F2C0CD69CD6ACD6BF2C5CD6CCD6DCD6ECD6FCD70D6FBCD71CD72CD73F2C1CD74C7F9C9DFCD75F2C8B9C6B5B0CD76CD77F2C3F2C9F2D0F2D6CD78CD79BBD7CD7ACD7BCD7CF2D5CDDCCD7DD6EBCD7ECD80F2D2F2D4CD81CD82CD83CD84B8F2CD85CD86CD87CD88F2CBCD89CD8ACD8BF2CEC2F9CD8CD5DDF2CCF2CDF2CFF2D3CD8DCD8ECD8FF2D9D3BCCD90CD91CD92CD93B6EACD94CAF1CD95B7E4F2D7CD96CD97CD98F2D8F2DAF2DDF2DBCD99CD9AF2DCCD9BCD9CCD9DCD9ED1D1F2D1CD9FCDC9CDA0CECFD6A9CE40F2E3CE41C3DBCE42F2E0CE43CE44C0AFF2ECF2DECE45F2E1CE46CE47CE48F2E8CE49CE4ACE4BCE4CF2E2CE4DCE4EF2E7CE4FCE50F2E6CE51CE52F2E9CE53CE54CE55F2DFCE56CE57F2E4F2EACE58CE59CE5ACE5BCE5CCE5DCE5ED3ACF2E5B2F5CE5FCE60F2F2CE61D0ABCE62CE63CE64CE65F2F5CE66CE67CE68BBC8CE69F2F9CE6ACE6BCE6CCE6DCE6ECE6FF2F0CE70CE71F2F6F2F8F2FACE72CE73CE74CE75CE76CE77CE78CE79F2F3CE7AF2F1CE7BCE7CCE7DBAFBCE7EB5FBCE80CE81CE82CE83F2EFF2F7F2EDF2EECE84CE85CE86F2EBF3A6CE87F3A3CE88CE89F3A2CE8ACE8BF2F4CE8CC8DACE8DCE8ECE8FCE90CE91F2FBCE92CE93CE94F3A5CE95CE96CE97CE98CE99CE9ACE9BC3F8CE9CCE9DCE9ECE9FCEA0CF40CF41CF42F2FDCF43CF44F3A7F3A9F3A4CF45F2FCCF46CF47CF48F3ABCF49F3AACF4ACF4BCF4CCF4DC2DDCF4ECF4FF3AECF50CF51F3B0CF52CF53CF54CF55CF56F3A1CF57CF58CF59F3B1F3ACCF5ACF5BCF5CCF5DCF5EF3AFF2FEF3ADCF5FCF60CF61CF62CF63CF64CF65F3B2CF66CF67CF68CF69F3B4CF6ACF6BCF6CCF6DF3A8CF6ECF6FCF70CF71F3B3CF72CF73CF74F3B5CF75CF76CF77CF78CF79CF7ACF7BCF7CCF7DCF7ED0B7CF80CF81CF82CF83F3B8CF84CF85CF86CF87D9F9CF88CF89CF8ACF8BCF8CCF8DF3B9CF8ECF8FCF90CF91CF92CF93CF94CF95F3B7CF96C8E4F3B6CF97CF98CF99CF9AF3BACF9BCF9CCF9DCF9ECF9FF3BBB4C0CFA0D040D041D042D043D044D045D046D047D048D049D04AD04BD04CD04DEEC3D04ED04FD050D051D052D053F3BCD054D055F3BDD056D057D058D1AAD059D05AD05BF4ACD0C6D05CD05DD05ED05FD060D061D0D0D1DCD062D063D064D065D066D067CFCED068D069BDD6D06AD1C3D06BD06CD06DD06ED06FD070D071BAE2E1E9D2C2F1C2B2B9D072D073B1EDF1C3D074C9C0B3C4D075D9F2D076CBA5D077F1C4D078D079D07AD07BD6D4D07CD07DD07ED080D081F1C5F4C0F1C6D082D4ACF1C7D083B0C0F4C1D084D085F4C2D086D087B4FCD088C5DBD089D08AD08BD08CCCBBD08DD08ED08FD0E4D090D091D092D093D094CDE0D095D096D097D098D099F1C8D09AD9F3D09BD09CD09DD09ED09FD0A0B1BBD140CFAED141D142D143B8A4D144D145D146D147D148F1CAD149D14AD14BD14CF1CBD14DD14ED14FD150B2C3C1D1D151D152D7B0F1C9D153D154F1CCD155D156D157D158F1CED159D15AD15BD9F6D15CD2E1D4A3D15DD15EF4C3C8B9D15FD160D161D162D163F4C4D164D165F1CDF1CFBFE3F1D0D166D167F1D4D168D169D16AD16BD16CD16DD16EF1D6F1D1D16FC9D1C5E1D170D171D172C2E3B9FCD173D174F1D3D175F1D5D176D177D178B9D3D179D17AD17BD17CD17DD17ED180F1DBD181D182D183D184D185BAD6D186B0FDF1D9D187D188D189D18AD18BF1D8F1D2F1DAD18CD18DD18ED18FD190F1D7D191D192D193C8ECD194D195D196D197CDCAF1DDD198D199D19AD19BE5BDD19CD19DD19EF1DCD19FF1DED1A0D240D241D242D243D244D245D246D247D248F1DFD249D24ACFE5D24BD24CD24DD24ED24FD250D251D252D253D254D255D256D257D258D259D25AD25BD25CD25DD25ED25FD260D261D262D263F4C5BDF3D264D265D266D267D268D269F1E0D26AD26BD26CD26DD26ED26FD270D271D272D273D274D275D276D277D278D279D27AD27BD27CD27DF1E1D27ED280D281CEF7D282D2AAD283F1FBD284D285B8B2D286D287D288D289D28AD28BD28CD28DD28ED28FD290D291D292D293D294D295D296D297D298D299D29AD29BD29CD29DD29ED29FD2A0D340D341D342D343D344D345D346D347D348D349D34AD34BD34CD34DD34ED34FD350D351D352D353D354D355D356D357D358D359D35AD35BD35CD35DD35EBCFBB9DBD35FB9E6C3D9CAD3EAE8C0C0BEF5EAE9EAEAEAEBD360EAECEAEDEAEEEAEFBDC7D361D362D363F5FBD364D365D366F5FDD367F5FED368F5FCD369D36AD36BD36CBDE2D36DF6A1B4A5D36ED36FD370D371F6A2D372D373D374F6A3D375D376D377ECB2D378D379D37AD37BD37CD37DD37ED380D381D382D383D384D1D4D385D386D387D388D389D38AD9EAD38BD38CD38DD38ED38FD390D391D392D393D394D395D396D397D398D399D39AD39BD39CD39DD39ED39FD3A0D440D441D442D443D444D445D446D447D448D449D44AD44BD44CD44DD44ED44FD450D451D452D453D454D455D456D457D458D459D45AD45BD45CD45DD45ED45FF6A4D460D461D462D463D464D465D466D467D468EEBAD469D46AD46BD46CD46DD46ED46FD470D471D472D473D474D475D476D477D478D479D47AD47BD47CD47DD47ED480D481D482D483D484D485D486D487D488D489D48AD48BD48CD48DD48ED48FD490D491D492D493D494D495D496D497D498D499D5B2D49AD49BD49CD49DD49ED49FD4A0D540D541D542D543D544D545D546D547D3FECCDCD548D549D54AD54BD54CD54DD54ED54FCAC4D550D551D552D553D554D555D556D557D558D559D55AD55BD55CD55DD55ED55FD560D561D562D563D564D565D566D567D568D569D56AD56BD56CD56DD56ED56FD570D571D572D573D574D575D576D577D578D579D57AD57BD57CD57DD57ED580D581D582D583D584D585D586D587D588D589D58AD58BD58CD58DD58ED58FD590D591D592D593D594D595D596D597D598D599D59AD59BD59CD59DD59ED59FD5A0D640D641D642D643D644D645D646D647D648D649D64AD64BD64CD64DD64ED64FD650D651D652D653D654D655D656D657D658D659D65AD65BD65CD65DD65ED65FD660D661D662E5C0D663D664D665D666D667D668D669D66AD66BD66CD66DD66ED66FD670D671D672D673D674D675D676D677D678D679D67AD67BD67CD67DD67ED680D681F6A5D682D683D684D685D686D687D688D689D68AD68BD68CD68DD68ED68FD690D691D692D693D694D695D696D697D698D699D69AD69BD69CD69DD69ED69FD6A0D740D741D742D743D744D745D746D747D748D749D74AD74BD74CD74DD74ED74FD750D751D752D753D754D755D756D757D758D759D75AD75BD75CD75DD75ED75FBEAFD760D761D762D763D764C6A9D765D766D767D768D769D76AD76BD76CD76DD76ED76FD770D771D772D773D774D775D776D777D778D779D77AD77BD77CD77DD77ED780D781D782D783D784D785D786D787D788D789D78AD78BD78CD78DD78ED78FD790D791D792D793D794D795D796D797D798DAA5BCC6B6A9B8BCC8CFBCA5DAA6DAA7CCD6C8C3DAA8C6FDD799D1B5D2E9D1B6BCC7D79ABDB2BBE4DAA9DAAAD1C8DAABD0EDB6EFC2DBD79BCBCFB7EDC9E8B7C3BEF7D6A4DAACDAADC6C0D7E7CAB6D79CD5A9CBDFD5EFDAAED6DFB4CADAB0DAAFD79DD2EBDAB1DAB2DAB3CAD4DAB4CAABDAB5DAB6B3CFD6EFDAB7BBB0B5AEDAB8DAB9B9EED1AFD2E8DABAB8C3CFEAB2EFDABBDABCD79EBDEBCEDCD3EFDABDCEF3DABED3D5BBE5DABFCBB5CBD0DAC0C7EBD6EEDAC1C5B5B6C1DAC2B7CCBFCEDAC3DAC4CBADDAC5B5F7DAC6C1C2D7BBDAC7CCB8D79FD2EAC4B1DAC8B5FDBBD1DAC9D0B3DACADACBCEBDDACCDACDDACEB2F7DAD1DACFD1E8DAD0C3D5DAD2D7A0DAD3DAD4DAD5D0BBD2A5B0F9DAD6C7ABDAD7BDF7C3A1DAD8DAD9C3FDCCB7DADADADBC0BEC6D7DADCDADDC7B4DADEDADFB9C8D840D841D842D843D844D845D846D847D848BBEDD849D84AD84BD84CB6B9F4F8D84DF4F9D84ED84FCDE3D850D851D852D853D854D855D856D857F5B9D858D859D85AD85BEBE0D85CD85DD85ED85FD860D861CFF3BBBFD862D863D864D865D866D867D868BAC0D4A5D869D86AD86BD86CD86DD86ED86FE1D9D870D871D872D873F5F4B1AAB2F2D874D875D876D877D878D879D87AF5F5D87BD87CF5F7D87DD87ED880BAD1F5F6D881C3B2D882D883D884D885D886D887D888F5F9D889D88AD88BF5F8D88CD88DD88ED88FD890D891D892D893D894D895D896D897D898D899D89AD89BD89CD89DD89ED89FD8A0D940D941D942D943D944D945D946D947D948D949D94AD94BD94CD94DD94ED94FD950D951D952D953D954D955D956D957D958D959D95AD95BD95CD95DD95ED95FD960D961D962D963D964D965D966D967D968D969D96AD96BD96CD96DD96ED96FD970D971D972D973D974D975D976D977D978D979D97AD97BD97CD97DD97ED980D981D982D983D984D985D986D987D988D989D98AD98BD98CD98DD98ED98FD990D991D992D993D994D995D996D997D998D999D99AD99BD99CD99DD99ED99FD9A0DA40DA41DA42DA43DA44DA45DA46DA47DA48DA49DA4ADA4BDA4CDA4DDA4EB1B4D5EAB8BADA4FB9B1B2C6D4F0CFCDB0DCD5CBBBF5D6CAB7B7CCB0C6B6B1E1B9BAD6FCB9E1B7A1BCFAEADAEADBCCF9B9F3EADCB4FBC3B3B7D1BAD8EADDD4F4EADEBCD6BBDFEADFC1DEC2B8D4DFD7CAEAE0EAE1EAE4EAE2EAE3C9DEB8B3B6C4EAE5CAEAC9CDB4CDDA50DA51E2D9C5E2EAE6C0B5DA52D7B8EAE7D7ACC8FCD8D3D8CDD4DEDA53D4F9C9C4D3AEB8D3B3E0DA54C9E2F4F6DA55DA56DA57BAD5DA58F4F7DA59DA5AD7DFDA5BDA5CF4F1B8B0D5D4B8CFC6F0DA5DDA5EDA5FDA60DA61DA62DA63DA64DA65B3C3DA66DA67F4F2B3ACDA68DA69DA6ADA6BD4BDC7F7DA6CDA6DDA6EDA6FDA70F4F4DA71DA72F4F3DA73DA74DA75DA76DA77DA78DA79DA7ADA7BDA7CCCCBDA7DDA7EDA80C8A4DA81DA82DA83DA84DA85DA86DA87DA88DA89DA8ADA8BDA8CDA8DF4F5DA8ED7E3C5BFF5C0DA8FDA90F5BBDA91F5C3DA92F5C2DA93D6BAF5C1DA94DA95DA96D4BEF5C4DA97F5CCDA98DA99DA9ADA9BB0CFB5F8DA9CF5C9F5CADA9DC5DCDA9EDA9FDAA0DB40F5C5F5C6DB41DB42F5C7F5CBDB43BEE0F5C8B8FADB44DB45DB46F5D0F5D3DB47DB48DB49BFE7DB4AB9F2F5BCF5CDDB4BDB4CC2B7DB4DDB4EDB4FCCF8DB50BCF9DB51F5CEF5CFF5D1B6E5F5D2DB52F5D5DB53DB54DB55DB56DB57DB58DB59F5BDDB5ADB5BDB5CF5D4D3BBDB5DB3ECDB5EDB5FCCA4DB60DB61DB62DB63F5D6DB64DB65DB66DB67DB68DB69DB6ADB6BF5D7BEE1F5D8DB6CDB6DCCDFF5DBDB6EDB6FDB70DB71DB72B2C8D7D9DB73F5D9DB74F5DAF5DCDB75F5E2DB76DB77DB78F5E0DB79DB7ADB7BF5DFF5DDDB7CDB7DF5E1DB7EDB80F5DEF5E4F5E5DB81CCE3DB82DB83E5BFB5B8F5E3F5E8CCA3DB84DB85DB86DB87DB88F5E6F5E7DB89DB8ADB8BDB8CDB8DDB8EF5BEDB8FDB90DB91DB92DB93DB94DB95DB96DB97DB98DB99DB9AB1C4DB9BDB9CF5BFDB9DDB9EB5C5B2E4DB9FF5ECF5E9DBA0B6D7DC40F5EDDC41F5EADC42DC43DC44DC45DC46F5EBDC47DC48B4DADC49D4EADC4ADC4BDC4CF5EEDC4DB3F9DC4EDC4FDC50DC51DC52DC53DC54F5EFF5F1DC55DC56DC57F5F0DC58DC59DC5ADC5BDC5CDC5DDC5EF5F2DC5FF5F3DC60DC61DC62DC63DC64DC65DC66DC67DC68DC69DC6ADC6BC9EDB9AADC6CDC6DC7FBDC6EDC6FB6E3DC70DC71DC72DC73DC74DC75DC76CCC9DC77DC78DC79DC7ADC7BDC7CDC7DDC7EDC80DC81DC82DC83DC84DC85DC86DC87DC88DC89DC8AEAA6DC8BDC8CDC8DDC8EDC8FDC90DC91DC92DC93DC94DC95DC96DC97DC98DC99DC9ADC9BDC9CDC9DDC9EDC9FDCA0DD40DD41DD42DD43DD44DD45DD46DD47DD48DD49DD4ADD4BDD4CDD4DDD4EDD4FDD50DD51DD52DD53DD54DD55DD56DD57DD58DD59DD5ADD5BDD5CDD5DDD5EDD5FDD60DD61DD62DD63DD64DD65DD66DD67DD68DD69DD6ADD6BDD6CDD6DDD6EDD6FDD70DD71DD72DD73DD74DD75DD76DD77DD78DD79DD7ADD7BDD7CDD7DDD7EDD80DD81DD82DD83DD84DD85DD86DD87DD88DD89DD8ADD8BDD8CDD8DDD8EDD8FDD90DD91DD92DD93DD94DD95DD96DD97DD98DD99DD9ADD9BDD9CDD9DDD9EDD9FDDA0DE40DE41DE42DE43DE44DE45DE46DE47DE48DE49DE4ADE4BDE4CDE4DDE4EDE4FDE50DE51DE52DE53DE54DE55DE56DE57DE58DE59DE5ADE5BDE5CDE5DDE5EDE5FDE60B3B5D4FEB9ECD0F9DE61E9EDD7AAE9EEC2D6C8EDBAE4E9EFE9F0E9F1D6E1E9F2E9F3E9F5E9F4E9F6E9F7C7E1E9F8D4D8E9F9BDCEDE62E9FAE9FBBDCFE9FCB8A8C1BEE9FDB1B2BBD4B9F5E9FEDE63EAA1EAA2EAA3B7F8BCADDE64CAE4E0CED4AFCFBDD5B7EAA4D5DEEAA5D0C1B9BCDE65B4C7B1D9DE66DE67DE68C0B1DE69DE6ADE6BDE6CB1E6B1E7DE6DB1E8DE6EDE6FDE70DE71B3BDC8E8DE72DE73DE74DE75E5C1DE76DE77B1DFDE78DE79DE7AC1C9B4EFDE7BDE7CC7A8D3D8DE7DC6F9D1B8DE7EB9FDC2F5DE80DE81DE82DE83DE84D3ADDE85D4CBBDFCDE86E5C2B7B5E5C3DE87DE88BBB9D5E2DE89BDF8D4B6CEA5C1ACB3D9DE8ADE8BCCF6DE8CE5C6E5C4E5C8DE8DE5CAE5C7B5CFC6C8DE8EB5FCE5C5DE8FCAF6DE90DE91E5C9DE92DE93DE94C3D4B1C5BCA3DE95DE96DE97D7B7DE98DE99CDCBCBCDCACACCD3E5CCE5CBC4E6DE9ADE9BD1A1D1B7E5CDDE9CE5D0DE9DCDB8D6F0E5CFB5DDDE9ECDBEDE9FE5D1B6BADEA0DF40CDA8B9E4DF41CAC5B3D1CBD9D4ECE5D2B7EADF42DF43DF44E5CEDF45DF46DF47DF48DF49DF4AE5D5B4FEE5D6DF4BDF4CDF4DDF4EDF4FE5D3E5D4DF50D2DDDF51DF52C2DFB1C6DF53D3E2DF54DF55B6DDCBECDF56E5D7DF57DF58D3F6DF59DF5ADF5BDF5CDF5DB1E9DF5EB6F4E5DAE5D8E5D9B5C0DF5FDF60DF61D2C5E5DCDF62DF63E5DEDF64DF65DF66DF67DF68DF69E5DDC7B2DF6AD2A3DF6BDF6CE5DBDF6DDF6EDF6FDF70D4E2D5DADF71DF72DF73DF74DF75E5E0D7F1DF76DF77DF78DF79DF7ADF7BDF7CE5E1DF7DB1DCD1FBDF7EE5E2E5E4DF80DF81DF82DF83E5E3DF84DF85E5E5DF86DF87DF88DF89DF8AD2D8DF8BB5CBDF8CE7DFDF8DDAF5DF8EDAF8DF8FDAF6DF90DAF7DF91DF92DF93DAFAD0CFC4C7DF94DF95B0EEDF96DF97DF98D0B0DF99DAF9DF9AD3CABAAADBA2C7F1DF9BDAFCDAFBC9DBDAFDDF9CDBA1D7DEDAFEC1DADF9DDF9EDBA5DF9FDFA0D3F4E040E041DBA7DBA4E042DBA8E043E044BDBCE045E046E047C0C9DBA3DBA6D6A3E048DBA9E049E04AE04BDBADE04CE04DE04EDBAEDBACBAC2E04FE050E051BFA4DBABE052E053E054DBAAD4C7B2BFE055E056DBAFE057B9F9E058DBB0E059E05AE05BE05CB3BBE05DE05EE05FB5A6E060E061E062E063B6BCDBB1E064E065E066B6F5E067DBB2E068E069E06AE06BE06CE06DE06EE06FE070E071E072E073E074E075E076E077E078E079E07AE07BB1C9E07CE07DE07EE080DBB4E081E082E083DBB3DBB5E084E085E086E087E088E089E08AE08BE08CE08DE08EDBB7E08FDBB6E090E091E092E093E094E095E096DBB8E097E098E099E09AE09BE09CE09DE09EE09FDBB9E0A0E140DBBAE141E142D3CFF4FAC7F5D7C3C5E4F4FCF4FDF4FBE143BEC6E144E145E146E147D0EFE148E149B7D3E14AE14BD4CDCCAAE14CE14DF5A2F5A1BAA8F4FECBD6E14EE14FE150F5A4C0D2E151B3EAE152CDAAF5A5F5A3BDB4F5A8E153F5A9BDCDC3B8BFE1CBE1F5AAE154E155E156F5A6F5A7C4F0E157E158E159E15AE15BF5ACE15CB4BCE15DD7EDE15EB4D7F5ABF5AEE15FE160F5ADF5AFD0D1E161E162E163E164E165E166E167C3D1C8A9E168E169E16AE16BE16CE16DF5B0F5B1E16EE16FE170E171E172E173F5B2E174E175F5B3F5B4F5B5E176E177E178E179F5B7F5B6E17AE17BE17CE17DF5B8E17EE180E181E182E183E184E185E186E187E188E189E18AB2C9E18BD3D4CACDE18CC0EFD6D8D2B0C1BFE18DBDF0E18EE18FE190E191E192E193E194E195E196E197B8AAE198E199E19AE19BE19CE19DE19EE19FE1A0E240E241E242E243E244E245E246E247E248E249E24AE24BE24CE24DE24EE24FE250E251E252E253E254E255E256E257E258E259E25AE25BE25CE25DE25EE25FE260E261E262E263E264E265E266E267E268E269E26AE26BE26CE26DE26EE26FE270E271E272E273E274E275E276E277E278E279E27AE27BE27CE27DE27EE280E281E282E283E284E285E286E287E288E289E28AE28BE28CE28DE28EE28FE290E291E292E293E294E295E296E297E298E299E29AE29BE29CE29DE29EE29FE2A0E340E341E342E343E344E345E346E347E348E349E34AE34BE34CE34DE34EE34FE350E351E352E353E354E355E356E357E358E359E35AE35BE35CE35DE35EE35FE360E361E362E363E364E365E366E367E368E369E36AE36BE36CE36DBCF8E36EE36FE370E371E372E373E374E375E376E377E378E379E37AE37BE37CE37DE37EE380E381E382E383E384E385E386E387F6C6E388E389E38AE38BE38CE38DE38EE38FE390E391E392E393E394E395E396E397E398E399E39AE39BE39CE39DE39EE39FE3A0E440E441E442E443E444E445F6C7E446E447E448E449E44AE44BE44CE44DE44EE44FE450E451E452E453E454E455E456E457E458E459E45AE45BE45CE45DE45EF6C8E45FE460E461E462E463E464E465E466E467E468E469E46AE46BE46CE46DE46EE46FE470E471E472E473E474E475E476E477E478E479E47AE47BE47CE47DE47EE480E481E482E483E484E485E486E487E488E489E48AE48BE48CE48DE48EE48FE490E491E492E493E494E495E496E497E498E499E49AE49BE49CE49DE49EE49FE4A0E540E541E542E543E544E545E546E547E548E549E54AE54BE54CE54DE54EE54FE550E551E552E553E554E555E556E557E558E559E55AE55BE55CE55DE55EE55FE560E561E562E563E564E565E566E567E568E569E56AE56BE56CE56DE56EE56FE570E571E572E573F6C9E574E575E576E577E578E579E57AE57BE57CE57DE57EE580E581E582E583E584E585E586E587E588E589E58AE58BE58CE58DE58EE58FE590E591E592E593E594E595E596E597E598E599E59AE59BE59CE59DE59EE59FF6CAE5A0E640E641E642E643E644E645E646E647E648E649E64AE64BE64CE64DE64EE64FE650E651E652E653E654E655E656E657E658E659E65AE65BE65CE65DE65EE65FE660E661E662F6CCE663E664E665E666E667E668E669E66AE66BE66CE66DE66EE66FE670E671E672E673E674E675E676E677E678E679E67AE67BE67CE67DE67EE680E681E682E683E684E685E686E687E688E689E68AE68BE68CE68DE68EE68FE690E691E692E693E694E695E696E697E698E699E69AE69BE69CE69DF6CBE69EE69FE6A0E740E741E742E743E744E745E746E747F7E9E748E749E74AE74BE74CE74DE74EE74FE750E751E752E753E754E755E756E757E758E759E75AE75BE75CE75DE75EE75FE760E761E762E763E764E765E766E767E768E769E76AE76BE76CE76DE76EE76FE770E771E772E773E774E775E776E777E778E779E77AE77BE77CE77DE77EE780E781E782E783E784E785E786E787E788E789E78AE78BE78CE78DE78EE78FE790E791E792E793E794E795E796E797E798E799E79AE79BE79CE79DE79EE79FE7A0E840E841E842E843E844E845E846E847E848E849E84AE84BE84CE84DE84EF6CDE84FE850E851E852E853E854E855E856E857E858E859E85AE85BE85CE85DE85EE85FE860E861E862E863E864E865E866E867E868E869E86AE86BE86CE86DE86EE86FE870E871E872E873E874E875E876E877E878E879E87AF6CEE87BE87CE87DE87EE880E881E882E883E884E885E886E887E888E889E88AE88BE88CE88DE88EE88FE890E891E892E893E894EEC4EEC5EEC6D5EBB6A4EEC8EEC7EEC9EECAC7A5EECBEECCE895B7B0B5F6EECDEECFE896EECEE897B8C6EED0EED1EED2B6DBB3AED6D3C4C6B1B5B8D6EED3EED4D4BFC7D5BEFBCED9B9B3EED6EED5EED8EED7C5A5EED9EEDAC7AEEEDBC7AFEEDCB2A7EEDDEEDEEEDFEEE0EEE1D7EAEEE2EEE3BCD8EEE4D3CBCCFAB2ACC1E5EEE5C7A6C3ADE898EEE6EEE7EEE8EEE9EEEAEEEBEEECE899EEEDEEEEEEEFE89AE89BEEF0EEF1EEF2EEF4EEF3E89CEEF5CDADC2C1EEF6EEF7EEF8D5A1EEF9CFB3EEFAEEFBE89DEEFCEEFDEFA1EEFEEFA2B8F5C3FAEFA3EFA4BDC2D2BFB2F9EFA5EFA6EFA7D2F8EFA8D6FDEFA9C6CCE89EEFAAEFABC1B4EFACCFFACBF8EFAEEFADB3FAB9F8EFAFEFB0D0E2EFB1EFB2B7E6D0BFEFB3EFB4EFB5C8F1CCE0EFB6EFB7EFB8EFB9EFBAD5E0EFBBB4EDC3AAEFBCE89FEFBDEFBEEFBFE8A0CEFDEFC0C2E0B4B8D7B6BDF5E940CFC7EFC3EFC1EFC2EFC4B6A7BCFCBEE2C3CCEFC5EFC6E941EFC7EFCFEFC8EFC9EFCAC7C2EFF1B6CDEFCBE942EFCCEFCDB6C6C3BEEFCEE943EFD0EFD1EFD2D5F2E944EFD3C4F7E945EFD4C4F8EFD5EFD6B8E4B0F7EFD7EFD8EFD9E946EFDAEFDBEFDCEFDDE947EFDEBEB5EFE1EFDFEFE0E948EFE2EFE3C1CDEFE4EFE5EFE6EFE7EFE8EFE9EFEAEFEBEFECC0D8E949EFEDC1ADEFEEEFEFEFF0E94AE94BCFE2E94CE94DE94EE94FE950E951E952E953B3A4E954E955E956E957E958E959E95AE95BE95CE95DE95EE95FE960E961E962E963E964E965E966E967E968E969E96AE96BE96CE96DE96EE96FE970E971E972E973E974E975E976E977E978E979E97AE97BE97CE97DE97EE980E981E982E983E984E985E986E987E988E989E98AE98BE98CE98DE98EE98FE990E991E992E993E994E995E996E997E998E999E99AE99BE99CE99DE99EE99FE9A0EA40EA41EA42EA43EA44EA45EA46EA47EA48EA49EA4AEA4BEA4CEA4DEA4EEA4FEA50EA51EA52EA53EA54EA55EA56EA57EA58EA59EA5AEA5BC3C5E3C5C9C1E3C6EA5CB1D5CECAB4B3C8F2E3C7CFD0E3C8BCE4E3C9E3CAC3C6D5A2C4D6B9EBCEC5E3CBC3F6E3CCEA5DB7A7B8F3BAD2E3CDE3CED4C4E3CFEA5EE3D0D1CBE3D1E3D2E3D3E3D4D1D6E3D5B2FBC0BBE3D6EA5FC0ABE3D7E3D8E3D9EA60E3DAE3DBEA61B8B7DAE2EA62B6D3EA63DAE4DAE3EA64EA65EA66EA67EA68EA69EA6ADAE6EA6BEA6CEA6DC8EEEA6EEA6FDAE5B7C0D1F4D2F5D5F3BDD7EA70EA71EA72EA73D7E8DAE8DAE7EA74B0A2CDD3EA75DAE9EA76B8BDBCCAC2BDC2A4B3C2DAEAEA77C2AAC4B0BDB5EA78EA79CFDEEA7AEA7BEA7CDAEBC9C2EA7DEA7EEA80EA81EA82B1DDEA83EA84EA85DAECEA86B6B8D4BAEA87B3FDEA88EA89DAEDD4C9CFD5C5E3EA8ADAEEEA8BEA8CEA8DEA8EEA8FDAEFEA90DAF0C1EACCD5CFDDEA91EA92EA93EA94EA95EA96EA97EA98EA99EA9AEA9BEA9CEA9DD3E7C2A1EA9EDAF1EA9FEAA0CBE5EB40DAF2EB41CBE6D2FEEB42EB43EB44B8F4EB45EB46DAF3B0AFCFB6EB47EB48D5CFEB49EB4AEB4BEB4CEB4DEB4EEB4FEB50EB51EB52CBEDEB53EB54EB55EB56EB57EB58EB59EB5ADAF4EB5BEB5CE3C4EB5DEB5EC1A5EB5FEB60F6BFEB61EB62F6C0F6C1C4D1EB63C8B8D1E3EB64EB65D0DBD1C5BCAFB9CDEB66EFF4EB67EB68B4C6D3BAF6C2B3FBEB69EB6AF6C3EB6BEB6CB5F1EB6DEB6EEB6FEB70EB71EB72EB73EB74EB75EB76F6C5EB77EB78EB79EB7AEB7BEB7CEB7DD3EAF6A7D1A9EB7EEB80EB81EB82F6A9EB83EB84EB85F6A8EB86EB87C1E3C0D7EB88B1A2EB89EB8AEB8BEB8CCEEDEB8DD0E8F6ABEB8EEB8FCFF6EB90F6AAD5F0F6ACC3B9EB91EB92EB93BBF4F6AEF6ADEB94EB95EB96C4DEEB97EB98C1D8EB99EB9AEB9BEB9CEB9DCBAAEB9ECFBCEB9FEBA0EC40EC41EC42EC43EC44EC45EC46EC47EC48F6AFEC49EC4AF6B0EC4BEC4CF6B1EC4DC2B6EC4EEC4FEC50EC51EC52B0D4C5F9EC53EC54EC55EC56F6B2EC57EC58EC59EC5AEC5BEC5CEC5DEC5EEC5FEC60EC61EC62EC63EC64EC65EC66EC67EC68EC69C7E0F6A6EC6AEC6BBEB8EC6CEC6DBEB2EC6EB5E5EC6FEC70B7C7EC71BFBFC3D2C3E6EC72EC73D8CCEC74EC75EC76B8EFEC77EC78EC79EC7AEC7BEC7CEC7DEC7EEC80BDF9D1A5EC81B0D0EC82EC83EC84EC85EC86F7B0EC87EC88EC89EC8AEC8BEC8CEC8DEC8EF7B1EC8FEC90EC91EC92EC93D0ACEC94B0B0EC95EC96EC97F7B2F7B3EC98F7B4EC99EC9AEC9BC7CAEC9CEC9DEC9EEC9FECA0ED40ED41BECFED42ED43F7B7ED44ED45ED46ED47ED48ED49ED4AF7B6ED4BB1DEED4CF7B5ED4DED4EF7B8ED4FF7B9ED50ED51ED52ED53ED54ED55ED56ED57ED58ED59ED5AED5BED5CED5DED5EED5FED60ED61ED62ED63ED64ED65ED66ED67ED68ED69ED6AED6BED6CED6DED6EED6FED70ED71ED72ED73ED74ED75ED76ED77ED78ED79ED7AED7BED7CED7DED7EED80ED81CEA4C8CDED82BAABE8B8E8B9E8BABEC2ED83ED84ED85ED86ED87D2F4ED88D4CFC9D8ED89ED8AED8BED8CED8DED8EED8FED90ED91ED92ED93ED94ED95ED96ED97ED98ED99ED9AED9BED9CED9DED9EED9FEDA0EE40EE41EE42EE43EE44EE45EE46EE47EE48EE49EE4AEE4BEE4CEE4DEE4EEE4FEE50EE51EE52EE53EE54EE55EE56EE57EE58EE59EE5AEE5BEE5CEE5DEE5EEE5FEE60EE61EE62EE63EE64EE65EE66EE67EE68EE69EE6AEE6BEE6CEE6DEE6EEE6FEE70EE71EE72EE73EE74EE75EE76EE77EE78EE79EE7AEE7BEE7CEE7DEE7EEE80EE81EE82EE83EE84EE85EE86EE87EE88EE89EE8AEE8BEE8CEE8DEE8EEE8FEE90EE91EE92EE93EE94EE95EE96EE97EE98EE99EE9AEE9BEE9CEE9DEE9EEE9FEEA0EF40EF41EF42EF43EF44EF45D2B3B6A5C7EAF1FCCFEECBB3D0EBE7EFCDE7B9CBB6D9F1FDB0E4CBCCF1FED4A4C2ADC1ECC6C4BEB1F2A1BCD5EF46F2A2F2A3EF47F2A4D2C3C6B5EF48CDC7F2A5EF49D3B1BFC5CCE2EF4AF2A6F2A7D1D5B6EEF2A8F2A9B5DFF2AAF2ABEF4BB2FCF2ACF2ADC8A7EF4CEF4DEF4EEF4FEF50EF51EF52EF53EF54EF55EF56EF57EF58EF59EF5AEF5BEF5CEF5DEF5EEF5FEF60EF61EF62EF63EF64EF65EF66EF67EF68EF69EF6AEF6BEF6CEF6DEF6EEF6FEF70EF71B7E7EF72EF73ECA9ECAAECABEF74ECACEF75EF76C6AEECADECAEEF77EF78EF79B7C9CAB3EF7AEF7BEF7CEF7DEF7EEF80EF81E2B8F7CFEF82EF83EF84EF85EF86EF87EF88EF89EF8AEF8BEF8CEF8DEF8EEF8FEF90EF91EF92EF93EF94EF95EF96EF97EF98EF99EF9AEF9BEF9CEF9DEF9EEF9FEFA0F040F041F042F043F044F7D0F045F046B2CDF047F048F049F04AF04BF04CF04DF04EF04FF050F051F052F053F054F055F056F057F058F059F05AF05BF05CF05DF05EF05FF060F061F062F063F7D1F064F065F066F067F068F069F06AF06BF06CF06DF06EF06FF070F071F072F073F074F075F076F077F078F079F07AF07BF07CF07DF07EF080F081F082F083F084F085F086F087F088F089F7D3F7D2F08AF08BF08CF08DF08EF08FF090F091F092F093F094F095F096E2BBF097BCA2F098E2BCE2BDE2BEE2BFE2C0E2C1B7B9D2FBBDA4CACEB1A5CBC7F099E2C2B6FCC8C4E2C3F09AF09BBDC8F09CB1FDE2C4F09DB6F6E2C5C4D9F09EF09FE2C6CFDAB9DDE2C7C0A1F0A0E2C8B2F6F140E2C9F141C1F3E2CAE2CBC2F8E2CCE2CDE2CECAD7D8B8D9E5CFE3F142F143F144F145F146F147F148F149F14AF14BF14CF0A5F14DF14EDCB0F14FF150F151F152F153F154F155F156F157F158F159F15AF15BF15CF15DF15EF15FF160F161F162F163F164F165F166F167F168F169F16AF16BF16CF16DF16EF16FF170F171F172F173F174F175F176F177F178F179F17AF17BF17CF17DF17EF180F181F182F183F184F185F186F187F188F189F18AF18BF18CF18DF18EF18FF190F191F192F193F194F195F196F197F198F199F19AF19BF19CF19DF19EF19FF1A0F240F241F242F243F244F245F246F247F248F249F24AF24BF24CF24DF24EF24FF250F251F252F253F254F255F256F257F258F259F25AF25BF25CF25DF25EF25FF260F261F262F263F264F265F266F267F268F269F26AF26BF26CF26DF26EF26FF270F271F272F273F274F275F276F277F278F279F27AF27BF27CF27DF27EF280F281F282F283F284F285F286F287F288F289F28AF28BF28CF28DF28EF28FF290F291F292F293F294F295F296F297F298F299F29AF29BF29CF29DF29EF29FF2A0F340F341F342F343F344F345F346F347F348F349F34AF34BF34CF34DF34EF34FF350F351C2EDD4A6CDD4D1B1B3DBC7FDF352B2B5C2BFE6E0CABBE6E1E6E2BED4E6E3D7A4CDD5E6E5BCDDE6E4E6E6E6E7C2EEF353BDBEE6E8C2E6BAA7E6E9F354E6EAB3D2D1E9F355F356BFA5E6EBC6EFE6ECE6EDF357F358E6EEC6ADE6EFF359C9A7E6F0E6F1E6F2E5B9E6F3E6F4C2E2E6F5E6F6D6E8E6F7F35AE6F8B9C7F35BF35CF35DF35EF35FF360F361F7BBF7BAF362F363F364F365F7BEF7BCBAA1F366F7BFF367F7C0F368F369F36AF7C2F7C1F7C4F36BF36CF7C3F36DF36EF36FF370F371F7C5F7C6F372F373F374F375F7C7F376CBE8F377F378F379F37AB8DFF37BF37CF37DF37EF380F381F7D4F382F7D5F383F384F385F386F7D6F387F388F389F38AF7D8F38BF7DAF38CF7D7F38DF38EF38FF390F391F392F393F394F395F7DBF396F7D9F397F398F399F39AF39BF39CF39DD7D7F39EF39FF3A0F440F7DCF441F442F443F444F445F446F7DDF447F448F449F7DEF44AF44BF44CF44DF44EF44FF450F451F452F453F454F7DFF455F456F457F7E0F458F459F45AF45BF45CF45DF45EF45FF460F461F462DBCBF463F464D8AAF465F466F467F468F469F46AF46BF46CE5F7B9EDF46DF46EF46FF470BFFDBBEAF7C9C6C7F7C8F471F7CAF7CCF7CBF472F473F474F7CDF475CEBAF476F7CEF477F478C4A7F479F47AF47BF47CF47DF47EF480F481F482F483F484F485F486F487F488F489F48AF48BF48CF48DF48EF48FF490F491F492F493F494F495F496F497F498F499F49AF49BF49CF49DF49EF49FF4A0F540F541F542F543F544F545F546F547F548F549F54AF54BF54CF54DF54EF54FF550F551F552F553F554F555F556F557F558F559F55AF55BF55CF55DF55EF55FF560F561F562F563F564F565F566F567F568F569F56AF56BF56CF56DF56EF56FF570F571F572F573F574F575F576F577F578F579F57AF57BF57CF57DF57EF580F581F582F583F584F585F586F587F588F589F58AF58BF58CF58DF58EF58FF590F591F592F593F594F595F596F597F598F599F59AF59BF59CF59DF59EF59FF5A0F640F641F642F643F644F645F646F647F648F649F64AF64BF64CF64DF64EF64FF650F651F652F653F654F655F656F657F658F659F65AF65BF65CF65DF65EF65FF660F661F662F663F664F665F666F667F668F669F66AF66BF66CF66DF66EF66FF670F671F672F673F674F675F676F677F678F679F67AF67BF67CF67DF67EF680F681F682F683F684F685F686F687F688F689F68AF68BF68CF68DF68EF68FF690F691F692F693F694F695F696F697F698F699F69AF69BF69CF69DF69EF69FF6A0F740F741F742F743F744F745F746F747F748F749F74AF74BF74CF74DF74EF74FF750F751F752F753F754F755F756F757F758F759F75AF75BF75CF75DF75EF75FF760F761F762F763F764F765F766F767F768F769F76AF76BF76CF76DF76EF76FF770F771F772F773F774F775F776F777F778F779F77AF77BF77CF77DF77EF780D3E3F781F782F6CFF783C2B3F6D0F784F785F6D1F6D2F6D3F6D4F786F787F6D6F788B1ABF6D7F789F6D8F6D9F6DAF78AF6DBF6DCF78BF78CF78DF78EF6DDF6DECFCAF78FF6DFF6E0F6E1F6E2F6E3F6E4C0F0F6E5F6E6F6E7F6E8F6E9F790F6EAF791F6EBF6ECF792F6EDF6EEF6EFF6F0F6F1F6F2F6F3F6F4BEA8F793F6F5F6F6F6F7F6F8F794F795F796F797F798C8FAF6F9F6FAF6FBF6FCF799F79AF6FDF6FEF7A1F7A2F7A3F7A4F7A5F79BF79CF7A6F7A7F7A8B1EEF7A9F7AAF7ABF79DF79EF7ACF7ADC1DBF7AEF79FF7A0F7AFF840F841F842F843F844F845F846F847F848F849F84AF84BF84CF84DF84EF84FF850F851F852F853F854F855F856F857F858F859F85AF85BF85CF85DF85EF85FF860F861F862F863F864F865F866F867F868F869F86AF86BF86CF86DF86EF86FF870F871F872F873F874F875F876F877F878F879F87AF87BF87CF87DF87EF880F881F882F883F884F885F886F887F888F889F88AF88BF88CF88DF88EF88FF890F891F892F893F894F895F896F897F898F899F89AF89BF89CF89DF89EF89FF8A0F940F941F942F943F944F945F946F947F948F949F94AF94BF94CF94DF94EF94FF950F951F952F953F954F955F956F957F958F959F95AF95BF95CF95DF95EF95FF960F961F962F963F964F965F966F967F968F969F96AF96BF96CF96DF96EF96FF970F971F972F973F974F975F976F977F978F979F97AF97BF97CF97DF97EF980F981F982F983F984F985F986F987F988F989F98AF98BF98CF98DF98EF98FF990F991F992F993F994F995F996F997F998F999F99AF99BF99CF99DF99EF99FF9A0FA40FA41FA42FA43FA44FA45FA46FA47FA48FA49FA4AFA4BFA4CFA4DFA4EFA4FFA50FA51FA52FA53FA54FA55FA56FA57FA58FA59FA5AFA5BFA5CFA5DFA5EFA5FFA60FA61FA62FA63FA64FA65FA66FA67FA68FA69FA6AFA6BFA6CFA6DFA6EFA6FFA70FA71FA72FA73FA74FA75FA76FA77FA78FA79FA7AFA7BFA7CFA7DFA7EFA80FA81FA82FA83FA84FA85FA86FA87FA88FA89FA8AFA8BFA8CFA8DFA8EFA8FFA90FA91FA92FA93FA94FA95FA96FA97FA98FA99FA9AFA9BFA9CFA9DFA9EFA9FFAA0FB40FB41FB42FB43FB44FB45FB46FB47FB48FB49FB4AFB4BFB4CFB4DFB4EFB4FFB50FB51FB52FB53FB54FB55FB56FB57FB58FB59FB5AFB5BC4F1F0AFBCA6F0B0C3F9FB5CC5B8D1BBFB5DF0B1F0B2F0B3F0B4F0B5D1BCFB5ED1ECFB5FF0B7F0B6D4A7FB60CDD2F0B8F0BAF0B9F0BBF0BCFB61FB62B8EBF0BDBAE8FB63F0BEF0BFBEE9F0C0B6ECF0C1F0C2F0C3F0C4C8B5F0C5F0C6FB64F0C7C5F4FB65F0C8FB66FB67FB68F0C9FB69F0CAF7BDFB6AF0CBF0CCF0CDFB6BF0CEFB6CFB6DFB6EFB6FF0CFBAD7FB70F0D0F0D1F0D2F0D3F0D4F0D5F0D6F0D8FB71FB72D3A5F0D7FB73F0D9FB74FB75FB76FB77FB78FB79FB7AFB7BFB7CFB7DF5BAC2B9FB7EFB80F7E4FB81FB82FB83FB84F7E5F7E6FB85FB86F7E7FB87FB88FB89FB8AFB8BFB8CF7E8C2B4FB8DFB8EFB8FFB90FB91FB92FB93FB94FB95F7EAFB96F7EBFB97FB98FB99FB9AFB9BFB9CC2F3FB9DFB9EFB9FFBA0FC40FC41FC42FC43FC44FC45FC46FC47FC48F4F0FC49FC4AFC4BF4EFFC4CFC4DC2E9FC4EF7E1F7E2FC4FFC50FC51FC52FC53BBC6FC54FC55FC56FC57D9E4FC58FC59FC5ACAF2C0E8F0A4FC5BBADAFC5CFC5DC7ADFC5EFC5FFC60C4ACFC61FC62F7ECF7EDF7EEFC63F7F0F7EFFC64F7F1FC65FC66F7F4FC67F7F3FC68F7F2F7F5FC69FC6AFC6BFC6CF7F6FC6DFC6EFC6FFC70FC71FC72FC73FC74FC75EDE9FC76EDEAEDEBFC77F6BCFC78FC79FC7AFC7BFC7CFC7DFC7EFC80FC81FC82FC83FC84F6BDFC85F6BEB6A6FC86D8BEFC87FC88B9C4FC89FC8AFC8BD8BBFC8CDCB1FC8DFC8EFC8FFC90FC91FC92CAF3FC93F7F7FC94FC95FC96FC97FC98FC99FC9AFC9BFC9CF7F8FC9DFC9EF7F9FC9FFCA0FD40FD41FD42FD43FD44F7FBFD45F7FAFD46B1C7FD47F7FCF7FDFD48FD49FD4AFD4BFD4CF7FEFD4DFD4EFD4FFD50FD51FD52FD53FD54FD55FD56FD57C6EBECB4FD58FD59FD5AFD5BFD5CFD5DFD5EFD5FFD60FD61FD62FD63FD64FD65FD66FD67FD68FD69FD6AFD6BFD6CFD6DFD6EFD6FFD70FD71FD72FD73FD74FD75FD76FD77FD78FD79FD7AFD7BFD7CFD7DFD7EFD80FD81FD82FD83FD84FD85B3DDF6B3FD86FD87F6B4C1E4F6B5F6B6F6B7F6B8F6B9F6BAC8A3F6BBFD88FD89FD8AFD8BFD8CFD8DFD8EFD8FFD90FD91FD92FD93C1FAB9A8EDE8FD94FD95FD96B9EAD9DFFD97FD98FD99FD9AFD9'; for (var i = 0; i < str.length; i++) { var c = str.charAt(i), code = str.charCodeAt(i); if (c == " ") strOut += "+"; else if (code >= 19968 && code <= 40869) { var index = code - 19968; strOut += "%" + z.substr(index * 4, 2) + "%" + z.substr(index * 4 + 2, 2); } else { strOut += "%" + str.charCodeAt(i).toString(16); } } return strOut; }, /* 改变图片大小 */ scale: function (img, w, h) { var ow = img.width, oh = img.height; if (ow >= oh) { img.width = w * ow / oh; img.height = h; img.style.marginLeft = '-' + parseInt((img.width - w) / 2) + 'px'; } else { img.width = w; img.height = h * oh / ow; img.style.marginTop = '-' + parseInt((img.height - h) / 2) + 'px'; } }, getImageData: function(){ var _this = this, key = $G('searchTxt').value, type = $G('searchType').value, keepOriginName = editor.options.keepOriginName ? "1" : "0", url = "http://image.baidu.com/i?ct=201326592&cl=2&lm=-1&st=-1&tn=baiduimagejson&istype=2&rn=32&fm=index&pv=&word=" + _this.encodeToGb2312(key) + type + "&keeporiginname=" + keepOriginName + "&" + +new Date; $G('searchListUl').innerHTML = lang.searchLoading; ajax.request(url, { 'dataType': 'jsonp', 'charset': 'GB18030', 'onsuccess':function(json){ var list = []; if(json && json.data) { for(var i = 0; i < json.data.length; i++) { if(json.data[i].objURL) { list.push({ title: json.data[i].fromPageTitleEnc, src: json.data[i].objURL, url: json.data[i].fromURL }); } } } _this.setList(list); }, 'onerror':function(){ $G('searchListUl').innerHTML = lang.searchRetry; } }); }, /* 添加图片到列表界面上 */ setList: function (list) { var i, item, p, img, link, _this = this, listUl = $G('searchListUl'); listUl.innerHTML = ''; if(list.length) { for (i = 0; i < list.length; i++) { item = document.createElement('li'); p = document.createElement('p'); img = document.createElement('img'); link = document.createElement('a'); img.onload = function () { _this.scale(this, 113, 113); }; img.width = 113; img.setAttribute('src', list[i].src); link.href = list[i].url; link.target = '_blank'; link.title = list[i].title; link.innerHTML = list[i].title; p.appendChild(img); item.appendChild(p); item.appendChild(link); listUl.appendChild(item); } } else { listUl.innerHTML = lang.searchRetry; } }, getInsertList: function () { var child, src, align = getAlign(), list = [], items = $G('searchListUl').children; for(var i = 0; i < items.length; i++) { child = items[i].firstChild && items[i].firstChild.firstChild; if(child.tagName && child.tagName.toLowerCase() == 'img' && domUtils.hasClass(items[i], 'selected')) { src = child.src; list.push({ src: src, _src: src, alt: src.substr(src.lastIndexOf('/') + 1), floatStyle: align }); } } return list; } }; })(); ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/insertframe/insertframe.html ================================================
    px
    px
    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/internal.js ================================================ (function () { /* eslint-disable */ if (window.frameElement.id) { let parent = window.parent, dialog = parent.$EDITORUI[window.frameElement.id.replace(/_iframe$/, '')], editor = dialog.editor, UE = parent.UE, domUtils = UE.dom.domUtils, utils = UE.utils, browser = UE.browser, /* eslint-disable */ ajax = UE.ajax, $G = function (id) { return document.getElementById(id) }, $focus = function (node) { setTimeout(function () { if (browser.ie) { var r = node.createTextRange(); r.collapse(false); r.select(); } else { node.focus() } }, 0) }; window.nowEditor = {editor: editor, dialog: dialog}; utils.loadFile(document, { href: editor.options.themePath + editor.options.theme + '/dialogbase.css?cache=' + Math.random(), tag: 'link', type: 'text/css', rel: 'stylesheet' }); var lang = editor.getLang(dialog.className.split('-')[2]); if (lang) { domUtils.on(window, 'load', function () { var langImgPath = editor.options.langPath + editor.options.lang + '/images/'; // 针对静态资源 for (var i in lang['static']) { var dom = $G(i); if (!dom) continue; let tagName = dom.tagName, content = lang['static'][i]; if (content.src) { // clone content = utils.extend({}, content, false); content.src = langImgPath + content.src; } if (content.style) { content = utils.extend({}, content, false); content.style = content.style.replace(/url\s*\(/g, 'url(' + langImgPath) } switch (tagName.toLowerCase()) { case 'var': dom.parentNode.replaceChild(document.createTextNode(content), dom); break; case 'select': var ops = dom.options; for (var j = 0, oj; oj = ops[j];) { oj.innerHTML = content.options[j++]; } for (var p in content) { p != 'options' && dom.setAttribute(p, content[p]); } break; default : domUtils.setAttributes(dom, content); } } }); } } })(); ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/link/link.html ================================================
    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/map/map.html ================================================
    : :
    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/map/show.html ================================================ 百度地图API自定义地图
    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/music/music.css ================================================ .wrapper{margin: 5px 10px;} .searchBar{height:30px;padding:7px 0 3px;text-align:center;} .searchBtn{font-size:13px;height:24px;} .resultBar{width:460px;margin:5px auto;border: 1px solid #CCC;border-radius: 5px;box-shadow: 2px 2px 5px #D3D6DA;overflow: hidden;} .listPanel{overflow: hidden;} .panelon{display:block;} .paneloff{display:none} .page{width:220px;margin:20px auto;overflow: hidden;} .pageon{float:right;width:24px;line-height:24px;height:24px;margin-right: 5px;background: none;border: none;color: #000;font-weight: bold;text-align:center} .pageoff{float:right;width:24px;line-height:24px;height:24px;cursor:pointer;background-color: #fff; border: 1px solid #E7ECF0;color: #2D64B3;margin-right: 5px;text-decoration: none;text-align:center;} .m-box{width:460px;} .m-m{float: left;line-height: 20px;height: 20px;} .m-h{height:24px;line-height:24px;padding-left: 46px;background-color:#FAFAFA;border-bottom: 1px solid #DAD8D8;font-weight: bold;font-size: 12px;color: #333;} .m-l{float:left;width:40px; } .m-t{float:left;width:140px;} .m-s{float:left;width:110px;} .m-z{float:left;width:100px;} .m-try-t{float: left;width: 60px;;} .m-try{float:left;width:20px;height:20px;background:url('http://static.tieba.baidu.com/tb/editor/images/try_music.gif') no-repeat ;} .m-trying{float:left;width:20px;height:20px;background:url('http://static.tieba.baidu.com/tb/editor/images/stop_music.gif') no-repeat ;} .loading{width:95px;height:7px;font-size:7px;margin:60px auto;background:url(http://static.tieba.baidu.com/tb/editor/images/loading.gif) no-repeat} .empty{width:300px;height:40px;padding:2px;margin:50px auto;line-height:40px; color:#006699;text-align:center;} ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/music/music.html ================================================ 插入音乐
    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/music/music.js ================================================ function Music() { this.init(); } (function () { var pages = [], panels = [], selectedItem = null; Music.prototype = { total:70, pageSize:10, dataUrl:"http://tingapi.ting.baidu.com/v1/restserver/ting?method=baidu.ting.search.common", playerUrl:"http://box.baidu.com/widget/flash/bdspacesong.swf", init:function () { var me = this; domUtils.on($G("J_searchName"), "keyup", function (event) { var e = window.event || event; if (e.keyCode == 13) { me.dosearch(); } }); domUtils.on($G("J_searchBtn"), "click", function () { me.dosearch(); }); }, callback:function (data) { var me = this; me.data = data.song_list; setTimeout(function () { $G('J_resultBar').innerHTML = me._renderTemplate(data.song_list); }, 300); }, dosearch:function () { var me = this; selectedItem = null; var key = $G('J_searchName').value; if (utils.trim(key) == "")return false; key = encodeURIComponent(key); me._sent(key); }, doselect:function (i) { var me = this; if (typeof i == 'object') { selectedItem = i; } else if (typeof i == 'number') { selectedItem = me.data[i]; } }, onpageclick:function (id) { var me = this; for (var i = 0; i < pages.length; i++) { $G(pages[i]).className = 'pageoff'; $G(panels[i]).className = 'paneloff'; } $G('page' + id).className = 'pageon'; $G('panel' + id).className = 'panelon'; }, listenTest:function (elem) { var me = this, view = $G('J_preview'), is_play_action = (elem.className == 'm-try'), old_trying = me._getTryingElem(); if (old_trying) { old_trying.className = 'm-try'; view.innerHTML = ''; } if (is_play_action) { elem.className = 'm-trying'; view.innerHTML = me._buildMusicHtml(me._getUrl(true)); } }, _sent:function (param) { var me = this; $G('J_resultBar').innerHTML = '
    '; utils.loadFile(document, { src:me.dataUrl + '&query=' + param + '&page_size=' + me.total + '&callback=music.callback&.r=' + Math.random(), tag:"script", type:"text/javascript", defer:"defer" }); }, _removeHtml:function (str) { var reg = /<\s*\/?\s*[^>]*\s*>/gi; return str.replace(reg, ""); }, _getUrl:function (isTryListen) { var me = this; var param = 'from=tiebasongwidget&url=&name=' + encodeURIComponent(me._removeHtml(selectedItem.title)) + '&artist=' + encodeURIComponent(me._removeHtml(selectedItem.author)) + '&extra=' + encodeURIComponent(me._removeHtml(selectedItem.album_title)) + '&autoPlay='+isTryListen+'' + '&loop=true'; return me.playerUrl + "?" + param; }, _getTryingElem:function () { var s = $G('J_listPanel').getElementsByTagName('span'); for (var i = 0; i < s.length; i++) { if (s[i].className == 'm-trying') return s[i]; } return null; }, _buildMusicHtml:function (playerUrl) { var html = ' 12) return s.substring(0, 5) + '...'; if (!s) s = " "; return s; }, _rebuildData:function (data) { var me = this, newData = [], d = me.pageSize, itembox; for (var i = 0; i < data.length; i++) { if ((i + d) % d == 0) { itembox = []; newData.push(itembox) } itembox.push(data[i]); } return newData; }, _renderTemplate:function (data) { var me = this; if (data.length == 0)return '
    ' + lang.emptyTxt + '
    '; data = me._rebuildData(data); var s = [], p = [], t = []; s.push('
    '); p.push('
    '); for (var i = 0, tmpList; tmpList = data[i++];) { panels.push('panel' + i); pages.push('page' + i); if (i == 1) { s.push('
    '); if (data.length != 1) { t.push('
    ' + (i ) + '
    '); } } else { s.push('
    '); t.push('
    ' + (i ) + '
    '); } s.push('
    '); s.push('
    ' + lang.chapter + '' + lang.singer + '' + lang.special + '' + lang.listenTest + '
    '); for (var j = 0, tmpObj; tmpObj = tmpList[j++];) { s.push(''); } s.push('
    '); s.push('
    '); } t.reverse(); p.push(t.join('')); s.push('
    '); p.push('
    '); return s.join('') + p.join(''); }, exec:function () { var me = this; if (selectedItem == null) return; $G('J_preview').innerHTML = ""; editor.execCommand('music', { url:me._getUrl(false), width:400, height:95 }); } }; })(); ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/preview/preview.html ================================================
    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/scrawl/scrawl.css ================================================ /*common */ body{margin: 0;} table{width:100%;} table td{padding:2px 4px;vertical-align: middle;} a{text-decoration: none;} em{font-style: normal;} .border_style1{border: 1px solid #ccc;border-radius: 5px;box-shadow:2px 2px 5px #d3d6da;} /*module */ .main{margin: 8px;overflow: hidden;} .hot{float:left;height:335px;} .drawBoard{position: relative; cursor: crosshair;} .brushBorad{position: absolute;left:0;top:0;z-index: 998;} .picBoard{border: none;text-align: center;line-height: 300px;cursor: default;} .operateBar{margin-top:10px;font-size:12px;text-align: center;} .operateBar span{margin-left: 10px;} .drawToolbar{float:right;width:110px;height:300px;overflow: hidden;} .colorBar{margin-top:10px;font-size: 12px;text-align: center;} .colorBar a{display:block;width: 10px;height: 10px;border:1px solid #1006F1;border-radius: 3px; box-shadow:2px 2px 5px #d3d6da;opacity: 0.3} .sectionBar{margin-top:15px;font-size: 12px;text-align: center;} .sectionBar a{display:inline-block;width:10px;height:12px;color: #888;text-indent: -999px;opacity: 0.3} .size1{background: url('images/size.png') 1px center no-repeat ;} .size2{background: url('images/size.png') -10px center no-repeat;} .size3{background: url('images/size.png') -22px center no-repeat;} .size4{background: url('images/size.png') -35px center no-repeat;} .addImgH{position: relative;} .addImgH_form{position: absolute;left: 18px;top: -1px;width: 75px;height: 21px;opacity: 0;cursor: pointer;} .addImgH_form input{width: 100%;} /*scrawl遮罩层 */ .maskLayerNull{display: none;} .maskLayer{position: absolute;top:0;left:0;width: 100%; height: 100%;opacity: 0.7; background-color: #fff;text-align:center;font-weight:bold;line-height:300px;z-index: 1000;} /*btn state */ .previousStepH .icon{display: inline-block;width:16px;height:16px;background-image: url('images/undoH.png');cursor: pointer;} .previousStepH .text{color:#888;cursor:pointer;} .previousStep .icon{display: inline-block;width:16px;height:16px;background-image: url('images/undo.png');cursor:default;} .previousStep .text{color:#ccc;cursor:default;} .nextStepH .icon{display: inline-block;width:16px;height:16px;background-image: url('images/redoH.png');cursor: pointer;} .nextStepH .text{color:#888;cursor:pointer;} .nextStep .icon{display: inline-block;width:16px;height:16px;background-image: url('images/redo.png');cursor:default;} .nextStep .text{color:#ccc;cursor:default;} .clearBoardH .icon{display: inline-block;width:16px;height:16px;background-image: url('images/emptyH.png');cursor: pointer;} .clearBoardH .text{color:#888;cursor:pointer;} .clearBoard .icon{display: inline-block;width:16px;height:16px;background-image: url('images/empty.png');cursor:default;} .clearBoard .text{color:#ccc;cursor:default;} .scaleBoardH .icon{display: inline-block;width:16px;height:16px;background-image: url('images/scaleH.png');cursor: pointer;} .scaleBoardH .text{color:#888;cursor:pointer;} .scaleBoard .icon{display: inline-block;width:16px;height:16px;background-image: url('images/scale.png');cursor:default;} .scaleBoard .text{color:#ccc;cursor:default;} .removeImgH .icon{display: inline-block;width:16px;height:16px;background-image: url('images/delimgH.png');cursor: pointer;} .removeImgH .text{color:#888;cursor:pointer;} .removeImg .icon{display: inline-block;width:16px;height:16px;background-image: url('images/delimg.png');cursor:default;} .removeImg .text{color:#ccc;cursor:default;} .addImgH .icon{vertical-align:top;display: inline-block;width:16px;height:16px;background-image: url('images/addimg.png')} .addImgH .text{color:#888;cursor:pointer;} /*icon */ .brushIcon{display: inline-block;width:16px;height:16px;background-image: url('images/brush.png')} .eraserIcon{display: inline-block;width:16px;height:16px;background-image: url('images/eraser.png')} ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/scrawl/scrawl.html ================================================
    1 3 5 7
    1 3 5 7
    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/scrawl/scrawl.js ================================================ /** * Created with JetBrains PhpStorm. * User: xuheng * Date: 12-5-22 * Time: 上午11:38 * To change this template use File | Settings | File Templates. */ var scrawl = function (options) { options && this.initOptions(options); }; (function () { var canvas = $G("J_brushBoard"), context = canvas.getContext('2d'), drawStep = [], //undo redo存储 drawStepIndex = 0; //undo redo指针 scrawl.prototype = { isScrawl:false, //是否涂鸦 brushWidth:-1, //画笔粗细 brushColor:"", //画笔颜色 initOptions:function (options) { var me = this; me.originalState(options);//初始页面状态 me._buildToolbarColor(options.colorList);//动态生成颜色选择集合 me._addBoardListener(options.saveNum);//添加画板处理 me._addOPerateListener(options.saveNum);//添加undo redo clearBoard处理 me._addColorBarListener();//添加颜色选择处理 me._addBrushBarListener();//添加画笔大小处理 me._addEraserBarListener();//添加橡皮大小处理 me._addAddImgListener();//添加增添背景图片处理 me._addRemoveImgListenter();//删除背景图片处理 me._addScalePicListenter();//添加缩放处理 me._addClearSelectionListenter();//添加清楚选中状态处理 me._originalColorSelect(options.drawBrushColor);//初始化颜色选中 me._originalBrushSelect(options.drawBrushSize);//初始化画笔选中 me._clearSelection();//清楚选中状态 }, originalState:function (options) { var me = this; me.brushWidth = options.drawBrushSize;//同步画笔粗细 me.brushColor = options.drawBrushColor;//同步画笔颜色 context.lineWidth = me.brushWidth;//初始画笔大小 context.strokeStyle = me.brushColor;//初始画笔颜色 context.fillStyle = "transparent";//初始画布背景颜色 context.lineCap = "round";//去除锯齿 context.fill(); }, _buildToolbarColor:function (colorList) { var tmp = null, arr = []; arr.push(""); for (var i = 0, color; color = colorList[i++];) { if ((i - 1) % 5 == 0) { if (i != 1) { arr.push(""); } arr.push(""); } tmp = '#' + color; arr.push(""); } arr.push("
    "); $G("J_colorBar").innerHTML = arr.join(""); }, _addBoardListener:function (saveNum) { var me = this, margin = 0, startX = -1, startY = -1, isMouseDown = false, isMouseMove = false, isMouseUp = false, buttonPress = 0, button, flag = ''; margin = parseInt(domUtils.getComputedStyle($G("J_wrap"), "margin-left")); drawStep.push(context.getImageData(0, 0, context.canvas.width, context.canvas.height)); drawStepIndex += 1; domUtils.on(canvas, ["mousedown", "mousemove", "mouseup", "mouseout"], function (e) { button = browser.webkit ? e.which : buttonPress; switch (e.type) { case 'mousedown': buttonPress = 1; flag = 1; isMouseDown = true; isMouseUp = false; isMouseMove = false; me.isScrawl = true; startX = e.clientX - margin;//10为外边距总和 startY = e.clientY - margin; context.beginPath(); break; case 'mousemove' : if (!flag && button == 0) { return; } if (!flag && button) { startX = e.clientX - margin;//10为外边距总和 startY = e.clientY - margin; context.beginPath(); flag = 1; } if (isMouseUp || !isMouseDown) { return; } var endX = e.clientX - margin, endY = e.clientY - margin; context.moveTo(startX, startY); context.lineTo(endX, endY); context.stroke(); startX = endX; startY = endY; isMouseMove = true; break; case 'mouseup': buttonPress = 0; if (!isMouseDown)return; if (!isMouseMove) { context.arc(startX, startY, context.lineWidth, 0, Math.PI * 2, false); context.fillStyle = context.strokeStyle; context.fill(); } context.closePath(); me._saveOPerate(saveNum); isMouseDown = false; isMouseMove = false; isMouseUp = true; startX = -1; startY = -1; break; case 'mouseout': flag = ''; buttonPress = 0; if (button == 1) return; context.closePath(); break; } }); }, _addOPerateListener:function (saveNum) { var me = this; domUtils.on($G("J_previousStep"), "click", function () { if (drawStepIndex > 1) { drawStepIndex -= 1; context.clearRect(0, 0, context.canvas.width, context.canvas.height); context.putImageData(drawStep[drawStepIndex - 1], 0, 0); me.btn2Highlight("J_nextStep"); drawStepIndex == 1 && me.btn2disable("J_previousStep"); } }); domUtils.on($G("J_nextStep"), "click", function () { if (drawStepIndex > 0 && drawStepIndex < drawStep.length) { context.clearRect(0, 0, context.canvas.width, context.canvas.height); context.putImageData(drawStep[drawStepIndex], 0, 0); drawStepIndex += 1; me.btn2Highlight("J_previousStep"); drawStepIndex == drawStep.length && me.btn2disable("J_nextStep"); } }); domUtils.on($G("J_clearBoard"), "click", function () { context.clearRect(0, 0, context.canvas.width, context.canvas.height); drawStep = []; me._saveOPerate(saveNum); drawStepIndex = 1; me.isScrawl = false; me.btn2disable("J_previousStep"); me.btn2disable("J_nextStep"); me.btn2disable("J_clearBoard"); }); }, _addColorBarListener:function () { var me = this; domUtils.on($G("J_colorBar"), "click", function (e) { var target = me.getTarget(e), color = target.title; if (!!color) { me._addColorSelect(target); me.brushColor = color; context.globalCompositeOperation = "source-over"; context.lineWidth = me.brushWidth; context.strokeStyle = color; } }); }, _addBrushBarListener:function () { var me = this; domUtils.on($G("J_brushBar"), "click", function (e) { var target = me.getTarget(e), size = browser.ie ? target.innerText : target.text; if (!!size) { me._addBESelect(target); context.globalCompositeOperation = "source-over"; context.lineWidth = parseInt(size); context.strokeStyle = me.brushColor; me.brushWidth = context.lineWidth; } }); }, _addEraserBarListener:function () { var me = this; domUtils.on($G("J_eraserBar"), "click", function (e) { var target = me.getTarget(e), size = browser.ie ? target.innerText : target.text; if (!!size) { me._addBESelect(target); context.lineWidth = parseInt(size); context.globalCompositeOperation = "destination-out"; context.strokeStyle = "#FFF"; } }); }, _addAddImgListener:function () { var file = $G("J_imgTxt"); if (!window.FileReader) { $G("J_addImg").style.display = 'none'; $G("J_removeImg").style.display = 'none'; $G("J_sacleBoard").style.display = 'none'; } domUtils.on(file, "change", function (e) { var frm = file.parentNode; addMaskLayer(lang.backgroundUploading); var target = e.target || e.srcElement, reader = new FileReader(); reader.onload = function(evt){ var target = evt.target || evt.srcElement; ue_callback(target.result, 'SUCCESS'); }; reader.readAsDataURL(target.files[0]); frm.reset(); }); }, _addRemoveImgListenter:function () { var me = this; domUtils.on($G("J_removeImg"), "click", function () { $G("J_picBoard").innerHTML = ""; me.btn2disable("J_removeImg"); me.btn2disable("J_sacleBoard"); }); }, _addScalePicListenter:function () { domUtils.on($G("J_sacleBoard"), "click", function () { var picBoard = $G("J_picBoard"), scaleCon = $G("J_scaleCon"), img = picBoard.children[0]; if (img) { if (!scaleCon) { picBoard.style.cssText = "position:relative;z-index:1;"+picBoard.style.cssText; img.style.cssText = "position: absolute;top:" + (canvas.height - img.height) / 2 + "px;left:" + (canvas.width - img.width) / 2 + "px;"; var scale = new ScaleBoy(); picBoard.appendChild(scale.init()); scale.startScale(img); } else { if (scaleCon.style.visibility == "visible") { scaleCon.style.visibility = "hidden"; picBoard.style.position = ""; picBoard.style.zIndex = ""; } else { scaleCon.style.visibility = "visible"; picBoard.style.cssText += "position:relative;z-index:1"; } } } }); }, _addClearSelectionListenter:function () { var doc = document; domUtils.on(doc, 'mousemove', function (e) { if (browser.ie && browser.version < 11) doc.selection.clear(); else window.getSelection().removeAllRanges(); }); }, _clearSelection:function () { var list = ["J_operateBar", "J_colorBar", "J_brushBar", "J_eraserBar", "J_picBoard"]; for (var i = 0, group; group = list[i++];) { domUtils.unSelectable($G(group)); } }, _saveOPerate:function (saveNum) { var me = this; if (drawStep.length <= saveNum) { if(drawStepIndex"); } scale.innerHTML = arr.join(""); return scale; } var rect = [ //[left, top, width, height] [1, 1, -1, -1], [0, 1, 0, -1], [0, 1, 1, -1], [1, 0, -1, 0], [0, 0, 1, 0], [1, 0, -1, 1], [0, 0, 0, 1], [0, 0, 1, 1] ]; ScaleBoy.prototype = { init:function () { _appendStyle(); var me = this, scale = me.dom = _getDom(); me.scaleMousemove.fp = me; domUtils.on(scale, 'mousedown', function (e) { var target = e.target || e.srcElement; me.start = {x:e.clientX, y:e.clientY}; if (target.className.indexOf('hand') != -1) { me.dir = target.className.replace('hand', ''); } domUtils.on(document.body, 'mousemove', me.scaleMousemove); e.stopPropagation ? e.stopPropagation() : e.cancelBubble = true; }); domUtils.on(document.body, 'mouseup', function (e) { if (me.start) { domUtils.un(document.body, 'mousemove', me.scaleMousemove); if (me.moved) { me.updateScaledElement({position:{x:scale.style.left, y:scale.style.top}, size:{w:scale.style.width, h:scale.style.height}}); } delete me.start; delete me.moved; delete me.dir; } }); return scale; }, startScale:function (objElement) { var me = this, Idom = me.dom; Idom.style.cssText = 'visibility:visible;top:' + objElement.style.top + ';left:' + objElement.style.left + ';width:' + objElement.offsetWidth + 'px;height:' + objElement.offsetHeight + 'px;'; me.scalingElement = objElement; }, updateScaledElement:function (objStyle) { var cur = this.scalingElement, pos = objStyle.position, size = objStyle.size; if (pos) { typeof pos.x != 'undefined' && (cur.style.left = pos.x); typeof pos.y != 'undefined' && (cur.style.top = pos.y); } if (size) { size.w && (cur.style.width = size.w); size.h && (cur.style.height = size.h); } }, updateStyleByDir:function (dir, offset) { var me = this, dom = me.dom, tmp; rect['def'] = [1, 1, 0, 0]; if (rect[dir][0] != 0) { tmp = parseInt(dom.style.left) + offset.x; dom.style.left = me._validScaledProp('left', tmp) + 'px'; } if (rect[dir][1] != 0) { tmp = parseInt(dom.style.top) + offset.y; dom.style.top = me._validScaledProp('top', tmp) + 'px'; } if (rect[dir][2] != 0) { tmp = dom.clientWidth + rect[dir][2] * offset.x; dom.style.width = me._validScaledProp('width', tmp) + 'px'; } if (rect[dir][3] != 0) { tmp = dom.clientHeight + rect[dir][3] * offset.y; dom.style.height = me._validScaledProp('height', tmp) + 'px'; } if (dir === 'def') { me.updateScaledElement({position:{x:dom.style.left, y:dom.style.top}}); } }, scaleMousemove:function (e) { var me = arguments.callee.fp, start = me.start, dir = me.dir || 'def', offset = {x:e.clientX - start.x, y:e.clientY - start.y}; me.updateStyleByDir(dir, offset); arguments.callee.fp.start = {x:e.clientX, y:e.clientY}; arguments.callee.fp.moved = 1; }, _validScaledProp:function (prop, value) { var ele = this.dom, wrap = $G("J_picBoard"); value = isNaN(value) ? 0 : value; switch (prop) { case 'left': return value < 0 ? 0 : (value + ele.clientWidth) > wrap.clientWidth ? wrap.clientWidth - ele.clientWidth : value; case 'top': return value < 0 ? 0 : (value + ele.clientHeight) > wrap.clientHeight ? wrap.clientHeight - ele.clientHeight : value; case 'width': return value <= 0 ? 1 : (value + ele.offsetLeft) > wrap.clientWidth ? wrap.clientWidth - ele.offsetLeft : value; case 'height': return value <= 0 ? 1 : (value + ele.offsetTop) > wrap.clientHeight ? wrap.clientHeight - ele.offsetTop : value; } } }; })(); //后台回调 function ue_callback(url, state) { var doc = document, picBorard = $G("J_picBoard"), img = doc.createElement("img"); //图片缩放 function scale(img, max, oWidth, oHeight) { var width = 0, height = 0, percent, ow = img.width || oWidth, oh = img.height || oHeight; if (ow > max || oh > max) { if (ow >= oh) { if (width = ow - max) { percent = (width / ow).toFixed(2); img.height = oh - oh * percent; img.width = max; } } else { if (height = oh - max) { percent = (height / oh).toFixed(2); img.width = ow - ow * percent; img.height = max; } } } } //移除遮罩层 removeMaskLayer(); //状态响应 if (state == "SUCCESS") { picBorard.innerHTML = ""; img.onload = function () { scale(this, 300); picBorard.appendChild(img); var obj = new scrawl(); obj.btn2Highlight("J_removeImg"); //trace 2457 obj.btn2Highlight("J_sacleBoard"); }; img.src = url; } else { alert(state); } } //去掉遮罩层 function removeMaskLayer() { var maskLayer = $G("J_maskLayer"); maskLayer.className = "maskLayerNull"; maskLayer.innerHTML = ""; dialog.buttons[0].setDisabled(false); } //添加遮罩层 function addMaskLayer(html) { var maskLayer = $G("J_maskLayer"); dialog.buttons[0].setDisabled(true); maskLayer.className = "maskLayer"; maskLayer.innerHTML = html; } //执行确认按钮方法 function exec(scrawlObj) { if (scrawlObj.isScrawl) { addMaskLayer(lang.scrawlUpLoading); var base64 = scrawlObj.getCanvasData(); if (!!base64) { var options = { timeout:100000, onsuccess:function (xhr) { if (!scrawlObj.isCancelScrawl) { var responseObj; responseObj = eval("(" + xhr.responseText + ")"); if (responseObj.state == "SUCCESS") { var imgObj = {}, url = editor.options.scrawlUrlPrefix + responseObj.url; imgObj.src = url; imgObj._src = url; imgObj.alt = responseObj.original || ''; imgObj.title = responseObj.title || ''; editor.execCommand("insertImage", imgObj); dialog.close(); } else { alert(responseObj.state); } } }, onerror:function () { alert(lang.imageError); dialog.close(); } }; options[editor.getOpt('scrawlFieldName')] = base64; var actionUrl = editor.getActionUrl(editor.getOpt('scrawlActionName')), params = utils.serializeParam(editor.queryCommandValue('serverparam')) || '', url = utils.formatUrl(actionUrl + (actionUrl.indexOf('?') == -1 ? '?':'&') + params); ajax.request(url, options); } } else { addMaskLayer(lang.noScarwl + "   "); } } ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/searchreplace/searchreplace.html ================================================
    :
     
    :
    :
     
    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/searchreplace/searchreplace.js ================================================ /** * Created with JetBrains PhpStorm. * User: xuheng * Date: 12-9-26 * Time: 下午12:29 * To change this template use File | Settings | File Templates. */ //清空上次查选的痕迹 editor.firstForSR = 0; editor.currentRangeForSR = null; //给tab注册切换事件 /** * tab点击处理事件 * @param tabHeads * @param tabBodys * @param obj */ function clickHandler( tabHeads,tabBodys,obj ) { //head样式更改 for ( var k = 0, len = tabHeads.length; k < len; k++ ) { tabHeads[k].className = ""; } obj.className = "focus"; //body显隐 var tabSrc = obj.getAttribute( "tabSrc" ); for ( var j = 0, length = tabBodys.length; j < length; j++ ) { var body = tabBodys[j], id = body.getAttribute( "id" ); if ( id != tabSrc ) { body.style.zIndex = 1; } else { body.style.zIndex = 200; } } } /** * TAB切换 * @param tabParentId tab的父节点ID或者对象本身 */ function switchTab( tabParentId ) { var tabElements = $G( tabParentId ).children, tabHeads = tabElements[0].children, tabBodys = tabElements[1].children; for ( var i = 0, length = tabHeads.length; i < length; i++ ) { var head = tabHeads[i]; if ( head.className === "focus" )clickHandler(tabHeads,tabBodys, head ); head.onclick = function () { clickHandler(tabHeads,tabBodys,this); } } } $G('searchtab').onmousedown = function(){ $G('search-msg').innerHTML = ''; $G('replace-msg').innerHTML = '' } //是否区分大小写 function getMatchCase(id) { return $G(id).checked ? true : false; } //查找 $G("nextFindBtn").onclick = function (txt, dir, mcase) { var findtxt = $G("findtxt").value, obj; if (!findtxt) { return false; } obj = { searchStr:findtxt, dir:1, casesensitive:getMatchCase("matchCase") }; if (!frCommond(obj)) { var bk = editor.selection.getRange().createBookmark(); $G('search-msg').innerHTML = lang.getEnd; editor.selection.getRange().moveToBookmark(bk).select(); } }; $G("nextReplaceBtn").onclick = function (txt, dir, mcase) { var findtxt = $G("findtxt1").value, obj; if (!findtxt) { return false; } obj = { searchStr:findtxt, dir:1, casesensitive:getMatchCase("matchCase1") }; frCommond(obj); }; $G("preFindBtn").onclick = function (txt, dir, mcase) { var findtxt = $G("findtxt").value, obj; if (!findtxt) { return false; } obj = { searchStr:findtxt, dir:-1, casesensitive:getMatchCase("matchCase") }; if (!frCommond(obj)) { $G('search-msg').innerHTML = lang.getStart; } }; $G("preReplaceBtn").onclick = function (txt, dir, mcase) { var findtxt = $G("findtxt1").value, obj; if (!findtxt) { return false; } obj = { searchStr:findtxt, dir:-1, casesensitive:getMatchCase("matchCase1") }; frCommond(obj); }; //替换 $G("repalceBtn").onclick = function () { var findtxt = $G("findtxt1").value.replace(/^\s|\s$/g, ""), obj, replacetxt = $G("replacetxt").value.replace(/^\s|\s$/g, ""); if (!findtxt) { return false; } if (findtxt == replacetxt || (!getMatchCase("matchCase1") && findtxt.toLowerCase() == replacetxt.toLowerCase())) { return false; } obj = { searchStr:findtxt, dir:1, casesensitive:getMatchCase("matchCase1"), replaceStr:replacetxt }; frCommond(obj); }; //全部替换 $G("repalceAllBtn").onclick = function () { var findtxt = $G("findtxt1").value.replace(/^\s|\s$/g, ""), obj, replacetxt = $G("replacetxt").value.replace(/^\s|\s$/g, ""); if (!findtxt) { return false; } if (findtxt == replacetxt || (!getMatchCase("matchCase1") && findtxt.toLowerCase() == replacetxt.toLowerCase())) { return false; } obj = { searchStr:findtxt, casesensitive:getMatchCase("matchCase1"), replaceStr:replacetxt, all:true }; var num = frCommond(obj); if (num) { $G('replace-msg').innerHTML = lang.countMsg.replace("{#count}", num); } }; //执行 var frCommond = function (obj) { return editor.execCommand("searchreplace", obj); }; switchTab("searchtab"); ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/snapscreen/snapscreen.html ================================================

    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/spechars/spechars.html ================================================
    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/spechars/spechars.js ================================================ /** * Created with JetBrains PhpStorm. * User: xuheng * Date: 12-9-26 * Time: 下午1:09 * To change this template use File | Settings | File Templates. */ var charsContent = [ { name:"tsfh", title:lang.tsfh, content:toArray("、,。,·,ˉ,ˇ,¨,〃,々,—,~,‖,…,‘,’,“,”,〔,〕,〈,〉,《,》,「,」,『,』,〖,〗,【,】,±,×,÷,∶,∧,∨,∑,∏,∪,∩,∈,∷,√,⊥,∥,∠,⌒,⊙,∫,∮,≡,≌,≈,∽,∝,≠,≮,≯,≤,≥,∞,∵,∴,♂,♀,°,′,″,℃,$,¤,¢,£,‰,§,№,☆,★,○,●,◎,◇,◆,□,■,△,▲,※,→,←,↑,↓,〓,〡,〢,〣,〤,〥,〦,〧,〨,〩,㊣,㎎,㎏,㎜,㎝,㎞,㎡,㏄,㏎,㏑,㏒,㏕,︰,¬,¦,℡,ˊ,ˋ,˙,–,―,‥,‵,℅,℉,↖,↗,↘,↙,∕,∟,∣,≒,≦,≧,⊿,═,║,╒,╓,╔,╕,╖,╗,╘,╙,╚,╛,╜,╝,╞,╟,╠,╡,╢,╣,╤,╥,╦,╧,╨,╩,╪,╫,╬,╭,╮,╯,╰,╱,╲,╳,▁,▂,▃,▄,▅,▆,▇,�,█,▉,▊,▋,▌,▍,▎,▏,▓,▔,▕,▼,▽,◢,◣,◤,◥,☉,⊕,〒,〝,〞")}, { name:"lmsz", title:lang.lmsz, content:toArray("ⅰ,ⅱ,ⅲ,ⅳ,ⅴ,ⅵ,ⅶ,ⅷ,ⅸ,ⅹ,Ⅰ,Ⅱ,Ⅲ,Ⅳ,Ⅴ,Ⅵ,Ⅶ,Ⅷ,Ⅸ,Ⅹ,Ⅺ,Ⅻ")}, { name:"szfh", title:lang.szfh, content:toArray("⒈,⒉,⒊,⒋,⒌,⒍,⒎,⒏,⒐,⒑,⒒,⒓,⒔,⒕,⒖,⒗,⒘,⒙,⒚,⒛,⑴,⑵,⑶,⑷,⑸,⑹,⑺,⑻,⑼,⑽,⑾,⑿,⒀,⒁,⒂,⒃,⒄,⒅,⒆,⒇,①,②,③,④,⑤,⑥,⑦,⑧,⑨,⑩,㈠,㈡,㈢,㈣,㈤,㈥,㈦,㈧,㈨,㈩")}, { name:"rwfh", title:lang.rwfh, content:toArray("ぁ,あ,ぃ,い,ぅ,う,ぇ,え,ぉ,お,か,が,き,ぎ,く,ぐ,け,げ,こ,ご,さ,ざ,し,じ,す,ず,せ,ぜ,そ,ぞ,た,だ,ち,ぢ,っ,つ,づ,て,で,と,ど,な,に,ぬ,ね,の,は,ば,ぱ,ひ,び,ぴ,ふ,ぶ,ぷ,へ,べ,ぺ,ほ,ぼ,ぽ,ま,み,む,め,も,ゃ,や,ゅ,ゆ,ょ,よ,ら,り,る,れ,ろ,ゎ,わ,ゐ,ゑ,を,ん,ァ,ア,ィ,イ,ゥ,ウ,ェ,エ,ォ,オ,カ,ガ,キ,ギ,ク,グ,ケ,ゲ,コ,ゴ,サ,ザ,シ,ジ,ス,ズ,セ,ゼ,ソ,ゾ,タ,ダ,チ,ヂ,ッ,ツ,ヅ,テ,デ,ト,ド,ナ,ニ,ヌ,ネ,ノ,ハ,バ,パ,ヒ,ビ,ピ,フ,ブ,プ,ヘ,ベ,ペ,ホ,ボ,ポ,マ,ミ,ム,メ,モ,ャ,ヤ,ュ,ユ,ョ,ヨ,ラ,リ,ル,レ,ロ,ヮ,ワ,ヰ,ヱ,ヲ,ン,ヴ,ヵ,ヶ")}, { name:"xlzm", title:lang.xlzm, content:toArray("Α,Β,Γ,Δ,Ε,Ζ,Η,Θ,Ι,Κ,Λ,Μ,Ν,Ξ,Ο,Π,Ρ,Σ,Τ,Υ,Φ,Χ,Ψ,Ω,α,β,γ,δ,ε,ζ,η,θ,ι,κ,λ,μ,ν,ξ,ο,π,ρ,σ,τ,υ,φ,χ,ψ,ω")}, { name:"ewzm", title:lang.ewzm, content:toArray("А,Б,В,Г,Д,Е,Ё,Ж,З,И,Й,К,Л,М,Н,О,П,Р,С,Т,У,Ф,Х,Ц,Ч,Ш,Щ,Ъ,Ы,Ь,Э,Ю,Я,а,б,в,г,д,е,ё,ж,з,и,й,к,л,м,н,о,п,р,с,т,у,ф,х,ц,ч,ш,щ,ъ,ы,ь,э,ю,я")}, { name:"pyzm", title:lang.pyzm, content:toArray("ā,á,ǎ,à,ē,é,ě,è,ī,í,ǐ,ì,ō,ó,ǒ,ò,ū,ú,ǔ,ù,ǖ,ǘ,ǚ,ǜ,ü")}, { name:"yyyb", title:lang.yyyb, content:toArray("i:,i,e,æ,ʌ,ə:,ə,u:,u,ɔ:,ɔ,a:,ei,ai,ɔi,əu,au,iə,εə,uə,p,t,k,b,d,g,f,s,ʃ,θ,h,v,z,ʒ,ð,tʃ,tr,ts,dʒ,dr,dz,m,n,ŋ,l,r,w,j,")}, { name:"zyzf", title:lang.zyzf, content:toArray("ㄅ,ㄆ,ㄇ,ㄈ,ㄉ,ㄊ,ㄋ,ㄌ,ㄍ,ㄎ,ㄏ,ㄐ,ㄑ,ㄒ,ㄓ,ㄔ,ㄕ,ㄖ,ㄗ,ㄘ,ㄙ,ㄚ,ㄛ,ㄜ,ㄝ,ㄞ,ㄟ,ㄠ,ㄡ,ㄢ,ㄣ,ㄤ,ㄥ,ㄦ,ㄧ,ㄨ")} ]; (function createTab(content) { for (var i = 0, ci; ci = content[i++];) { var span = document.createElement("span"); span.setAttribute("tabSrc", ci.name); span.innerHTML = ci.title; if (i == 1)span.className = "focus"; domUtils.on(span, "click", function () { var tmps = $G("tabHeads").children; for (var k = 0, sk; sk = tmps[k++];) { sk.className = ""; } tmps = $G("tabBodys").children; for (var k = 0, sk; sk = tmps[k++];) { sk.style.display = "none"; } this.className = "focus"; $G(this.getAttribute("tabSrc")).style.display = ""; }); $G("tabHeads").appendChild(span); domUtils.insertAfter(span, document.createTextNode("\n")); var div = document.createElement("div"); div.id = ci.name; div.style.display = (i == 1) ? "" : "none"; var cons = ci.content; for (var j = 0, con; con = cons[j++];) { var charSpan = document.createElement("span"); charSpan.innerHTML = con; domUtils.on(charSpan, "click", function () { editor.execCommand("insertHTML", this.innerHTML); dialog.close(); }); div.appendChild(charSpan); } $G("tabBodys").appendChild(div); } })(charsContent); function toArray(str) { return str.split(","); } ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/table/edittable.css ================================================ body{ overflow: hidden; width: 540px; } .wrapper { margin: 10px auto 0; font-size: 12px; overflow: hidden; width: 520px; height: 315px; } .clear { clear: both; } .wrapper .left { float: left; margin-left: 10px;; } .wrapper .right { float: right; border-left: 2px dotted #EDEDED; padding-left: 15px; } .section { margin-bottom: 15px; width: 240px; overflow: hidden; } .section h3 { font-weight: bold; padding: 5px 0; margin-bottom: 10px; border-bottom: 1px solid #EDEDED; font-size: 12px; } .section ul { list-style: none; overflow: hidden; clear: both; } .section li { float: left; width: 120px;; } .section .tone { width: 80px;; } .section .preview { width: 220px; } .section .preview table { text-align: center; vertical-align: middle; color: #666; } .section .preview caption { font-weight: bold; } .section .preview td { border-width: 1px; border-style: solid; height: 22px; } .section .preview th { border-style: solid; border-color: #DDD; border-width: 2px 1px 1px 1px; height: 22px; background-color: #F7F7F7; } ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/table/edittable.html ================================================

    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/table/edittable.js ================================================ /** * Created with JetBrains PhpStorm. * User: xuheng * Date: 12-12-19 * Time: 下午4:55 * To change this template use File | Settings | File Templates. */ (function () { var title = $G("J_title"), titleCol = $G("J_titleCol"), caption = $G("J_caption"), sorttable = $G("J_sorttable"), autoSizeContent = $G("J_autoSizeContent"), autoSizePage = $G("J_autoSizePage"), tone = $G("J_tone"), me, preview = $G("J_preview"); var editTable = function () { me = this; me.init(); }; editTable.prototype = { init:function () { var colorPiker = new UE.ui.ColorPicker({ editor:editor }), colorPop = new UE.ui.Popup({ editor:editor, content:colorPiker }); title.checked = editor.queryCommandState("inserttitle") == -1; titleCol.checked = editor.queryCommandState("inserttitlecol") == -1; caption.checked = editor.queryCommandState("insertcaption") == -1; sorttable.checked = editor.queryCommandState("enablesort") == 1; var enablesortState = editor.queryCommandState("enablesort"), disablesortState = editor.queryCommandState("disablesort"); sorttable.checked = !!(enablesortState < 0 && disablesortState >=0); sorttable.disabled = !!(enablesortState < 0 && disablesortState < 0); sorttable.title = enablesortState < 0 && disablesortState < 0 ? lang.errorMsg:''; me.createTable(title.checked, titleCol.checked, caption.checked); me.setAutoSize(); me.setColor(me.getColor()); domUtils.on(title, "click", me.titleHanler); domUtils.on(titleCol, "click", me.titleColHanler); domUtils.on(caption, "click", me.captionHanler); domUtils.on(sorttable, "click", me.sorttableHanler); domUtils.on(autoSizeContent, "click", me.autoSizeContentHanler); domUtils.on(autoSizePage, "click", me.autoSizePageHanler); domUtils.on(tone, "click", function () { colorPop.showAnchor(tone); }); domUtils.on(document, 'mousedown', function () { colorPop.hide(); }); colorPiker.addListener("pickcolor", function () { me.setColor(arguments[1]); colorPop.hide(); }); colorPiker.addListener("picknocolor", function () { me.setColor(""); colorPop.hide(); }); }, createTable:function (hasTitle, hasTitleCol, hasCaption) { var arr = [], sortSpan = '^'; arr.push(""); if (hasCaption) { arr.push("") } if (hasTitle) { arr.push(""); if(hasTitleCol) { arr.push(""); } for (var j = 0; j < 5; j++) { arr.push(""); } arr.push(""); } for (var i = 0; i < 6; i++) { arr.push(""); if(hasTitleCol) { arr.push("") } for (var k = 0; k < 5; k++) { arr.push("") } arr.push(""); } arr.push("
    " + lang.captionName + "
    " + lang.titleName + "" + lang.titleName + "
    " + lang.titleName + "" + lang.cellsName + "
    "); preview.innerHTML = arr.join(""); this.updateSortSpan(); }, titleHanler:function () { var example = $G("J_example"), frg=document.createDocumentFragment(), color = domUtils.getComputedStyle(domUtils.getElementsByTagName(example, "td")[0], "border-color"), colCount = example.rows[0].children.length; if (title.checked) { example.insertRow(0); for (var i = 0, node; i < colCount; i++) { node = document.createElement("th"); node.innerHTML = lang.titleName; frg.appendChild(node); } example.rows[0].appendChild(frg); } else { domUtils.remove(example.rows[0]); } me.setColor(color); me.updateSortSpan(); }, titleColHanler:function () { var example = $G("J_example"), color = domUtils.getComputedStyle(domUtils.getElementsByTagName(example, "td")[0], "border-color"), colArr = example.rows, colCount = colArr.length; if (titleCol.checked) { for (var i = 0, node; i < colCount; i++) { node = document.createElement("th"); node.innerHTML = lang.titleName; colArr[i].insertBefore(node, colArr[i].children[0]); } } else { for (var i = 0; i < colCount; i++) { domUtils.remove(colArr[i].children[0]); } } me.setColor(color); me.updateSortSpan(); }, captionHanler:function () { var example = $G("J_example"); if (caption.checked) { var row = document.createElement('caption'); row.innerHTML = lang.captionName; example.insertBefore(row, example.firstChild); } else { domUtils.remove(domUtils.getElementsByTagName(example, 'caption')[0]); } }, sorttableHanler:function(){ me.updateSortSpan(); }, autoSizeContentHanler:function () { var example = $G("J_example"); example.removeAttribute("width"); }, autoSizePageHanler:function () { var example = $G("J_example"); var tds = example.getElementsByTagName(example, "td"); utils.each(tds, function (td) { td.removeAttribute("width"); }); example.setAttribute('width', '100%'); }, updateSortSpan: function(){ var example = $G("J_example"), row = example.rows[0]; var spans = domUtils.getElementsByTagName(example,"span"); utils.each(spans,function(span){ span.parentNode.removeChild(span); }); if (sorttable.checked) { utils.each(row.cells, function(cell, i){ var span = document.createElement("span"); span.innerHTML = "^"; cell.appendChild(span); }); } }, getColor:function () { var start = editor.selection.getStart(), color, cell = domUtils.findParentByTagName(start, ["td", "th", "caption"], true); color = cell && domUtils.getComputedStyle(cell, "border-color"); if (!color) color = "#DDDDDD"; return color; }, setColor:function (color) { var example = $G("J_example"), arr = domUtils.getElementsByTagName(example, "td").concat( domUtils.getElementsByTagName(example, "th"), domUtils.getElementsByTagName(example, "caption") ); tone.value = color; utils.each(arr, function (node) { node.style.borderColor = color; }); }, setAutoSize:function () { var me = this; autoSizePage.checked = true; me.autoSizePageHanler(); } }; new editTable; dialog.onok = function () { editor.__hasEnterExecCommand = true; var checks = { title:"inserttitle deletetitle", titleCol:"inserttitlecol deletetitlecol", caption:"insertcaption deletecaption", sorttable:"enablesort disablesort" }; editor.fireEvent('saveScene'); for(var i in checks){ var cmds = checks[i].split(" "), input = $G("J_" + i); if(input["checked"]){ editor.queryCommandState(cmds[0])!=-1 &&editor.execCommand(cmds[0]); }else{ editor.queryCommandState(cmds[1])!=-1 &&editor.execCommand(cmds[1]); } } editor.execCommand("edittable", tone.value); autoSizeContent.checked ?editor.execCommand('adaptbytext') : ""; autoSizePage.checked ? editor.execCommand("adaptbywindow") : ""; editor.fireEvent('saveScene'); editor.__hasEnterExecCommand = false; }; })(); ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/table/edittd.html ================================================
    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/table/edittip.html ================================================ 表格删除提示
    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/template/config.js ================================================ /** * Created with JetBrains PhpStorm. * User: xuheng * Date: 12-8-8 * Time: 下午2:00 * To change this template use File | Settings | File Templates. */ var templates = [ { "pre":"pre0.png", 'title':lang.blank, 'preHtml':'

     欢迎使用UEditor!

    ', "html":'

    欢迎使用UEditor!

    ' }, { "pre":"pre1.png", 'title':lang.blog, 'preHtml':'

    深入理解Range

    UEditor二次开发

    什么是Range

    对于“插入”选项卡上的库,在设计时都充分考虑了其中的项与文档整体外观的协调性。


    Range能干什么

    在“开始”选项卡上,通过从快速样式库中为所选文本选择一种外观,您可以方便地更改文档中所选文本的格式。

    ', "html":'

    [键入文档标题]

    [键入文档副标题]

    [标题 1]

    对于“插入”选项卡上的库,在设计时都充分考虑了其中的项与文档整体外观的协调性。 您可以使用这些库来插入表格、页眉、页脚、列表、封面以及其他文档构建基块。 您创建的图片、图表或关系图也将与当前的文档外观协调一致。

    [标题 2]

    在“开始”选项卡上,通过从快速样式库中为所选文本选择一种外观,您可以方便地更改文档中所选文本的格式。 您还可以使用“开始”选项卡上的其他控件来直接设置文本格式。大多数控件都允许您选择是使用当前主题外观,还是使用某种直接指定的格式。

    [标题 3]

    对于“插入”选项卡上的库,在设计时都充分考虑了其中的项与文档整体外观的协调性。 您可以使用这些库来插入表格、页眉、页脚、列表、封面以及其他文档构建基块。 您创建的图片、图表或关系图也将与当前的文档外观协调一致。


    ' }, { "pre":"pre2.png", 'title':lang.resume, 'preHtml':'

    WEB前端开发简历


    联系电话:[键入您的电话]

    电子邮件:[键入您的电子邮件地址]

    家庭住址:[键入您的地址]

    目标职位

    WEB前端研发工程师

    学历

    1. [起止时间] [学校名称] [所学专业] [所获学位]

    工作经验


    ', "html":'

    [此处键入简历标题]


    【此处插入照片】


    联系电话:[键入您的电话]


    电子邮件:[键入您的电子邮件地址]


    家庭住址:[键入您的地址]


    目标职位

    [此处键入您的期望职位]

    学历

    1. [键入起止时间] [键入学校名称] [键入所学专业] [键入所获学位]

    2. [键入起止时间] [键入学校名称] [键入所学专业] [键入所获学位]

    工作经验

    1. [键入起止时间] [键入公司名称] [键入职位名称]

      1. [键入负责项目] [键入项目简介]

      2. [键入负责项目] [键入项目简介]

    2. [键入起止时间] [键入公司名称] [键入职位名称]

      1. [键入负责项目] [键入项目简介]

    掌握技能

     [这里可以键入您所掌握的技能]

    ' }, { "pre":"pre3.png", 'title':lang.richText, 'preHtml':'

    [此处键入文章标题]

    图文混排方法

    图片居左,文字围绕图片排版

    方法:在文字前面插入图片,设置居左对齐,然后即可在右边输入多行文


    还有没有什么其他的环绕方式呢?这里是居右环绕


    欢迎大家多多尝试,为UEditor提供更多高质量模板!

    ', "html":'


    [此处键入文章标题]

    图文混排方法

    1. 图片居左,文字围绕图片排版

    方法:在文字前面插入图片,设置居左对齐,然后即可在右边输入多行文本


    2. 图片居右,文字围绕图片排版

    方法:在文字前面插入图片,设置居右对齐,然后即可在左边输入多行文本


    3. 图片居中环绕排版

    方法:亲,这个真心没有办法。。。



    还有没有什么其他的环绕方式呢?这里是居右环绕


    欢迎大家多多尝试,为UEditor提供更多高质量模板!


    占位


    占位


    占位


    占位


    占位



    ' }, { "pre":"pre4.png", 'title':lang.sciPapers, 'preHtml':'

    [键入文章标题]

    摘要:这里可以输入很长很长很长很长很长很长很长很长很差的摘要

    标题 1

    这里可以输入很多内容,可以图文混排,可以有列表等。

    标题 2

    1. 列表 1

    2. 列表 2

      1. 多级列表 1

      2. 多级列表 2

    3. 列表 3

    标题 3

    来个文字图文混排的


    ', 'html':'

    [键入文章标题]

    摘要:这里可以输入很长很长很长很长很长很长很长很长很差的摘要

    标题 1

    这里可以输入很多内容,可以图文混排,可以有列表等。

    标题 2

    来个列表瞅瞅:

    1. 列表 1

    2. 列表 2

      1. 多级列表 1

      2. 多级列表 2

    3. 列表 3

    标题 3

    来个文字图文混排的

    这里可以多行

    右边是图片

    绝对没有问题的,不信你也可以试试看


    ' } ]; ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/template/template.css ================================================ .wrap{ padding: 5px;font-size: 14px;} .left{width:425px;float: left;} .right{width:160px;border: 1px solid #ccc;float: right;padding: 5px;margin-right: 5px;} .right .pre{height: 332px;overflow-y: auto;} .right .preitem{border: white 1px solid;margin: 5px 0;padding: 2px 0;} .right .preitem:hover{background-color: lemonChiffon;cursor: pointer;border: #ccc 1px solid;} .right .preitem img{display: block;margin: 0 auto;width:100px;} .clear{clear: both;} .top{height:26px;line-height: 26px;padding: 5px;} .bottom{height:320px;width:100%;margin: 0 auto;} .transparent{ background: url("images/bg.gif") repeat;} .bottom table tr td{border:1px dashed #ccc;} #colorPicker{width: 17px;height: 17px;border: 1px solid #CCC;display: inline-block;border-radius: 3px;box-shadow: 2px 2px 5px #D3D6DA;} .border_style1{padding:2px;border: 1px solid #ccc;border-radius: 5px;box-shadow:2px 2px 5px #d3d6da;} p{margin: 5px 0} table{clear:both;margin-bottom:10px;border-collapse:collapse;word-break:break-all;} li{clear:both} ol{padding-left:40px; } ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/template/template.html ================================================
    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/template/template.js ================================================ /** * Created with JetBrains PhpStorm. * User: xuheng * Date: 12-8-8 * Time: 下午2:09 * To change this template use File | Settings | File Templates. */ (function () { var me = editor, preview = $G( "preview" ), preitem = $G( "preitem" ), tmps = templates, currentTmp; var initPre = function () { var str = ""; for ( var i = 0, tmp; tmp = tmps[i++]; ) { str += '
    '; } preitem.innerHTML = str; }; var pre = function ( n ) { var tmp = tmps[n - 1]; currentTmp = tmp; clearItem(); domUtils.setStyles( preitem.childNodes[n - 1], { "background-color":"lemonChiffon", "border":"#ccc 1px solid" } ); preview.innerHTML = tmp.preHtml ? tmp.preHtml : ""; }; var clearItem = function () { var items = preitem.children; for ( var i = 0, item; item = items[i++]; ) { domUtils.setStyles( item, { "background-color":"", "border":"white 1px solid" } ); } }; dialog.onok = function () { if ( !$G( "issave" ).checked ){ me.execCommand( "cleardoc" ); } var obj = { html:currentTmp && currentTmp.html }; me.execCommand( "template", obj ); }; initPre(); window.pre = pre; pre(2) })(); ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/video/video.css ================================================ @charset "utf-8"; .wrapper{ width: 570px;_width:575px;margin: 10px auto; zoom:1;position: relative} .tabbody{height: 335px;} .tabbody .panel { position: absolute; width: 0; height: 0; background: #fff; overflow: hidden; display: none; } .tabbody .panel.focus { width: 100%; height: 335px; display: block; } .tabbody .panel table td{vertical-align: middle;} #videoUrl { width: 490px; height: 21px; line-height: 21px; margin: 8px 5px; background: #FFF; border: 1px solid #d7d7d7; } #videoSearchTxt{margin-left:15px;background: #FFF;width:200px;height:21px;line-height:21px;border: 1px solid #d7d7d7;} #searchList{width: 570px;overflow: auto;zoom:1;height: 270px;} #searchList div{float: left;width: 120px;height: 135px;margin: 5px 15px;} #searchList img{margin: 2px 8px;cursor: pointer;border: 2px solid #fff} /*不用缩略图*/ #searchList p{margin-left: 10px;} #videoType{ width: 65px; height: 23px; line-height: 22px; border: 1px solid #d7d7d7; } #videoSearchBtn,#videoSearchReset{ /*width: 80px;*/ height: 25px; line-height: 25px; background: #eee; border: 1px solid #d7d7d7; cursor: pointer; padding: 0 5px; } #preview{position: relative;width: 420px;padding:0;overflow: hidden; margin-left: 10px; _margin-left:5px; height: 280px;background-color: #ddd;float: left} #preview .previewMsg {position:absolute;top:0;margin:0;padding:0;height:280px;width:100%;background-color: #666;} #preview .previewMsg span{display:block;margin: 125px auto 0 auto;text-align:center;font-size:18px;color:#fff;} #preview .previewVideo {position:absolute;top:0;margin:0;padding:0;height:280px;width:100%;} .edui-video-wrapper fieldset{ border: 1px solid #ddd; padding-left: 5px; margin-bottom: 20px; padding-bottom: 5px; width: 115px; } #videoInfo {width: 120px;float: left;margin-left: 10px;_margin-left:7px;} fieldset{ border: 1px solid #ddd; padding-left: 5px; margin-bottom: 20px; padding-bottom: 5px; width: 115px; } fieldset legend{font-weight: bold;} fieldset p{line-height: 30px;} fieldset input.txt{ width: 65px; height: 21px; line-height: 21px; margin: 8px 5px; background: #FFF; border: 1px solid #d7d7d7; } label.url{font-weight: bold;margin-left: 5px;color: #06c;} #videoFloat div{cursor:pointer;opacity: 0.5;filter: alpha(opacity = 50);margin:9px;_margin:5px;width:38px;height:36px;float:left;} #videoFloat .focus{opacity: 1;filter: alpha(opacity = 100)} span.view{display: inline-block;width: 30px;float: right;cursor: pointer;color: blue} /* upload video */ .tabbody #upload.panel { width: 0; height: 0; overflow: hidden; position: absolute !important; clip: rect(1px, 1px, 1px, 1px); background: #fff; display: block; } .tabbody #upload.panel.focus { width: 100%; height: 335px; display: block; clip: auto; } #upload_alignment div{cursor:pointer;opacity: 0.5;filter: alpha(opacity = 50);margin:9px;_margin:5px;width:38px;height:36px;float:left;} #upload_alignment .focus{opacity: 1;filter: alpha(opacity = 100)} #upload_left { width:427px; float:left; } #upload_left .controller { height: 30px; clear: both; } #uploadVideoInfo{margin-top:10px;float:right;padding-right:8px;} #upload .queueList { margin: 0; } #upload p { margin: 0; } .element-invisible { width: 0 !important; height: 0 !important; border: 0; padding: 0; margin: 0; overflow: hidden; position: absolute !important; clip: rect(1px, 1px, 1px, 1px); } #upload .placeholder { margin: 10px; margin-right:0; border: 2px dashed #e6e6e6; *border: 0px dashed #e6e6e6; height: 161px; padding-top: 150px; text-align: center; width: 97%; float: left; background: url(./images/image.png) center 70px no-repeat; color: #cccccc; font-size: 18px; position: relative; top:0; *margin-left: 0; *left: 10px; } #upload .placeholder .webuploader-pick { font-size: 18px; background: #00b7ee; border-radius: 3px; line-height: 44px; padding: 0 30px; *width: 120px; color: #fff; display: inline-block; margin: 0 auto 20px auto; cursor: pointer; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); } #upload .placeholder .webuploader-pick-hover { background: #00a2d4; } #filePickerContainer { text-align: center; } #upload .placeholder .flashTip { color: #666666; font-size: 12px; position: absolute; width: 100%; text-align: center; bottom: 20px; } #upload .placeholder .flashTip a { color: #0785d1; text-decoration: none; } #upload .placeholder .flashTip a:hover { text-decoration: underline; } #upload .placeholder.webuploader-dnd-over { border-color: #999999; } #upload .filelist { list-style: none; margin: 0; padding: 0; overflow-x: hidden; overflow-y: auto; position: relative; height: 285px; } #upload .filelist:after { content: ''; display: block; width: 0; height: 0; overflow: hidden; clear: both; } #upload .filelist li { width: 113px; height: 113px; background: url(./images/bg.png); text-align: center; margin: 15px 0 0 20px; *margin: 15px 0 0 15px; position: relative; display: block; float: left; overflow: hidden; font-size: 12px; } #upload .filelist li p.log { position: relative; top: -45px; } #upload .filelist li p.title { position: absolute; top: 0; left: 0; width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; top: 5px; text-indent: 5px; text-align: left; } #upload .filelist li p.progress { position: absolute; width: 100%; bottom: 0; left: 0; height: 8px; overflow: hidden; z-index: 50; margin: 0; border-radius: 0; background: none; -webkit-box-shadow: 0 0 0; } #upload .filelist li p.progress span { display: none; overflow: hidden; width: 0; height: 100%; background: #1483d8 url(./images/progress.png) repeat-x; -webit-transition: width 200ms linear; -moz-transition: width 200ms linear; -o-transition: width 200ms linear; -ms-transition: width 200ms linear; transition: width 200ms linear; -webkit-animation: progressmove 2s linear infinite; -moz-animation: progressmove 2s linear infinite; -o-animation: progressmove 2s linear infinite; -ms-animation: progressmove 2s linear infinite; animation: progressmove 2s linear infinite; -webkit-transform: translateZ(0); } @-webkit-keyframes progressmove { 0% { background-position: 0 0; } 100% { background-position: 17px 0; } } @-moz-keyframes progressmove { 0% { background-position: 0 0; } 100% { background-position: 17px 0; } } @keyframes progressmove { 0% { background-position: 0 0; } 100% { background-position: 17px 0; } } #upload .filelist li p.imgWrap { position: relative; z-index: 2; line-height: 113px; vertical-align: middle; overflow: hidden; width: 113px; height: 113px; -webkit-transform-origin: 50% 50%; -moz-transform-origin: 50% 50%; -o-transform-origin: 50% 50%; -ms-transform-origin: 50% 50%; transform-origin: 50% 50%; -webit-transition: 200ms ease-out; -moz-transition: 200ms ease-out; -o-transition: 200ms ease-out; -ms-transition: 200ms ease-out; transition: 200ms ease-out; } #upload .filelist li p.imgWrap.notimage { margin-top: 0; width: 111px; height: 111px; border: 1px #eeeeee solid; } #upload .filelist li p.imgWrap.notimage i.file-preview { margin-top: 15px; } #upload .filelist li img { width: 100%; } #upload .filelist li p.error { background: #f43838; color: #fff; position: absolute; bottom: 0; left: 0; height: 28px; line-height: 28px; width: 100%; z-index: 100; display:none; } #upload .filelist li .success { display: block; position: absolute; left: 0; bottom: 0; height: 40px; width: 100%; z-index: 200; background: url(./images/success.png) no-repeat right bottom; background-image: url(./images/success.gif) \9; } #upload .filelist li.filePickerBlock { width: 113px; height: 113px; background: url(./images/image.png) no-repeat center 12px; border: 1px solid #eeeeee; border-radius: 0; } #upload .filelist li.filePickerBlock div.webuploader-pick { width: 100%; height: 100%; margin: 0; padding: 0; opacity: 0; background: none; font-size: 0; } #upload .filelist div.file-panel { position: absolute; height: 0; filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#80000000', endColorstr='#80000000') \0; background: rgba(0, 0, 0, 0.5); width: 100%; top: 0; left: 0; overflow: hidden; z-index: 300; } #upload .filelist div.file-panel span { width: 24px; height: 24px; display: inline; float: right; text-indent: -9999px; overflow: hidden; background: url(./images/icons.png) no-repeat; background: url(./images/icons.gif) no-repeat \9; margin: 5px 1px 1px; cursor: pointer; -webkit-tap-highlight-color: rgba(0,0,0,0); -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } #upload .filelist div.file-panel span.rotateLeft { display:none; background-position: 0 -24px; } #upload .filelist div.file-panel span.rotateLeft:hover { background-position: 0 0; } #upload .filelist div.file-panel span.rotateRight { display:none; background-position: -24px -24px; } #upload .filelist div.file-panel span.rotateRight:hover { background-position: -24px 0; } #upload .filelist div.file-panel span.cancel { background-position: -48px -24px; } #upload .filelist div.file-panel span.cancel:hover { background-position: -48px 0; } #upload .statusBar { height: 45px; border-bottom: 1px solid #dadada; margin: 0 10px; padding: 0; line-height: 45px; vertical-align: middle; position: relative; } #upload .statusBar .progress { border: 1px solid #1483d8; width: 198px; background: #fff; height: 18px; position: absolute; top: 12px; display: none; text-align: center; line-height: 18px; color: #6dbfff; margin: 0 10px 0 0; } #upload .statusBar .progress span.percentage { width: 0; height: 100%; left: 0; top: 0; background: #1483d8; position: absolute; } #upload .statusBar .progress span.text { position: relative; z-index: 10; } #upload .statusBar .info { display: inline-block; font-size: 14px; color: #666666; } #upload .statusBar .btns { position: absolute; top: 7px; right: 0; line-height: 30px; } #filePickerBtn { display: inline-block; float: left; } #upload .statusBar .btns .webuploader-pick, #upload .statusBar .btns .uploadBtn, #upload .statusBar .btns .uploadBtn.state-uploading, #upload .statusBar .btns .uploadBtn.state-paused { background: #ffffff; border: 1px solid #cfcfcf; color: #565656; padding: 0 18px; display: inline-block; border-radius: 3px; margin-left: 10px; cursor: pointer; font-size: 14px; float: left; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } #upload .statusBar .btns .webuploader-pick-hover, #upload .statusBar .btns .uploadBtn:hover, #upload .statusBar .btns .uploadBtn.state-uploading:hover, #upload .statusBar .btns .uploadBtn.state-paused:hover { background: #f0f0f0; } #upload .statusBar .btns .uploadBtn, #upload .statusBar .btns .uploadBtn.state-paused{ background: #00b7ee; color: #fff; border-color: transparent; } #upload .statusBar .btns .uploadBtn:hover, #upload .statusBar .btns .uploadBtn.state-paused:hover{ background: #00a2d4; } #upload .statusBar .btns .uploadBtn.disabled { pointer-events: none; filter:alpha(opacity=60); -moz-opacity:0.6; -khtml-opacity: 0.6; opacity: 0.6; } /* 在线文件的文件预览图标 */ i.file-preview { display: block; margin: 10px auto; width: 70px; height: 70px; background-image: url("./images/file-icons.png"); background-image: url("./images/file-icons.gif") \9; background-position: -140px center; background-repeat: no-repeat; } i.file-preview.file-type-dir{ background-position: 0 center; } i.file-preview.file-type-file{ background-position: -140px center; } i.file-preview.file-type-filelist{ background-position: -210px center; } i.file-preview.file-type-zip, i.file-preview.file-type-rar, i.file-preview.file-type-7z, i.file-preview.file-type-tar, i.file-preview.file-type-gz, i.file-preview.file-type-bz2{ background-position: -280px center; } i.file-preview.file-type-xls, i.file-preview.file-type-xlsx{ background-position: -350px center; } i.file-preview.file-type-doc, i.file-preview.file-type-docx{ background-position: -420px center; } i.file-preview.file-type-ppt, i.file-preview.file-type-pptx{ background-position: -490px center; } i.file-preview.file-type-vsd{ background-position: -560px center; } i.file-preview.file-type-pdf{ background-position: -630px center; } i.file-preview.file-type-txt, i.file-preview.file-type-md, i.file-preview.file-type-json, i.file-preview.file-type-htm, i.file-preview.file-type-xml, i.file-preview.file-type-html, i.file-preview.file-type-js, i.file-preview.file-type-css, i.file-preview.file-type-php, i.file-preview.file-type-jsp, i.file-preview.file-type-asp{ background-position: -700px center; } i.file-preview.file-type-apk{ background-position: -770px center; } i.file-preview.file-type-exe{ background-position: -840px center; } i.file-preview.file-type-ipa{ background-position: -910px center; } i.file-preview.file-type-mp4, i.file-preview.file-type-swf, i.file-preview.file-type-mkv, i.file-preview.file-type-avi, i.file-preview.file-type-flv, i.file-preview.file-type-mov, i.file-preview.file-type-mpg, i.file-preview.file-type-mpeg, i.file-preview.file-type-ogv, i.file-preview.file-type-webm, i.file-preview.file-type-rm, i.file-preview.file-type-rmvb{ background-position: -980px center; } i.file-preview.file-type-ogg, i.file-preview.file-type-wav, i.file-preview.file-type-wmv, i.file-preview.file-type-mid, i.file-preview.file-type-mp3{ background-position: -1050px center; } i.file-preview.file-type-jpg, i.file-preview.file-type-jpeg, i.file-preview.file-type-gif, i.file-preview.file-type-bmp, i.file-preview.file-type-png, i.file-preview.file-type-psd{ background-position: -140px center; } ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/video/video.html ================================================
    0%
    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/video/video.js ================================================ /** * Created by JetBrains PhpStorm. * User: taoqili * Date: 12-2-20 * Time: 上午11:19 * To change this template use File | Settings | File Templates. */ (function(){ var video = {}, uploadVideoList = [], isModifyUploadVideo = false, uploadFile; window.onload = function(){ $focus($G("videoUrl")); initTabs(); initVideo(); initUpload(); }; /* 初始化tab标签 */ function initTabs(){ var tabs = $G('tabHeads').children; for (var i = 0; i < tabs.length; i++) { domUtils.on(tabs[i], "click", function (e) { var j, bodyId, target = e.target || e.srcElement; for (j = 0; j < tabs.length; j++) { bodyId = tabs[j].getAttribute('data-content-id'); if(tabs[j] == target){ domUtils.addClass(tabs[j], 'focus'); domUtils.addClass($G(bodyId), 'focus'); }else { domUtils.removeClasses(tabs[j], 'focus'); domUtils.removeClasses($G(bodyId), 'focus'); } } }); } } function initVideo(){ createAlignButton( ["videoFloat", "upload_alignment"] ); addUrlChangeListener($G("videoUrl")); addOkListener(); //编辑视频时初始化相关信息 (function(){ var img = editor.selection.getRange().getClosedNode(),url; if(img && img.className){ var hasFakedClass = (img.className == "edui-faked-video"), hasUploadClass = img.className.indexOf("edui-upload-video")!=-1; if(hasFakedClass || hasUploadClass) { $G("videoUrl").value = url = img.getAttribute("_url"); $G("videoWidth").value = img.width; $G("videoHeight").value = img.height; var align = domUtils.getComputedStyle(img,"float"), parentAlign = domUtils.getComputedStyle(img.parentNode,"text-align"); updateAlignButton(parentAlign==="center"?"center":align); } if(hasUploadClass) { isModifyUploadVideo = true; } } createPreviewVideo(url); })(); } /** * 监听确认和取消两个按钮事件,用户执行插入或者清空正在播放的视频实例操作 */ function addOkListener(){ dialog.onok = function(){ $G("preview").innerHTML = ""; var currentTab = findFocus("tabHeads","tabSrc"); switch(currentTab){ case "video": return insertSingle(); break; case "videoSearch": return insertSearch("searchList"); break; case "upload": return insertUpload(); break; } }; dialog.oncancel = function(){ $G("preview").innerHTML = ""; }; } /** * 依据传入的align值更新按钮信息 * @param align */ function updateAlignButton( align ) { var aligns = $G( "videoFloat" ).children; for ( var i = 0, ci; ci = aligns[i++]; ) { if ( ci.getAttribute( "name" ) == align ) { if ( ci.className !="focus" ) { ci.className = "focus"; } } else { if ( ci.className =="focus" ) { ci.className = ""; } } } } /** * 将单个视频信息插入编辑器中 */ function insertSingle(){ var width = $G("videoWidth"), height = $G("videoHeight"), url=$G('videoUrl').value, align = findFocus("videoFloat","name"); if(!url) return false; if ( !checkNum( [width, height] ) ) return false; editor.execCommand('insertvideo', { url: convert_url(url), width: width.value, height: height.value, align: align }, isModifyUploadVideo ? 'upload':null); } /** * 将元素id下的所有代表视频的图片插入编辑器中 * @param id */ function insertSearch(id){ var imgs = domUtils.getElementsByTagName($G(id),"img"), videoObjs=[]; for(var i=0,img; img=imgs[i++];){ if(img.getAttribute("selected")){ videoObjs.push({ url:img.getAttribute("ue_video_url"), width:420, height:280, align:"none" }); } } editor.execCommand('insertvideo',videoObjs); } /** * 找到id下具有focus类的节点并返回该节点下的某个属性 * @param id * @param returnProperty */ function findFocus( id, returnProperty ) { var tabs = $G( id ).children, property; for ( var i = 0, ci; ci = tabs[i++]; ) { if ( ci.className=="focus" ) { property = ci.getAttribute( returnProperty ); break; } } return property; } function convert_url(url){ if ( !url ) return ''; url = utils.trim(url) .replace(/v\.youku\.com\/v_show\/id_([\w\-=]+)\.html/i, 'player.youku.com/player.php/sid/$1/v.swf') .replace(/(www\.)?youtube\.com\/watch\?v=([\w\-]+)/i, "www.youtube.com/v/$2") .replace(/youtu.be\/(\w+)$/i, "www.youtube.com/v/$1") .replace(/v\.ku6\.com\/.+\/([\w\.]+)\.html.*$/i, "player.ku6.com/refer/$1/v.swf") .replace(/www\.56\.com\/u\d+\/v_([\w\-]+)\.html/i, "player.56.com/v_$1.swf") .replace(/www.56.com\/w\d+\/play_album\-aid\-\d+_vid\-([^.]+)\.html/i, "player.56.com/v_$1.swf") .replace(/v\.pps\.tv\/play_([\w]+)\.html.*$/i, "player.pps.tv/player/sid/$1/v.swf") .replace(/www\.letv\.com\/ptv\/vplay\/([\d]+)\.html.*$/i, "i7.imgs.letv.com/player/swfPlayer.swf?id=$1&autoplay=0") .replace(/www\.tudou\.com\/programs\/view\/([\w\-]+)\/?/i, "www.tudou.com/v/$1") .replace(/v\.qq\.com\/cover\/[\w]+\/[\w]+\/([\w]+)\.html/i, "static.video.qq.com/TPout.swf?vid=$1") .replace(/v\.qq\.com\/.+[\?\&]vid=([^&]+).*$/i, "static.video.qq.com/TPout.swf?vid=$1") .replace(/my\.tv\.sohu\.com\/[\w]+\/[\d]+\/([\d]+)\.shtml.*$/i, "share.vrs.sohu.com/my/v.swf&id=$1"); return url; } /** * 检测传入的所有input框中输入的长宽是否是正数 * @param nodes input框集合, */ function checkNum( nodes ) { for ( var i = 0, ci; ci = nodes[i++]; ) { var value = ci.value; if ( !isNumber( value ) && value) { alert( lang.numError ); ci.value = ""; ci.focus(); return false; } } return true; } /** * 数字判断 * @param value */ function isNumber( value ) { return /(0|^[1-9]\d*$)/.test( value ); } /** * 创建图片浮动选择按钮 * @param ids */ function createAlignButton( ids ) { for ( var i = 0, ci; ci = ids[i++]; ) { var floatContainer = $G( ci ), nameMaps = {"none":lang['default'], "left":lang.floatLeft, "right":lang.floatRight, "center":lang.block}; for ( var j in nameMaps ) { var div = document.createElement( "div" ); div.setAttribute( "name", j ); if ( j == "none" ) div.className="focus"; div.style.cssText = "background:url(images/" + j + "_focus.jpg);"; div.setAttribute( "title", nameMaps[j] ); floatContainer.appendChild( div ); } switchSelect( ci ); } } /** * 选择切换 * @param selectParentId */ function switchSelect( selectParentId ) { var selects = $G( selectParentId ).children; for ( var i = 0, ci; ci = selects[i++]; ) { domUtils.on( ci, "click", function () { for ( var j = 0, cj; cj = selects[j++]; ) { cj.className = ""; cj.removeAttribute && cj.removeAttribute( "class" ); } this.className = "focus"; } ) } } /** * 监听url改变事件 * @param url */ function addUrlChangeListener(url){ if (browser.ie) { url.onpropertychange = function () { createPreviewVideo( this.value ); } } else { url.addEventListener( "input", function () { createPreviewVideo( this.value ); }, false ); } } /** * 根据url生成视频预览 * @param url */ function createPreviewVideo(url){ if ( !url )return; var conUrl = convert_url(url); conUrl = utils.unhtmlForUrl(conUrl); $G("preview").innerHTML = '
    '+lang.urlError+'
    '+ '' + ''; } /* 插入上传视频 */ function insertUpload(){ var videoObjs=[], uploadDir = editor.getOpt('videoUrlPrefix'), width = parseInt($G('upload_width').value, 10) || 420, height = parseInt($G('upload_height').value, 10) || 280, align = findFocus("upload_alignment","name") || 'none'; for(var key in uploadVideoList) { var file = uploadVideoList[key]; videoObjs.push({ url: uploadDir + file.url, width:width, height:height, align:align }); } var count = uploadFile.getQueueCount(); if (count) { $('.info', '#queueList').html('' + '还有2个未上传文件'.replace(/[\d]/, count) + ''); return false; } else { editor.execCommand('insertvideo', videoObjs, 'upload'); } } /*初始化上传标签*/ function initUpload(){ uploadFile = new UploadFile('queueList'); } /* 上传附件 */ function UploadFile(target) { this.$wrap = target.constructor == String ? $('#' + target) : $(target); this.init(); } UploadFile.prototype = { init: function () { this.fileList = []; this.initContainer(); this.initUploader(); }, initContainer: function () { this.$queue = this.$wrap.find('.filelist'); }, /* 初始化容器 */ initUploader: function () { var _this = this, $ = jQuery, // just in case. Make sure it's not an other libaray. $wrap = _this.$wrap, // 图片容器 $queue = $wrap.find('.filelist'), // 状态栏,包括进度和控制按钮 $statusBar = $wrap.find('.statusBar'), // 文件总体选择信息。 $info = $statusBar.find('.info'), // 上传按钮 $upload = $wrap.find('.uploadBtn'), // 上传按钮 $filePickerBtn = $wrap.find('.filePickerBtn'), // 上传按钮 $filePickerBlock = $wrap.find('.filePickerBlock'), // 没选择文件之前的内容。 $placeHolder = $wrap.find('.placeholder'), // 总体进度条 $progress = $statusBar.find('.progress').hide(), // 添加的文件数量 fileCount = 0, // 添加的文件总大小 fileSize = 0, // 优化retina, 在retina下这个值是2 ratio = window.devicePixelRatio || 1, // 缩略图大小 thumbnailWidth = 113 * ratio, thumbnailHeight = 113 * ratio, // 可能有pedding, ready, uploading, confirm, done. state = '', // 所有文件的进度信息,key为file id percentages = {}, supportTransition = (function () { var s = document.createElement('p').style, r = 'transition' in s || 'WebkitTransition' in s || 'MozTransition' in s || 'msTransition' in s || 'OTransition' in s; s = null; return r; })(), // WebUploader实例 uploader, actionUrl = editor.getActionUrl(editor.getOpt('videoActionName')), fileMaxSize = editor.getOpt('videoMaxSize'), acceptExtensions = (editor.getOpt('videoAllowFiles') || []).join('').replace(/\./g, ',').replace(/^[,]/, '');; if (!WebUploader.Uploader.support()) { $('#filePickerReady').after($('
    ').html(lang.errorNotSupport)).hide(); return; } else if (!editor.getOpt('videoActionName')) { $('#filePickerReady').after($('
    ').html(lang.errorLoadConfig)).hide(); return; } uploader = _this.uploader = WebUploader.create({ pick: { id: '#filePickerReady', label: lang.uploadSelectFile }, swf: '../../third-party/webuploader/Uploader.swf', server: actionUrl, fileVal: editor.getOpt('videoFieldName'), duplicate: true, fileSingleSizeLimit: fileMaxSize, compress: false }); uploader.addButton({ id: '#filePickerBlock' }); uploader.addButton({ id: '#filePickerBtn', label: lang.uploadAddFile }); setState('pedding'); // 当有文件添加进来时执行,负责view的创建 function addFile(file) { var $li = $('
  • ' + '

    ' + file.name + '

    ' + '

    ' + '

    ' + '
  • '), $btns = $('
    ' + '' + lang.uploadDelete + '' + '' + lang.uploadTurnRight + '' + '' + lang.uploadTurnLeft + '
    ').appendTo($li), $prgress = $li.find('p.progress span'), $wrap = $li.find('p.imgWrap'), $info = $('

    ').hide().appendTo($li), showError = function (code) { switch (code) { case 'exceed_size': text = lang.errorExceedSize; break; case 'interrupt': text = lang.errorInterrupt; break; case 'http': text = lang.errorHttp; break; case 'not_allow_type': text = lang.errorFileType; break; default: text = lang.errorUploadRetry; break; } $info.text(text).show(); }; if (file.getStatus() === 'invalid') { showError(file.statusText); } else { $wrap.text(lang.uploadPreview); if ('|png|jpg|jpeg|bmp|gif|'.indexOf('|'+file.ext.toLowerCase()+'|') == -1) { $wrap.empty().addClass('notimage').append('' + '' + file.name + ''); } else { if (browser.ie && browser.version <= 7) { $wrap.text(lang.uploadNoPreview); } else { uploader.makeThumb(file, function (error, src) { if (error || !src || (/^data:/.test(src) && browser.ie && browser.version <= 7)) { $wrap.text(lang.uploadNoPreview); } else { var $img = $(''); $wrap.empty().append($img); $img.on('error', function () { $wrap.text(lang.uploadNoPreview); }); } }, thumbnailWidth, thumbnailHeight); } } percentages[ file.id ] = [ file.size, 0 ]; file.rotation = 0; /* 检查文件格式 */ if (!file.ext || acceptExtensions.indexOf(file.ext.toLowerCase()) == -1) { showError('not_allow_type'); uploader.removeFile(file); } } file.on('statuschange', function (cur, prev) { if (prev === 'progress') { $prgress.hide().width(0); } else if (prev === 'queued') { $li.off('mouseenter mouseleave'); $btns.remove(); } // 成功 if (cur === 'error' || cur === 'invalid') { showError(file.statusText); percentages[ file.id ][ 1 ] = 1; } else if (cur === 'interrupt') { showError('interrupt'); } else if (cur === 'queued') { percentages[ file.id ][ 1 ] = 0; } else if (cur === 'progress') { $info.hide(); $prgress.css('display', 'block'); } else if (cur === 'complete') { } $li.removeClass('state-' + prev).addClass('state-' + cur); }); $li.on('mouseenter', function () { $btns.stop().animate({height: 30}); }); $li.on('mouseleave', function () { $btns.stop().animate({height: 0}); }); $btns.on('click', 'span', function () { var index = $(this).index(), deg; switch (index) { case 0: uploader.removeFile(file); return; case 1: file.rotation += 90; break; case 2: file.rotation -= 90; break; } if (supportTransition) { deg = 'rotate(' + file.rotation + 'deg)'; $wrap.css({ '-webkit-transform': deg, '-mos-transform': deg, '-o-transform': deg, 'transform': deg }); } else { $wrap.css('filter', 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' + (~~((file.rotation / 90) % 4 + 4) % 4) + ')'); } }); $li.insertBefore($filePickerBlock); } // 负责view的销毁 function removeFile(file) { var $li = $('#' + file.id); delete percentages[ file.id ]; updateTotalProgress(); $li.off().find('.file-panel').off().end().remove(); } function updateTotalProgress() { var loaded = 0, total = 0, spans = $progress.children(), percent; $.each(percentages, function (k, v) { total += v[ 0 ]; loaded += v[ 0 ] * v[ 1 ]; }); percent = total ? loaded / total : 0; spans.eq(0).text(Math.round(percent * 100) + '%'); spans.eq(1).css('width', Math.round(percent * 100) + '%'); updateStatus(); } function setState(val, files) { if (val != state) { var stats = uploader.getStats(); $upload.removeClass('state-' + state); $upload.addClass('state-' + val); switch (val) { /* 未选择文件 */ case 'pedding': $queue.addClass('element-invisible'); $statusBar.addClass('element-invisible'); $placeHolder.removeClass('element-invisible'); $progress.hide(); $info.hide(); uploader.refresh(); break; /* 可以开始上传 */ case 'ready': $placeHolder.addClass('element-invisible'); $queue.removeClass('element-invisible'); $statusBar.removeClass('element-invisible'); $progress.hide(); $info.show(); $upload.text(lang.uploadStart); uploader.refresh(); break; /* 上传中 */ case 'uploading': $progress.show(); $info.hide(); $upload.text(lang.uploadPause); break; /* 暂停上传 */ case 'paused': $progress.show(); $info.hide(); $upload.text(lang.uploadContinue); break; case 'confirm': $progress.show(); $info.hide(); $upload.text(lang.uploadStart); stats = uploader.getStats(); if (stats.successNum && !stats.uploadFailNum) { setState('finish'); return; } break; case 'finish': $progress.hide(); $info.show(); if (stats.uploadFailNum) { $upload.text(lang.uploadRetry); } else { $upload.text(lang.uploadStart); } break; } state = val; updateStatus(); } if (!_this.getQueueCount()) { $upload.addClass('disabled') } else { $upload.removeClass('disabled') } } function updateStatus() { var text = '', stats; if (state === 'ready') { text = lang.updateStatusReady.replace('_', fileCount).replace('_KB', WebUploader.formatSize(fileSize)); } else if (state === 'confirm') { stats = uploader.getStats(); if (stats.uploadFailNum) { text = lang.updateStatusConfirm.replace('_', stats.successNum).replace('_', stats.successNum); } } else { stats = uploader.getStats(); text = lang.updateStatusFinish.replace('_', fileCount). replace('_KB', WebUploader.formatSize(fileSize)). replace('_', stats.successNum); if (stats.uploadFailNum) { text += lang.updateStatusError.replace('_', stats.uploadFailNum); } } $info.html(text); } uploader.on('fileQueued', function (file) { fileCount++; fileSize += file.size; if (fileCount === 1) { $placeHolder.addClass('element-invisible'); $statusBar.show(); } addFile(file); }); uploader.on('fileDequeued', function (file) { fileCount--; fileSize -= file.size; removeFile(file); updateTotalProgress(); }); uploader.on('filesQueued', function (file) { if (!uploader.isInProgress() && (state == 'pedding' || state == 'finish' || state == 'confirm' || state == 'ready')) { setState('ready'); } updateTotalProgress(); }); uploader.on('all', function (type, files) { switch (type) { case 'uploadFinished': setState('confirm', files); break; case 'startUpload': /* 添加额外的GET参数 */ var params = utils.serializeParam(editor.queryCommandValue('serverparam')) || '', url = utils.formatUrl(actionUrl + (actionUrl.indexOf('?') == -1 ? '?':'&') + 'encode=utf-8&' + params); uploader.option('server', url); setState('uploading', files); break; case 'stopUpload': setState('paused', files); break; } }); uploader.on('uploadBeforeSend', function (file, data, header) { //这里可以通过data对象添加POST参数 header['X_Requested_With'] = 'XMLHttpRequest'; // HaoChuan9421 if(editor.options.headers && Object.prototype.toString.apply(editor.options.headers) === "[object Object]"){ for(var key in editor.options.headers){ header[key] = editor.options.headers[key] } } }); uploader.on('uploadProgress', function (file, percentage) { var $li = $('#' + file.id), $percent = $li.find('.progress span'); $percent.css('width', percentage * 100 + '%'); percentages[ file.id ][ 1 ] = percentage; updateTotalProgress(); }); uploader.on('uploadSuccess', function (file, ret) { var $file = $('#' + file.id); try { var responseText = (ret._raw || ret), json = utils.str2json(responseText); if (json.state == 'SUCCESS') { uploadVideoList.push({ 'url': json.url, 'type': json.type, 'original':json.original }); $file.append(''); } else { $file.find('.error').text(json.state).show(); } } catch (e) { $file.find('.error').text(lang.errorServerUpload).show(); } }); uploader.on('uploadError', function (file, code) { }); uploader.on('error', function (code, file) { if (code == 'Q_TYPE_DENIED' || code == 'F_EXCEED_SIZE') { addFile(file); } }); uploader.on('uploadComplete', function (file, ret) { }); $upload.on('click', function () { if ($(this).hasClass('disabled')) { return false; } if (state === 'ready') { uploader.upload(); } else if (state === 'paused') { uploader.upload(); } else if (state === 'uploading') { uploader.stop(); } }); $upload.addClass('state-' + state); updateTotalProgress(); }, getQueueCount: function () { var file, i, status, readyFile = 0, files = this.uploader.getFiles(); for (i = 0; file = files[i++]; ) { status = file.getStatus(); if (status == 'queued' || status == 'uploading' || status == 'progress') readyFile++; } return readyFile; }, refresh: function(){ this.uploader.refresh(); } }; })(); ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/webapp/webapp.html ================================================
    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/wordimage/tangram.js ================================================ // Copyright (c) 2009, Baidu Inc. All rights reserved. // // Licensed under the BSD License // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http:// tangram.baidu.com/license.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @namespace T Tangram七巧板 * @name T * @version 1.6.0 */ /** * 声明baidu包 * @author: allstar, erik, meizz, berg */ var T, baidu = T = baidu || {version: "1.5.0"}; baidu.guid = "$BAIDU$"; baidu.$$ = window[baidu.guid] = window[baidu.guid] || {global:{}}; /** * 使用flash资源封装的一些功能 * @namespace baidu.flash */ baidu.flash = baidu.flash || {}; /** * 操作dom的方法 * @namespace baidu.dom */ baidu.dom = baidu.dom || {}; /** * 从文档中获取指定的DOM元素 * @name baidu.dom.g * @function * @grammar baidu.dom.g(id) * @param {string|HTMLElement} id 元素的id或DOM元素. * @shortcut g,T.G * @meta standard * @see baidu.dom.q * * @return {HTMLElement|null} 获取的元素,查找不到时返回null,如果参数不合法,直接返回参数. */ baidu.dom.g = function(id) { if (!id) return null; if ('string' == typeof id || id instanceof String) { return document.getElementById(id); } else if (id.nodeName && (id.nodeType == 1 || id.nodeType == 9)) { return id; } return null; }; baidu.g = baidu.G = baidu.dom.g; /** * 操作数组的方法 * @namespace baidu.array */ baidu.array = baidu.array || {}; /** * 遍历数组中所有元素 * @name baidu.array.each * @function * @grammar baidu.array.each(source, iterator[, thisObject]) * @param {Array} source 需要遍历的数组 * @param {Function} iterator 对每个数组元素进行调用的函数,该函数有两个参数,第一个为数组元素,第二个为数组索引值,function (item, index)。 * @param {Object} [thisObject] 函数调用时的this指针,如果没有此参数,默认是当前遍历的数组 * @remark * each方法不支持对Object的遍历,对Object的遍历使用baidu.object.each 。 * @shortcut each * @meta standard * * @returns {Array} 遍历的数组 */ baidu.each = baidu.array.forEach = baidu.array.each = function (source, iterator, thisObject) { var returnValue, item, i, len = source.length; if ('function' == typeof iterator) { for (i = 0; i < len; i++) { item = source[i]; returnValue = iterator.call(thisObject || source, item, i); if (returnValue === false) { break; } } } return source; }; /** * 对语言层面的封装,包括类型判断、模块扩展、继承基类以及对象自定义事件的支持。 * @namespace baidu.lang */ baidu.lang = baidu.lang || {}; /** * 判断目标参数是否为function或Function实例 * @name baidu.lang.isFunction * @function * @grammar baidu.lang.isFunction(source) * @param {Any} source 目标参数 * @version 1.2 * @see baidu.lang.isString,baidu.lang.isObject,baidu.lang.isNumber,baidu.lang.isArray,baidu.lang.isElement,baidu.lang.isBoolean,baidu.lang.isDate * @meta standard * @returns {boolean} 类型判断结果 */ baidu.lang.isFunction = function (source) { return '[object Function]' == Object.prototype.toString.call(source); }; /** * 判断目标参数是否string类型或String对象 * @name baidu.lang.isString * @function * @grammar baidu.lang.isString(source) * @param {Any} source 目标参数 * @shortcut isString * @meta standard * @see baidu.lang.isObject,baidu.lang.isNumber,baidu.lang.isArray,baidu.lang.isElement,baidu.lang.isBoolean,baidu.lang.isDate * * @returns {boolean} 类型判断结果 */ baidu.lang.isString = function (source) { return '[object String]' == Object.prototype.toString.call(source); }; baidu.isString = baidu.lang.isString; /** * 判断浏览器类型和特性的属性 * @namespace baidu.browser */ baidu.browser = baidu.browser || {}; /** * 判断是否为opera浏览器 * @property opera opera版本号 * @grammar baidu.browser.opera * @meta standard * @see baidu.browser.ie,baidu.browser.firefox,baidu.browser.safari,baidu.browser.chrome * @returns {Number} opera版本号 */ /** * opera 从10开始不是用opera后面的字符串进行版本的判断 * 在Browser identification最后添加Version + 数字进行版本标识 * opera后面的数字保持在9.80不变 */ baidu.browser.opera = /opera(\/| )(\d+(\.\d+)?)(.+?(version\/(\d+(\.\d+)?)))?/i.test(navigator.userAgent) ? + ( RegExp["\x246"] || RegExp["\x242"] ) : undefined; /** * 在目标元素的指定位置插入HTML代码 * @name baidu.dom.insertHTML * @function * @grammar baidu.dom.insertHTML(element, position, html) * @param {HTMLElement|string} element 目标元素或目标元素的id * @param {string} position 插入html的位置信息,取值为beforeBegin,afterBegin,beforeEnd,afterEnd * @param {string} html 要插入的html * @remark * * 对于position参数,大小写不敏感
    * 参数的意思:beforeBegin<span>afterBegin this is span! beforeEnd</span> afterEnd
    * 此外,如果使用本函数插入带有script标签的HTML字符串,script标签对应的脚本将不会被执行。 * * @shortcut insertHTML * @meta standard * * @returns {HTMLElement} 目标元素 */ baidu.dom.insertHTML = function (element, position, html) { element = baidu.dom.g(element); var range,begin; if (element.insertAdjacentHTML && !baidu.browser.opera) { element.insertAdjacentHTML(position, html); } else { range = element.ownerDocument.createRange(); position = position.toUpperCase(); if (position == 'AFTERBEGIN' || position == 'BEFOREEND') { range.selectNodeContents(element); range.collapse(position == 'AFTERBEGIN'); } else { begin = position == 'BEFOREBEGIN'; range[begin ? 'setStartBefore' : 'setEndAfter'](element); range.collapse(begin); } range.insertNode(range.createContextualFragment(html)); } return element; }; baidu.insertHTML = baidu.dom.insertHTML; /** * 操作flash对象的方法,包括创建flash对象、获取flash对象以及判断flash插件的版本号 * @namespace baidu.swf */ baidu.swf = baidu.swf || {}; /** * 浏览器支持的flash插件版本 * @property version 浏览器支持的flash插件版本 * @grammar baidu.swf.version * @return {String} 版本号 * @meta standard */ baidu.swf.version = (function () { var n = navigator; if (n.plugins && n.mimeTypes.length) { var plugin = n.plugins["Shockwave Flash"]; if (plugin && plugin.description) { return plugin.description .replace(/([a-zA-Z]|\s)+/, "") .replace(/(\s)+r/, ".") + ".0"; } } else if (window.ActiveXObject && !window.opera) { for (var i = 12; i >= 2; i--) { try { var c = new ActiveXObject('ShockwaveFlash.ShockwaveFlash.' + i); if (c) { var version = c.GetVariable("$version"); return version.replace(/WIN/g,'').replace(/,/g,'.'); } } catch(e) {} } } })(); /** * 操作字符串的方法 * @namespace baidu.string */ baidu.string = baidu.string || {}; /** * 对目标字符串进行html编码 * @name baidu.string.encodeHTML * @function * @grammar baidu.string.encodeHTML(source) * @param {string} source 目标字符串 * @remark * 编码字符有5个:&<>"' * @shortcut encodeHTML * @meta standard * @see baidu.string.decodeHTML * * @returns {string} html编码后的字符串 */ baidu.string.encodeHTML = function (source) { return String(source) .replace(/&/g,'&') .replace(//g,'>') .replace(/"/g, """) .replace(/'/g, "'"); }; baidu.encodeHTML = baidu.string.encodeHTML; /** * 创建flash对象的html字符串 * @name baidu.swf.createHTML * @function * @grammar baidu.swf.createHTML(options) * * @param {Object} options 创建flash的选项参数 * @param {string} options.id 要创建的flash的标识 * @param {string} options.url flash文件的url * @param {String} options.errorMessage 未安装flash player或flash player版本号过低时的提示 * @param {string} options.ver 最低需要的flash player版本号 * @param {string} options.width flash的宽度 * @param {string} options.height flash的高度 * @param {string} options.align flash的对齐方式,允许值:middle/left/right/top/bottom * @param {string} options.base 设置用于解析swf文件中的所有相对路径语句的基本目录或URL * @param {string} options.bgcolor swf文件的背景色 * @param {string} options.salign 设置缩放的swf文件在由width和height设置定义的区域内的位置。允许值:l/r/t/b/tl/tr/bl/br * @param {boolean} options.menu 是否显示右键菜单,允许值:true/false * @param {boolean} options.loop 播放到最后一帧时是否重新播放,允许值: true/false * @param {boolean} options.play flash是否在浏览器加载时就开始播放。允许值:true/false * @param {string} options.quality 设置flash播放的画质,允许值:low/medium/high/autolow/autohigh/best * @param {string} options.scale 设置flash内容如何缩放来适应设置的宽高。允许值:showall/noborder/exactfit * @param {string} options.wmode 设置flash的显示模式。允许值:window/opaque/transparent * @param {string} options.allowscriptaccess 设置flash与页面的通信权限。允许值:always/never/sameDomain * @param {string} options.allownetworking 设置swf文件中允许使用的网络API。允许值:all/internal/none * @param {boolean} options.allowfullscreen 是否允许flash全屏。允许值:true/false * @param {boolean} options.seamlesstabbing 允许设置执行无缝跳格,从而使用户能跳出flash应用程序。该参数只能在安装Flash7及更高版本的Windows中使用。允许值:true/false * @param {boolean} options.devicefont 设置静态文本对象是否以设备字体呈现。允许值:true/false * @param {boolean} options.swliveconnect 第一次加载flash时浏览器是否应启动Java。允许值:true/false * @param {Object} options.vars 要传递给flash的参数,支持JSON或string类型。 * * @see baidu.swf.create * @meta standard * @returns {string} flash对象的html字符串 */ baidu.swf.createHTML = function (options) { options = options || {}; var version = baidu.swf.version, needVersion = options['ver'] || '6.0.0', vUnit1, vUnit2, i, k, len, item, tmpOpt = {}, encodeHTML = baidu.string.encodeHTML; for (k in options) { tmpOpt[k] = options[k]; } options = tmpOpt; if (version) { version = version.split('.'); needVersion = needVersion.split('.'); for (i = 0; i < 3; i++) { vUnit1 = parseInt(version[i], 10); vUnit2 = parseInt(needVersion[i], 10); if (vUnit2 < vUnit1) { break; } else if (vUnit2 > vUnit1) { return ''; } } } else { return ''; } var vars = options['vars'], objProperties = ['classid', 'codebase', 'id', 'width', 'height', 'align']; options['align'] = options['align'] || 'middle'; options['classid'] = 'clsid:d27cdb6e-ae6d-11cf-96b8-444553540000'; options['codebase'] = 'http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,0,0'; options['movie'] = options['url'] || ''; delete options['vars']; delete options['url']; if ('string' == typeof vars) { options['flashvars'] = vars; } else { var fvars = []; for (k in vars) { item = vars[k]; fvars.push(k + "=" + encodeURIComponent(item)); } options['flashvars'] = fvars.join('&'); } var str = [''); var params = { 'wmode' : 1, 'scale' : 1, 'quality' : 1, 'play' : 1, 'loop' : 1, 'menu' : 1, 'salign' : 1, 'bgcolor' : 1, 'base' : 1, 'allowscriptaccess' : 1, 'allownetworking' : 1, 'allowfullscreen' : 1, 'seamlesstabbing' : 1, 'devicefont' : 1, 'swliveconnect' : 1, 'flashvars' : 1, 'movie' : 1 }; for (k in options) { item = options[k]; k = k.toLowerCase(); if (params[k] && (item || item === false || item === 0)) { str.push(''); } } options['src'] = options['movie']; options['name'] = options['id']; delete options['id']; delete options['movie']; delete options['classid']; delete options['codebase']; options['type'] = 'application/x-shockwave-flash'; options['pluginspage'] = 'http://www.macromedia.com/go/getflashplayer'; str.push(''); return str.join(''); }; /** * 在页面中创建一个flash对象 * @name baidu.swf.create * @function * @grammar baidu.swf.create(options[, container]) * * @param {Object} options 创建flash的选项参数 * @param {string} options.id 要创建的flash的标识 * @param {string} options.url flash文件的url * @param {String} options.errorMessage 未安装flash player或flash player版本号过低时的提示 * @param {string} options.ver 最低需要的flash player版本号 * @param {string} options.width flash的宽度 * @param {string} options.height flash的高度 * @param {string} options.align flash的对齐方式,允许值:middle/left/right/top/bottom * @param {string} options.base 设置用于解析swf文件中的所有相对路径语句的基本目录或URL * @param {string} options.bgcolor swf文件的背景色 * @param {string} options.salign 设置缩放的swf文件在由width和height设置定义的区域内的位置。允许值:l/r/t/b/tl/tr/bl/br * @param {boolean} options.menu 是否显示右键菜单,允许值:true/false * @param {boolean} options.loop 播放到最后一帧时是否重新播放,允许值: true/false * @param {boolean} options.play flash是否在浏览器加载时就开始播放。允许值:true/false * @param {string} options.quality 设置flash播放的画质,允许值:low/medium/high/autolow/autohigh/best * @param {string} options.scale 设置flash内容如何缩放来适应设置的宽高。允许值:showall/noborder/exactfit * @param {string} options.wmode 设置flash的显示模式。允许值:window/opaque/transparent * @param {string} options.allowscriptaccess 设置flash与页面的通信权限。允许值:always/never/sameDomain * @param {string} options.allownetworking 设置swf文件中允许使用的网络API。允许值:all/internal/none * @param {boolean} options.allowfullscreen 是否允许flash全屏。允许值:true/false * @param {boolean} options.seamlesstabbing 允许设置执行无缝跳格,从而使用户能跳出flash应用程序。该参数只能在安装Flash7及更高版本的Windows中使用。允许值:true/false * @param {boolean} options.devicefont 设置静态文本对象是否以设备字体呈现。允许值:true/false * @param {boolean} options.swliveconnect 第一次加载flash时浏览器是否应启动Java。允许值:true/false * @param {Object} options.vars 要传递给flash的参数,支持JSON或string类型。 * * @param {HTMLElement|string} [container] flash对象的父容器元素,不传递该参数时在当前代码位置创建flash对象。 * @meta standard * @see baidu.swf.createHTML,baidu.swf.getMovie */ baidu.swf.create = function (options, target) { options = options || {}; var html = baidu.swf.createHTML(options) || options['errorMessage'] || ''; if (target && 'string' == typeof target) { target = document.getElementById(target); } baidu.dom.insertHTML( target || document.body ,'beforeEnd',html ); }; /** * 判断是否为ie浏览器 * @name baidu.browser.ie * @field * @grammar baidu.browser.ie * @returns {Number} IE版本号 */ baidu.browser.ie = baidu.ie = /msie (\d+\.\d+)/i.test(navigator.userAgent) ? (document.documentMode || + RegExp['\x241']) : undefined; /** * 移除数组中的项 * @name baidu.array.remove * @function * @grammar baidu.array.remove(source, match) * @param {Array} source 需要移除项的数组 * @param {Any} match 要移除的项 * @meta standard * @see baidu.array.removeAt * * @returns {Array} 移除后的数组 */ baidu.array.remove = function (source, match) { var len = source.length; while (len--) { if (len in source && source[len] === match) { source.splice(len, 1); } } return source; }; /** * 判断目标参数是否Array对象 * @name baidu.lang.isArray * @function * @grammar baidu.lang.isArray(source) * @param {Any} source 目标参数 * @meta standard * @see baidu.lang.isString,baidu.lang.isObject,baidu.lang.isNumber,baidu.lang.isElement,baidu.lang.isBoolean,baidu.lang.isDate * * @returns {boolean} 类型判断结果 */ baidu.lang.isArray = function (source) { return '[object Array]' == Object.prototype.toString.call(source); }; /** * 将一个变量转换成array * @name baidu.lang.toArray * @function * @grammar baidu.lang.toArray(source) * @param {mix} source 需要转换成array的变量 * @version 1.3 * @meta standard * @returns {array} 转换后的array */ baidu.lang.toArray = function (source) { if (source === null || source === undefined) return []; if (baidu.lang.isArray(source)) return source; if (typeof source.length !== 'number' || typeof source === 'string' || baidu.lang.isFunction(source)) { return [source]; } if (source.item) { var l = source.length, array = new Array(l); while (l--) array[l] = source[l]; return array; } return [].slice.call(source); }; /** * 获得flash对象的实例 * @name baidu.swf.getMovie * @function * @grammar baidu.swf.getMovie(name) * @param {string} name flash对象的名称 * @see baidu.swf.create * @meta standard * @returns {HTMLElement} flash对象的实例 */ baidu.swf.getMovie = function (name) { var movie = document[name], ret; return baidu.browser.ie == 9 ? movie && movie.length ? (ret = baidu.array.remove(baidu.lang.toArray(movie),function(item){ return item.tagName.toLowerCase() != "embed"; })).length == 1 ? ret[0] : ret : movie : movie || window[name]; }; baidu.flash._Base = (function(){ var prefix = 'bd__flash__'; /** * 创建一个随机的字符串 * @private * @return {String} */ function _createString(){ return prefix + Math.floor(Math.random() * 2147483648).toString(36); }; /** * 检查flash状态 * @private * @param {Object} target flash对象 * @return {Boolean} */ function _checkReady(target){ if(typeof target !== 'undefined' && typeof target.flashInit !== 'undefined' && target.flashInit()){ return true; }else{ return false; } }; /** * 调用之前进行压栈的函数 * @private * @param {Array} callQueue 调用队列 * @param {Object} target flash对象 * @return {Null} */ function _callFn(callQueue, target){ var result = null; callQueue = callQueue.reverse(); baidu.each(callQueue, function(item){ result = target.call(item.fnName, item.params); item.callBack(result); }); }; /** * 为传入的匿名函数创建函数名 * @private * @param {String|Function} fun 传入的匿名函数或者函数名 * @return {String} */ function _createFunName(fun){ var name = ''; if(baidu.lang.isFunction(fun)){ name = _createString(); window[name] = function(){ fun.apply(window, arguments); }; return name; }else if(baidu.lang.isString){ return fun; } }; /** * 绘制flash * @private * @param {Object} options 创建参数 * @return {Object} */ function _render(options){ if(!options.id){ options.id = _createString(); } var container = options.container || ''; delete(options.container); baidu.swf.create(options, container); return baidu.swf.getMovie(options.id); }; return function(options, callBack){ var me = this, autoRender = (typeof options.autoRender !== 'undefined' ? options.autoRender : true), createOptions = options.createOptions || {}, target = null, isReady = false, callQueue = [], timeHandle = null, callBack = callBack || []; /** * 将flash文件绘制到页面上 * @public * @return {Null} */ me.render = function(){ target = _render(createOptions); if(callBack.length > 0){ baidu.each(callBack, function(funName, index){ callBack[index] = _createFunName(options[funName] || new Function()); }); } me.call('setJSFuncName', [callBack]); }; /** * 返回flash状态 * @return {Boolean} */ me.isReady = function(){ return isReady; }; /** * 调用flash接口的统一入口 * @param {String} fnName 调用的函数名 * @param {Array} params 传入的参数组成的数组,若不许要参数,需传入空数组 * @param {Function} [callBack] 异步调用后将返回值作为参数的调用回调函数,如无返回值,可以不传入此参数 * @return {Null} */ me.call = function(fnName, params, callBack){ if(!fnName) return null; callBack = callBack || new Function(); var result = null; if(isReady){ result = target.call(fnName, params); callBack(result); }else{ callQueue.push({ fnName: fnName, params: params, callBack: callBack }); (!timeHandle) && (timeHandle = setInterval(_check, 200)); } }; /** * 为传入的匿名函数创建函数名 * @public * @param {String|Function} fun 传入的匿名函数或者函数名 * @return {String} */ me.createFunName = function(fun){ return _createFunName(fun); }; /** * 检查flash是否ready, 并进行调用 * @private * @return {Null} */ function _check(){ if(_checkReady(target)){ clearInterval(timeHandle); timeHandle = null; _call(); isReady = true; } }; /** * 调用之前进行压栈的函数 * @private * @return {Null} */ function _call(){ _callFn(callQueue, target); callQueue = []; } autoRender && me.render(); }; })(); /** * 创建flash based imageUploader * @class * @grammar baidu.flash.imageUploader(options) * @param {Object} createOptions 创建flash时需要的参数,请参照baidu.swf.create文档 * @config {Object} vars 创建imageUploader时所需要的参数 * @config {Number} vars.gridWidth 每一个预览图片所占的宽度,应该为flash寛的整除 * @config {Number} vars.gridHeight 每一个预览图片所占的高度,应该为flash高的整除 * @config {Number} vars.picWidth 单张预览图片的宽度 * @config {Number} vars.picHeight 单张预览图片的高度 * @config {String} vars.uploadDataFieldName POST请求中图片数据的key,默认值'picdata' * @config {String} vars.picDescFieldName POST请求中图片描述的key,默认值'picDesc' * @config {Number} vars.maxSize 文件的最大体积,单位'MB' * @config {Number} vars.compressSize 上传前如果图片体积超过该值,会先压缩 * @config {Number} vars.maxNum:32 最大上传多少个文件 * @config {Number} vars.compressLength 能接受的最大边长,超过该值会等比压缩 * @config {String} vars.url 上传的url地址 * @config {Number} vars.mode mode == 0时,是使用滚动条,mode == 1时,拉伸flash, 默认值为0 * @see baidu.swf.createHTML * @param {String} backgroundUrl 背景图片路径 * @param {String} listBacgroundkUrl 布局控件背景 * @param {String} buttonUrl 按钮图片不背景 * @param {String|Function} selectFileCallback 选择文件的回调 * @param {String|Function} exceedFileCallback文件超出限制的最大体积时的回调 * @param {String|Function} deleteFileCallback 删除文件的回调 * @param {String|Function} startUploadCallback 开始上传某个文件时的回调 * @param {String|Function} uploadCompleteCallback 某个文件上传完成的回调 * @param {String|Function} uploadErrorCallback 某个文件上传失败的回调 * @param {String|Function} allCompleteCallback 全部上传完成时的回调 * @param {String|Function} changeFlashHeight 改变Flash的高度,mode==1的时候才有用 */ baidu.flash.imageUploader = baidu.flash.imageUploader || function(options){ var me = this, options = options || {}, _flash = new baidu.flash._Base(options, [ 'selectFileCallback', 'exceedFileCallback', 'deleteFileCallback', 'startUploadCallback', 'uploadCompleteCallback', 'uploadErrorCallback', 'allCompleteCallback', 'changeFlashHeight' ]); /** * 开始或回复上传图片 * @public * @return {Null} */ me.upload = function(){ _flash.call('upload'); }; /** * 暂停上传图片 * @public * @return {Null} */ me.pause = function(){ _flash.call('pause'); }; me.addCustomizedParams = function(index,obj){ _flash.call('addCustomizedParams',[index,obj]); } }; /** * 操作原生对象的方法 * @namespace baidu.object */ baidu.object = baidu.object || {}; /** * 将源对象的所有属性拷贝到目标对象中 * @author erik * @name baidu.object.extend * @function * @grammar baidu.object.extend(target, source) * @param {Object} target 目标对象 * @param {Object} source 源对象 * @see baidu.array.merge * @remark * 1.目标对象中,与源对象key相同的成员将会被覆盖。
    2.源对象的prototype成员不会拷贝。 * @shortcut extend * @meta standard * * @returns {Object} 目标对象 */ baidu.extend = baidu.object.extend = function (target, source) { for (var p in source) { if (source.hasOwnProperty(p)) { target[p] = source[p]; } } return target; }; /** * 创建flash based fileUploader * @class * @grammar baidu.flash.fileUploader(options) * @param {Object} options * @config {Object} createOptions 创建flash时需要的参数,请参照baidu.swf.create文档 * @config {String} createOptions.width * @config {String} createOptions.height * @config {Number} maxNum 最大可选文件数 * @config {Function|String} selectFile * @config {Function|String} exceedMaxSize * @config {Function|String} deleteFile * @config {Function|String} uploadStart * @config {Function|String} uploadComplete * @config {Function|String} uploadError * @config {Function|String} uploadProgress */ baidu.flash.fileUploader = baidu.flash.fileUploader || function(options){ var me = this, options = options || {}; options.createOptions = baidu.extend({ wmod: 'transparent' },options.createOptions || {}); var _flash = new baidu.flash._Base(options, [ 'selectFile', 'exceedMaxSize', 'deleteFile', 'uploadStart', 'uploadComplete', 'uploadError', 'uploadProgress' ]); _flash.call('setMaxNum', options.maxNum ? [options.maxNum] : [1]); /** * 设置当鼠标移动到flash上时,是否变成手型 * @public * @param {Boolean} isCursor * @return {Null} */ me.setHandCursor = function(isCursor){ _flash.call('setHandCursor', [isCursor || false]); }; /** * 设置鼠标相应函数名 * @param {String|Function} fun */ me.setMSFunName = function(fun){ _flash.call('setMSFunName',[_flash.createFunName(fun)]); }; /** * 执行上传操作 * @param {String} url 上传的url * @param {String} fieldName 上传的表单字段名 * @param {Object} postData 键值对,上传的POST数据 * @param {Number|Array|null|-1} [index]上传的文件序列 * Int值上传该文件 * Array一次串行上传该序列文件 * -1/null上传所有文件 * @return {Null} */ me.upload = function(url, fieldName, postData, index){ if(typeof url !== 'string' || typeof fieldName !== 'string') return null; if(typeof index === 'undefined') index = -1; _flash.call('upload', [url, fieldName, postData, index]); }; /** * 取消上传操作 * @public * @param {Number|-1} index */ me.cancel = function(index){ if(typeof index === 'undefined') index = -1; _flash.call('cancel', [index]); }; /** * 删除文件 * @public * @param {Number|Array} [index] 要删除的index,不传则全部删除 * @param {Function} callBack * */ me.deleteFile = function(index, callBack){ var callBackAll = function(list){ callBack && callBack(list); }; if(typeof index === 'undefined'){ _flash.call('deleteFilesAll', [], callBackAll); return; }; if(typeof index === 'Number') index = [index]; index.sort(function(a,b){ return b-a; }); baidu.each(index, function(item){ _flash.call('deleteFileBy', item, callBackAll); }); }; /** * 添加文件类型,支持macType * @public * @param {Object|Array[Object]} type {description:String, extention:String} * @return {Null}; */ me.addFileType = function(type){ var type = type || [[]]; if(type instanceof Array) type = [type]; else type = [[type]]; _flash.call('addFileTypes', type); }; /** * 设置文件类型,支持macType * @public * @param {Object|Array[Object]} type {description:String, extention:String} * @return {Null}; */ me.setFileType = function(type){ var type = type || [[]]; if(type instanceof Array) type = [type]; else type = [[type]]; _flash.call('setFileTypes', type); }; /** * 设置可选文件的数量限制 * @public * @param {Number} num * @return {Null} */ me.setMaxNum = function(num){ _flash.call('setMaxNum', [num]); }; /** * 设置可选文件大小限制,以兆M为单位 * @public * @param {Number} num,0为无限制 * @return {Null} */ me.setMaxSize = function(num){ _flash.call('setMaxSize', [num]); }; /** * @public */ me.getFileAll = function(callBack){ _flash.call('getFileAll', [], callBack); }; /** * @public * @param {Number} index * @param {Function} [callBack] */ me.getFileByIndex = function(index, callBack){ _flash.call('getFileByIndex', [], callBack); }; /** * @public * @param {Number} index * @param {function} [callBack] */ me.getStatusByIndex = function(index, callBack){ _flash.call('getStatusByIndex', [], callBack); }; }; /** * 使用动态script标签请求服务器资源,包括由服务器端的回调和浏览器端的回调 * @namespace baidu.sio */ baidu.sio = baidu.sio || {}; /** * * @param {HTMLElement} src script节点 * @param {String} url script节点的地址 * @param {String} [charset] 编码 */ baidu.sio._createScriptTag = function(scr, url, charset){ scr.setAttribute('type', 'text/javascript'); charset && scr.setAttribute('charset', charset); scr.setAttribute('src', url); document.getElementsByTagName('head')[0].appendChild(scr); }; /** * 删除script的属性,再删除script标签,以解决修复内存泄漏的问题 * * @param {HTMLElement} src script节点 */ baidu.sio._removeScriptTag = function(scr){ if (scr.clearAttributes) { scr.clearAttributes(); } else { for (var attr in scr) { if (scr.hasOwnProperty(attr)) { delete scr[attr]; } } } if(scr && scr.parentNode){ scr.parentNode.removeChild(scr); } scr = null; }; /** * 通过script标签加载数据,加载完成由浏览器端触发回调 * @name baidu.sio.callByBrowser * @function * @grammar baidu.sio.callByBrowser(url, opt_callback, opt_options) * @param {string} url 加载数据的url * @param {Function|string} opt_callback 数据加载结束时调用的函数或函数名 * @param {Object} opt_options 其他可选项 * @config {String} [charset] script的字符集 * @config {Integer} [timeOut] 超时时间,超过这个时间将不再响应本请求,并触发onfailure函数 * @config {Function} [onfailure] timeOut设定后才生效,到达超时时间时触发本函数 * @remark * 1、与callByServer不同,callback参数只支持Function类型,不支持string。 * 2、如果请求了一个不存在的页面,callback函数在IE/opera下也会被调用,因此使用者需要在onsuccess函数中判断数据是否正确加载。 * @meta standard * @see baidu.sio.callByServer */ baidu.sio.callByBrowser = function (url, opt_callback, opt_options) { var scr = document.createElement("SCRIPT"), scriptLoaded = 0, options = opt_options || {}, charset = options['charset'], callback = opt_callback || function(){}, timeOut = options['timeOut'] || 0, timer; scr.onload = scr.onreadystatechange = function () { if (scriptLoaded) { return; } var readyState = scr.readyState; if ('undefined' == typeof readyState || readyState == "loaded" || readyState == "complete") { scriptLoaded = 1; try { callback(); clearTimeout(timer); } finally { scr.onload = scr.onreadystatechange = null; baidu.sio._removeScriptTag(scr); } } }; if( timeOut ){ timer = setTimeout(function(){ scr.onload = scr.onreadystatechange = null; baidu.sio._removeScriptTag(scr); options.onfailure && options.onfailure(); }, timeOut); } baidu.sio._createScriptTag(scr, url, charset); }; /** * 通过script标签加载数据,加载完成由服务器端触发回调 * @name baidu.sio.callByServer * @function * @grammar baidu.sio.callByServer(url, callback[, opt_options]) * @param {string} url 加载数据的url. * @param {Function|string} callback 服务器端调用的函数或函数名。如果没有指定本参数,将在URL中寻找options['queryField']做为callback的方法名. * @param {Object} opt_options 加载数据时的选项. * @config {string} [charset] script的字符集 * @config {string} [queryField] 服务器端callback请求字段名,默认为callback * @config {Integer} [timeOut] 超时时间(单位:ms),超过这个时间将不再响应本请求,并触发onfailure函数 * @config {Function} [onfailure] timeOut设定后才生效,到达超时时间时触发本函数 * @remark * 如果url中已经包含key为“options['queryField']”的query项,将会被替换成callback中参数传递或自动生成的函数名。 * @meta standard * @see baidu.sio.callByBrowser */ baidu.sio.callByServer = /**@function*/function(url, callback, opt_options) { var scr = document.createElement('SCRIPT'), prefix = 'bd__cbs__', callbackName, callbackImpl, options = opt_options || {}, charset = options['charset'], queryField = options['queryField'] || 'callback', timeOut = options['timeOut'] || 0, timer, reg = new RegExp('(\\?|&)' + queryField + '=([^&]*)'), matches; if (baidu.lang.isFunction(callback)) { callbackName = prefix + Math.floor(Math.random() * 2147483648).toString(36); window[callbackName] = getCallBack(0); } else if(baidu.lang.isString(callback)){ callbackName = callback; } else { if (matches = reg.exec(url)) { callbackName = matches[2]; } } if( timeOut ){ timer = setTimeout(getCallBack(1), timeOut); } url = url.replace(reg, '\x241' + queryField + '=' + callbackName); if (url.search(reg) < 0) { url += (url.indexOf('?') < 0 ? '?' : '&') + queryField + '=' + callbackName; } baidu.sio._createScriptTag(scr, url, charset); /* * 返回一个函数,用于立即(挂在window上)或者超时(挂在setTimeout中)时执行 */ function getCallBack(onTimeOut){ /*global callbackName, callback, scr, options;*/ return function(){ try { if( onTimeOut ){ options.onfailure && options.onfailure(); }else{ callback.apply(window, arguments); clearTimeout(timer); } window[callbackName] = null; delete window[callbackName]; } catch (exception) { } finally { baidu.sio._removeScriptTag(scr); } } } }; /** * 通过请求一个图片的方式令服务器存储一条日志 * @function * @grammar baidu.sio.log(url) * @param {string} url 要发送的地址. * @author: int08h,leeight */ baidu.sio.log = function(url) { var img = new Image(), key = 'tangram_sio_log_' + Math.floor(Math.random() * 2147483648).toString(36); window[key] = img; img.onload = img.onerror = img.onabort = function() { img.onload = img.onerror = img.onabort = null; window[key] = null; img = null; }; img.src = url; }; /* * Tangram * Copyright 2009 Baidu Inc. All rights reserved. * * path: baidu/json.js * author: erik * version: 1.1.0 * date: 2009/12/02 */ /** * 操作json对象的方法 * @namespace baidu.json */ baidu.json = baidu.json || {}; /* * Tangram * Copyright 2009 Baidu Inc. All rights reserved. * * path: baidu/json/parse.js * author: erik, berg * version: 1.2 * date: 2009/11/23 */ /** * 将字符串解析成json对象。注:不会自动祛除空格 * @name baidu.json.parse * @function * @grammar baidu.json.parse(data) * @param {string} source 需要解析的字符串 * @remark * 该方法的实现与ecma-262第五版中规定的JSON.parse不同,暂时只支持传入一个参数。后续会进行功能丰富。 * @meta standard * @see baidu.json.stringify,baidu.json.decode * * @returns {JSON} 解析结果json对象 */ baidu.json.parse = function (data) { //2010/12/09:更新至不使用原生parse,不检测用户输入是否正确 return (new Function("return (" + data + ")"))(); }; /* * Tangram * Copyright 2009 Baidu Inc. All rights reserved. * * path: baidu/json/decode.js * author: erik, cat * version: 1.3.4 * date: 2010/12/23 */ /** * 将字符串解析成json对象,为过时接口,今后会被baidu.json.parse代替 * @name baidu.json.decode * @function * @grammar baidu.json.decode(source) * @param {string} source 需要解析的字符串 * @meta out * @see baidu.json.encode,baidu.json.parse * * @returns {JSON} 解析结果json对象 */ baidu.json.decode = baidu.json.parse; /* * Tangram * Copyright 2009 Baidu Inc. All rights reserved. * * path: baidu/json/stringify.js * author: erik * version: 1.1.0 * date: 2010/01/11 */ /** * 将json对象序列化 * @name baidu.json.stringify * @function * @grammar baidu.json.stringify(value) * @param {JSON} value 需要序列化的json对象 * @remark * 该方法的实现与ecma-262第五版中规定的JSON.stringify不同,暂时只支持传入一个参数。后续会进行功能丰富。 * @meta standard * @see baidu.json.parse,baidu.json.encode * * @returns {string} 序列化后的字符串 */ baidu.json.stringify = (function () { /** * 字符串处理时需要转义的字符表 * @private */ var escapeMap = { "\b": '\\b', "\t": '\\t', "\n": '\\n', "\f": '\\f', "\r": '\\r', '"' : '\\"', "\\": '\\\\' }; /** * 字符串序列化 * @private */ function encodeString(source) { if (/["\\\x00-\x1f]/.test(source)) { source = source.replace( /["\\\x00-\x1f]/g, function (match) { var c = escapeMap[match]; if (c) { return c; } c = match.charCodeAt(); return "\\u00" + Math.floor(c / 16).toString(16) + (c % 16).toString(16); }); } return '"' + source + '"'; } /** * 数组序列化 * @private */ function encodeArray(source) { var result = ["["], l = source.length, preComma, i, item; for (i = 0; i < l; i++) { item = source[i]; switch (typeof item) { case "undefined": case "function": case "unknown": break; default: if(preComma) { result.push(','); } result.push(baidu.json.stringify(item)); preComma = 1; } } result.push("]"); return result.join(""); } /** * 处理日期序列化时的补零 * @private */ function pad(source) { return source < 10 ? '0' + source : source; } /** * 日期序列化 * @private */ function encodeDate(source){ return '"' + source.getFullYear() + "-" + pad(source.getMonth() + 1) + "-" + pad(source.getDate()) + "T" + pad(source.getHours()) + ":" + pad(source.getMinutes()) + ":" + pad(source.getSeconds()) + '"'; } return function (value) { switch (typeof value) { case 'undefined': return 'undefined'; case 'number': return isFinite(value) ? String(value) : "null"; case 'string': return encodeString(value); case 'boolean': return String(value); default: if (value === null) { return 'null'; } else if (value instanceof Array) { return encodeArray(value); } else if (value instanceof Date) { return encodeDate(value); } else { var result = ['{'], encode = baidu.json.stringify, preComma, item; for (var key in value) { if (Object.prototype.hasOwnProperty.call(value, key)) { item = value[key]; switch (typeof item) { case 'undefined': case 'unknown': case 'function': break; default: if (preComma) { result.push(','); } preComma = 1; result.push(encode(key) + ':' + encode(item)); } } } result.push('}'); return result.join(''); } } }; })(); /* * Tangram * Copyright 2009 Baidu Inc. All rights reserved. * * path: baidu/json/encode.js * author: erik, cat * version: 1.3.4 * date: 2010/12/23 */ /** * 将json对象序列化,为过时接口,今后会被baidu.json.stringify代替 * @name baidu.json.encode * @function * @grammar baidu.json.encode(value) * @param {JSON} value 需要序列化的json对象 * @meta out * @see baidu.json.decode,baidu.json.stringify * * @returns {string} 序列化后的字符串 */ baidu.json.encode = baidu.json.stringify; ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/wordimage/wordimage.html ================================================
    :
    ================================================ FILE: yshop-drink-vue3/public/UEditor/dialogs/wordimage/wordimage.js ================================================ /** * Created by JetBrains PhpStorm. * User: taoqili * Date: 12-1-30 * Time: 下午12:50 * To change this template use File | Settings | File Templates. */ var wordImage = {}; //(function(){ var g = baidu.g, flashObj,flashContainer; wordImage.init = function(opt, callbacks) { showLocalPath("localPath"); //createCopyButton("clipboard","localPath"); createFlashUploader(opt, callbacks); addUploadListener(); addOkListener(); }; function hideFlash(){ flashObj = null; flashContainer.innerHTML = ""; } function addOkListener() { dialog.onok = function() { if (!imageUrls.length) return; var urlPrefix = editor.getOpt('imageUrlPrefix'), images = domUtils.getElementsByTagName(editor.document,"img"); editor.fireEvent('saveScene'); for (var i = 0,img; img = images[i++];) { var src = img.getAttribute("word_img"); if (!src) continue; for (var j = 0,url; url = imageUrls[j++];) { if (src.indexOf(url.original.replace(" ","")) != -1) { img.src = urlPrefix + url.url; img.setAttribute("_src", urlPrefix + url.url); //同时修改"_src"属性 img.setAttribute("title",url.title); domUtils.removeAttributes(img, ["word_img","style","width","height"]); editor.fireEvent("selectionchange"); break; } } } editor.fireEvent('saveScene'); hideFlash(); }; dialog.oncancel = function(){ hideFlash(); } } /** * 绑定开始上传事件 */ function addUploadListener() { g("upload").onclick = function () { flashObj.upload(); this.style.display = "none"; }; } function showLocalPath(id) { //单张编辑 var img = editor.selection.getRange().getClosedNode(); var images = editor.execCommand('wordimage'); if(images.length==1 || img && img.tagName == 'IMG'){ g(id).value = images[0]; return; } var path = images[0]; var leftSlashIndex = path.lastIndexOf("/")||0, //不同版本的doc和浏览器都可能影响到这个符号,故直接判断两种 rightSlashIndex = path.lastIndexOf("\\")||0, separater = leftSlashIndex > rightSlashIndex ? "/":"\\" ; path = path.substring(0, path.lastIndexOf(separater)+1); g(id).value = path; } function createFlashUploader(opt, callbacks) { //由于lang.flashI18n是静态属性,不可以直接进行修改,否则会影响到后续内容 var i18n = utils.extend({},lang.flashI18n); //处理图片资源地址的编码,补全等问题 for(var i in i18n){ if(!(i in {"lang":1,"uploadingTF":1,"imageTF":1,"textEncoding":1}) && i18n[i]){ i18n[i] = encodeURIComponent(editor.options.langPath + editor.options.lang + "/images/" + i18n[i]); } } opt = utils.extend(opt,i18n,false); var option = { createOptions:{ id:'flash', url:opt.flashUrl, width:opt.width, height:opt.height, errorMessage:lang.flashError, wmode:browser.safari ? 'transparent' : 'window', ver:'10.0.0', vars:opt, container:opt.container } }; option = extendProperty(callbacks, option); flashObj = new baidu.flash.imageUploader(option); flashContainer = $G(opt.container); } function extendProperty(fromObj, toObj) { for (var i in fromObj) { if (!toObj[i]) { toObj[i] = fromObj[i]; } } return toObj; } //})(); function getPasteData(id) { baidu.g("msg").innerHTML = lang.copySuccess + "
    "; setTimeout(function() { baidu.g("msg").innerHTML = ""; }, 5000); return baidu.g(id).value; } function createCopyButton(id, dataFrom) { baidu.swf.create({ id:"copyFlash", url:"fClipboard_ueditor.swf", width:"58", height:"25", errorMessage:"", bgColor:"#CBCBCB", wmode:"transparent", ver:"10.0.0", vars:{ tid:dataFrom } }, id ); var clipboard = baidu.swf.getMovie("copyFlash"); var clipinterval = setInterval(function() { if (clipboard && clipboard.flashInit) { clearInterval(clipinterval); clipboard.setHandCursor(true); clipboard.setContentFuncName("getPasteData"); //clipboard.setMEFuncName("mouseEventHandler"); } }, 500); } createCopyButton("clipboard", "localPath"); ================================================ FILE: yshop-drink-vue3/public/UEditor/index.html ================================================ 完整demo

    完整demo

    ================================================ FILE: yshop-drink-vue3/public/UEditor/lang/en/en.js ================================================ /** * Created with JetBrains PhpStorm. * User: taoqili * Date: 12-6-12 * Time: 下午6:57 * To change this template use File | Settings | File Templates. */ UE.I18N['en'] = { 'labelMap': { 'anchor': 'Anchor', 'undo': 'Undo', 'redo': 'Redo', 'bold': 'Bold', 'indent': 'Indent', 'snapscreen': 'SnapScreen', 'italic': 'Italic', 'underline': 'Underline', 'strikethrough': 'Strikethrough', 'subscript': 'SubScript', 'fontborder': 'text border', 'superscript': 'SuperScript', 'formatmatch': 'Format Match', 'source': 'Source', 'blockquote': 'BlockQuote', 'pasteplain': 'PastePlain', 'selectall': 'SelectAll', 'print': 'Print', 'preview': 'Preview', 'horizontal': 'Horizontal', 'removeformat': 'RemoveFormat', 'time': 'Time', 'date': 'Date', 'unlink': 'Unlink', 'insertrow': 'InsertRow', 'insertcol': 'InsertCol', 'mergeright': 'MergeRight', 'mergedown': 'MergeDown', 'deleterow': 'DeleteRow', 'deletecol': 'DeleteCol', 'splittorows': 'SplitToRows', 'insertcode': 'insert code', 'splittocols': 'SplitToCols', 'splittocells': 'SplitToCells', 'deletecaption': 'DeleteCaption', 'inserttitle': 'InsertTitle', 'mergecells': 'MergeCells', 'deletetable': 'DeleteTable', 'cleardoc': 'Clear', 'insertparagraphbeforetable': 'InsertParagraphBeforeTable', 'fontfamily': 'FontFamily', 'fontsize': 'FontSize', 'paragraph': 'Paragraph', 'simpleupload': 'Single Image', 'insertimage': 'Multi Image', 'edittable': 'Edit Table', 'edittd': 'Edit Td', 'link': 'Link', 'emotion': 'Emotion', 'spechars': 'Spechars', 'searchreplace': 'SearchReplace', 'map': 'BaiduMap', 'gmap': 'GoogleMap', 'insertvideo': 'Video', 'help': 'Help', 'justifyleft': 'JustifyLeft', 'justifyright': 'JustifyRight', 'justifycenter': 'JustifyCenter', 'justifyjustify': 'Justify', 'forecolor': 'FontColor', 'backcolor': 'BackColor', 'insertorderedlist': 'OL', 'insertunorderedlist': 'UL', 'fullscreen': 'FullScreen', 'directionalityltr': 'EnterFromLeft', 'directionalityrtl': 'EnterFromRight', 'rowspacingtop': 'RowSpacingTop', 'rowspacingbottom': 'RowSpacingBottom', 'pagebreak': 'PageBreak', 'insertframe': 'Iframe', 'imagenone': 'Default', 'imageleft': 'ImageLeft', 'imageright': 'ImageRight', 'attachment': 'Attachment', 'imagecenter': 'ImageCenter', 'wordimage': 'WordImage', 'lineheight': 'LineHeight', 'edittip': 'EditTip', 'customstyle': 'CustomStyle', 'scrawl': 'Scrawl', 'autotypeset': 'AutoTypeset', 'webapp': 'WebAPP', 'touppercase': 'UpperCase', 'tolowercase': 'LowerCase', 'template': 'Template', 'background': 'Background', 'inserttable': 'InsertTable', 'music': 'Music', 'charts': 'charts', 'drafts': 'Load from Drafts' }, 'insertorderedlist': { 'num': '1,2,3...', 'num1': '1),2),3)...', 'num2': '(1),(2),(3)...', 'cn': '一,二,三....', 'cn1': '一),二),三)....', 'cn2': '(一),(二),(三)....', 'decimal': '1,2,3...', 'lower-alpha': 'a,b,c...', 'lower-roman': 'i,ii,iii...', 'upper-alpha': 'A,B,C...', 'upper-roman': 'I,II,III...' }, 'insertunorderedlist': { 'circle': '○ Circle', 'disc': '● Circle dot', 'square': '■ Rectangle ', 'dash': '- Dash', 'dot': '。dot' }, 'paragraph': { 'p': 'Paragraph', 'h1': 'Title 1', 'h2': 'Title 2', 'h3': 'Title 3', 'h4': 'Title 4', 'h5': 'Title 5', 'h6': 'Title 6' }, 'fontfamily': { 'songti': 'Sim Sun', 'kaiti': 'Sim Kai', 'heiti': 'Sim Hei', 'lishu': 'Sim Li', 'yahei': 'Microsoft YaHei', 'andaleMono': 'Andale Mono', 'arial': 'Arial', 'arialBlack': 'Arial Black', 'comicSansMs': 'Comic Sans MS', 'impact': 'Impact', 'timesNewRoman': 'Times New Roman' }, 'customstyle': { 'tc': 'Title center', 'tl': 'Title left', 'im': 'Important', 'hi': 'Highlight' }, 'autoupload': { 'exceedSizeError': 'File Size Exceed', 'exceedTypeError': 'File Type Not Allow', 'jsonEncodeError': 'Server Return Format Error', 'loading': 'loading...', 'loadError': 'load error', 'errorLoadConfig': 'Server config not loaded, upload can not work.' }, 'simpleupload': { 'exceedSizeError': 'File Size Exceed', 'exceedTypeError': 'File Type Not Allow', 'jsonEncodeError': 'Server Return Format Error', 'loading': 'loading...', 'loadError': 'load error', 'errorLoadConfig': 'Server config not loaded, upload can not work.' }, 'elementPathTip': 'Path', 'wordCountTip': 'Word Count', 'wordCountMsg': '{#count} characters entered,{#leave} left. ', 'wordOverFlowMsg': 'The number of characters has exceeded allowable maximum values, the server may refuse to save!', 'ok': 'OK', 'cancel': 'Cancel', 'closeDialog': 'closeDialog', 'tableDrag': 'You must import the file uiUtils.js before drag! ', 'autofloatMsg': 'The plugin AutoFloat depends on EditorUI!', 'loadconfigError': 'Get server config error.', 'loadconfigFormatError': 'Server config format error.', 'loadconfigHttpError': 'Get server config http error.', 'snapScreen_plugin': { 'browserMsg': 'Only IE supported!', 'callBackErrorMsg': 'The callback data is wrong,please check the config!', 'uploadErrorMsg': 'Upload error,please check your server environment! ' }, 'insertcode': { 'as3': 'ActionScript 3', 'bash': 'Bash/Shell', 'cpp': 'C/C++', 'css': 'CSS', 'cf': 'ColdFusion', 'c#': 'C#', 'delphi': 'Delphi', 'diff': 'Diff', 'erlang': 'Erlang', 'groovy': 'Groovy', 'html': 'HTML', 'java': 'Java', 'jfx': 'JavaFX', 'js': 'JavaScript', 'pl': 'Perl', 'php': 'PHP', 'plain': 'Plain Text', 'ps': 'PowerShell', 'python': 'Python', 'ruby': 'Ruby', 'scala': 'Scala', 'sql': 'SQL', 'vb': 'Visual Basic', 'xml': 'XML' }, 'confirmClear': 'Do you confirm to clear the Document?', 'contextMenu': { 'delete': 'Delete', 'selectall': 'Select all', 'deletecode': 'Delete Code', 'cleardoc': 'Clear Document', 'confirmclear': 'Do you confirm to clear the Document?', 'unlink': 'Unlink', 'paragraph': 'Paragraph', 'edittable': 'Table property', 'aligncell': 'Align cell', 'aligntable': 'Table alignment', 'tableleft': 'Left float', 'tablecenter': 'Center', 'tableright': 'Right float', 'aligntd': 'Cell alignment', 'edittd': 'Cell property', 'setbordervisible': 'set table edge visible', 'table': 'Table', 'justifyleft': 'Justify Left', 'justifyright': 'Justify Right', 'justifycenter': 'Justify Center', 'justifyjustify': 'Default', 'deletetable': 'Delete table', 'insertparagraphbefore': 'InsertedBeforeLine', 'insertparagraphafter': 'InsertedAfterLine', 'inserttable': 'Insert table', 'insertcaption': 'Insert caption', 'deletecaption': 'Delete Caption', 'inserttitle': 'Insert Title', 'deletetitle': 'Delete Title', 'inserttitlecol': 'Insert Title Col', 'deletetitlecol': 'Delete Title Col', 'averageDiseRow': 'AverageDise Row', 'averageDisCol': 'AverageDis Col', 'deleterow': 'Delete row', 'deletecol': 'Delete col', 'insertrow': 'Insert row', 'insertcol': 'Insert col', 'insertrownext': 'Insert Row Next', 'insertcolnext': 'Insert Col Next', 'mergeright': 'Merge right', 'mergeleft': 'Merge left', 'mergedown': 'Merge down', 'mergecells': 'Merge cells', 'splittocells': 'Split to cells', 'splittocols': 'Split to Cols', 'splittorows': 'Split to Rows', 'tablesort': 'Table sorting', 'enablesort': 'Sorting Enable', 'disablesort': 'Sorting Disable', 'reversecurrent': 'Reverse current', 'orderbyasc': 'Order By ASCII', 'reversebyasc': 'Reverse By ASCII', 'orderbynum': 'Order By Num', 'reversebynum': 'Reverse By Num', 'borderbk': 'Border shading', 'setcolor': 'interlaced color', 'unsetcolor': 'Cancel interlacedcolor', 'setbackground': 'Background interlaced', 'unsetbackground': 'Cancel Bk interlaced', 'redandblue': 'Blue and red', 'threecolorgradient': 'Three-color gradient', 'copy': 'Copy(Ctrl + c)', 'copymsg': "Browser does not support. Please use 'Ctrl + c' instead!", 'paste': 'Paste(Ctrl + v)', 'pastemsg': "Browser does not support. Please use 'Ctrl + v' instead!" }, 'copymsg': "Browser does not support. Please use 'Ctrl + c' instead!", 'pastemsg': "Browser does not support. Please use 'Ctrl + v' instead!", 'anthorMsg': 'Link', 'clearColor': 'Clear', 'standardColor': 'Standard color', 'themeColor': 'Theme color', 'property': 'Property', 'default': 'Default', 'modify': 'Modify', 'justifyleft': 'Justify Left', 'justifyright': 'Justify Right', 'justifycenter': 'Justify Center', 'justify': 'Default', 'clear': 'Clear', 'anchorMsg': 'Anchor', 'delete': 'Delete', 'clickToUpload': 'Click to upload', 'unset': 'Language hasn\'t been set!', 't_row': 'row', 't_col': 'col', 'pasteOpt': 'Paste Option', 'pasteSourceFormat': 'Keep Source Formatting', 'tagFormat': 'Keep tag', 'pasteTextFormat': 'Keep Text only', 'more': 'More', 'autoTypeSet': { 'mergeLine': 'Merge empty line', 'delLine': 'Del empty line', 'removeFormat': 'Remove format', 'indent': 'Indent', 'alignment': 'Alignment', 'imageFloat': 'Image float', 'removeFontsize': 'Remove font size', 'removeFontFamily': 'Remove fontFamily', 'removeHtml': 'Remove redundant HTML code', 'pasteFilter': 'Paste filter', 'run': 'Done', 'symbol': 'Symbol Conversion', 'bdc2sb': 'Full-width to Half-width', 'tobdc': 'Half-width to Full-width' }, 'background': { 'static': { 'lang_background_normal': 'Normal', 'lang_background_local': 'Online', 'lang_background_set': 'Background Set', 'lang_background_none': 'No Background', 'lang_background_colored': 'Colored Background', 'lang_background_color': 'Color Set', 'lang_background_netimg': 'Net-Image', 'lang_background_align': 'Align Type', 'lang_background_position': 'Position', 'repeatType': { 'options': ['Center', 'Repeat-x', 'Repeat-y', 'Tile', 'Custom'] } }, 'noUploadImage': 'No pictures has been uploaded!', 'toggleSelect': 'Change the active state by click!\n Image Size: ' }, //= ==============dialog i18N======================= 'insertimage': { 'static': { 'lang_tab_remote': 'Insert', 'lang_tab_upload': 'Local', 'lang_tab_online': 'Manager', 'lang_tab_search': 'Search', 'lang_input_url': 'Address:', 'lang_input_size': 'Size:', 'lang_input_width': 'Width', 'lang_input_height': 'Height', 'lang_input_border': 'Border:', 'lang_input_vhspace': 'Margins:', 'lang_input_title': 'Title:', 'lang_input_align': 'Image Float Style:', 'lang_imgLoading': 'Loading...', 'lang_start_upload': 'Start Upload', 'lock': { 'title': 'Lock rate' }, 'searchType': { 'title': 'ImageType', 'options': ['News', 'Wallpaper', 'emotions', 'photo'] }, 'searchTxt': { 'value': 'Enter the search keyword!' }, 'searchBtn': { 'value': 'Search' }, 'searchReset': { 'value': 'Clear' }, 'noneAlign': { 'title': 'None Float' }, 'leftAlign': { 'title': 'Left Float' }, 'rightAlign': { 'title': 'Right Float' }, 'centerAlign': { 'title': 'Center In A Line' } }, 'uploadSelectFile': 'Select File', 'uploadAddFile': 'Add File', 'uploadStart': 'Start Upload', 'uploadPause': 'Pause Upload', 'uploadContinue': 'Continue Upload', 'uploadRetry': 'Retry Upload', 'uploadDelete': 'Delete', 'uploadTurnLeft': 'Turn Left', 'uploadTurnRight': 'Turn Right', 'uploadPreview': 'Doing Preview', 'uploadNoPreview': 'Can Not Preview', 'updateStatusReady': 'Selected _ pictures, total _KB.', 'updateStatusConfirm': '_ uploaded successfully and _ upload failed', 'updateStatusFinish': 'Total _ pictures (_KB), _ uploaded successfully', 'updateStatusError': ' and _ upload failed', 'errorNotSupport': 'WebUploader does not support the browser you are using. Please upgrade your browser or flash player', 'errorLoadConfig': 'Server config not loaded, upload can not work.', 'errorExceedSize': 'File Size Exceed', 'errorFileType': 'File Type Not Allow', 'errorInterrupt': 'File Upload Interrupted', 'errorUploadRetry': 'Upload Error, Please Retry.', 'errorHttp': 'Http Error', 'errorServerUpload': 'Server Result Error.', 'remoteLockError': 'Cannot Lock the Proportion between width and height', 'numError': 'Please enter the correct Num. e.g 123,400', 'imageUrlError': 'The image format may be wrong!', 'imageLoadError': 'Error,please check the network or URL!', 'searchRemind': 'Enter the search keyword!', 'searchLoading': 'Image is loading,please wait...', 'searchRetry': " Sorry,can't find the image,please try again!" }, 'attachment': { 'static': { 'lang_tab_upload': 'Upload', 'lang_tab_online': 'Online', 'lang_start_upload': 'Start upload', 'lang_drop_remind': 'You can drop files here, a single maximum of 300 files' }, 'uploadSelectFile': 'Select File', 'uploadAddFile': 'Add File', 'uploadStart': 'Start Upload', 'uploadPause': 'Pause Upload', 'uploadContinue': 'Continue Upload', 'uploadRetry': 'Retry Upload', 'uploadDelete': 'Delete', 'uploadTurnLeft': 'Turn Left', 'uploadTurnRight': 'Turn Right', 'uploadPreview': 'Doing Preview', 'updateStatusReady': 'Selected _ files, total _KB.', 'updateStatusConfirm': '_ uploaded successfully and _ upload failed', 'updateStatusFinish': 'Total _ files (_KB), _ uploaded successfully', 'updateStatusError': ' and _ upload failed', 'errorNotSupport': 'WebUploader does not support the browser you are using. Please upgrade your browser or flash player', 'errorLoadConfig': 'Server config not loaded, upload can not work.', 'errorExceedSize': 'File Size Exceed', 'errorFileType': 'File Type Not Allow', 'errorInterrupt': 'File Upload Interrupted', 'errorUploadRetry': 'Upload Error, Please Retry.', 'errorHttp': 'Http Error', 'errorServerUpload': 'Server Result Error.' }, 'insertvideo': { 'static': { 'lang_tab_insertV': 'Video', 'lang_tab_searchV': 'Search', 'lang_tab_uploadV': 'Upload', 'lang_video_url': ' URL ', 'lang_video_size': 'Video Size', 'lang_videoW': 'Width', 'lang_videoH': 'Height', 'lang_alignment': 'Alignment', 'videoSearchTxt': { 'value': 'Enter the search keyword!' }, 'videoType': { 'options': ['All', 'Hot', 'Entertainment', 'Funny', 'Sports', 'Science', 'variety'] }, 'videoSearchBtn': { 'value': 'Search in Baidu' }, 'videoSearchReset': { 'value': 'Clear result' }, 'lang_input_fileStatus': ' No file uploaded!', 'startUpload': { 'style': 'background:url(upload.png) no-repeat;' }, 'lang_upload_size': 'Video Size', 'lang_upload_width': 'Width', 'lang_upload_height': 'Height', 'lang_upload_alignment': 'Alignment', 'lang_format_advice': 'Recommends mp4 format.' }, 'numError': 'Please enter the correct Num. e.g 123,400', 'floatLeft': 'Float left', 'floatRight': 'Float right', 'default': 'Default', 'block': 'Display in block', 'urlError': 'The video url format may be wrong!', 'loading': '  The video is loading, please wait…', 'clickToSelect': 'Click to select', 'goToSource': 'Visit source video ', 'noVideo': "    Sorry,can't find the video,please try again!", 'browseFiles': 'Open files', 'uploadSuccess': 'Upload Successful!', 'delSuccessFile': 'Remove from the success of the queue', 'delFailSaveFile': 'Remove the save failed file', 'statusPrompt': ' file(s) uploaded! ', 'flashVersionError': 'The current Flash version is too low, please update FlashPlayer,then try again!', 'flashLoadingError': 'The Flash failed loading! Please check the path or network state', 'fileUploadReady': 'Wait for uploading...', 'delUploadQueue': 'Remove from the uploading queue ', 'limitPrompt1': 'Can not choose more than single', 'limitPrompt2': 'file(s)!Please choose again!', 'delFailFile': 'Remove failure file', 'fileSizeLimit': 'File size exceeds the limit!', 'emptyFile': 'Can not upload an empty file!', 'fileTypeError': 'File type error!', 'unknownError': 'Unknown error!', 'fileUploading': 'Uploading,please wait...', 'cancelUpload': 'Cancel upload', 'netError': 'Network error', 'failUpload': 'Upload failed', 'serverIOError': 'Server IO error!', 'noAuthority': 'No Permission!', 'fileNumLimit': 'Upload limit to the number', 'failCheck': 'Authentication fails, the upload is skipped!', 'fileCanceling': 'Cancel, please wait...', 'stopUploading': 'Upload has stopped...', 'uploadSelectFile': 'Select File', 'uploadAddFile': 'Add File', 'uploadStart': 'Start Upload', 'uploadPause': 'Pause Upload', 'uploadContinue': 'Continue Upload', 'uploadRetry': 'Retry Upload', 'uploadDelete': 'Delete', 'uploadTurnLeft': 'Turn Left', 'uploadTurnRight': 'Turn Right', 'uploadPreview': 'Doing Preview', 'updateStatusReady': 'Selected _ files, total _KB.', 'updateStatusConfirm': '_ uploaded successfully and _ upload failed', 'updateStatusFinish': 'Total _ files (_KB), _ uploaded successfully', 'updateStatusError': ' and _ upload failed', 'errorNotSupport': 'WebUploader does not support the browser you are using. Please upgrade your browser or flash player', 'errorLoadConfig': 'Server config not loaded, upload can not work.', 'errorExceedSize': 'File Size Exceed', 'errorFileType': 'File Type Not Allow', 'errorInterrupt': 'File Upload Interrupted', 'errorUploadRetry': 'Upload Error, Please Retry.', 'errorHttp': 'Http Error', 'errorServerUpload': 'Server Result Error.' }, 'webapp': { 'tip1': 'This function provided by Baidu APP,please apply for baidu APPKey webmaster first!', 'tip2': 'And then open the file ueditor.config.js to set it! ', 'applyFor': 'APPLY FOR', 'anthorApi': 'Baidu API' }, 'template': { 'static': { 'lang_template_bkcolor': 'Background Color', 'lang_template_clear': 'Keep Content', 'lang_template_select': 'Select Template' }, 'blank': 'Blank', 'blog': 'Blog', 'resume': 'Resume', 'richText': 'Rich Text', 'scrPapers': 'Scientific Papers' }, scrawl: { 'static': { 'lang_input_previousStep': 'Previous', 'lang_input_nextsStep': 'Next', 'lang_input_clear': 'Clear', 'lang_input_addPic': 'AddImage', 'lang_input_ScalePic': 'ScaleImage', 'lang_input_removePic': 'RemoveImage', 'J_imgTxt': { title: 'Add background image' } }, 'noScarwl': 'No paint, a white paper...', 'scrawlUpLoading': 'Image is uploading, please wait...', 'continueBtn': 'Try again', 'imageError': 'Image failed to load!', 'backgroundUploading': 'Image is uploading,please wait...' }, 'music': { 'static': { 'lang_input_tips': 'Input singer/song/album, search you interested in music!', 'J_searchBtn': { value: 'Search songs' } }, 'emptyTxt': 'Not search to the relevant music results, please change a keyword try.', 'chapter': 'Songs', 'singer': 'Singer', 'special': 'Album', 'listenTest': 'Audition' }, anchor: { 'static': { 'lang_input_anchorName': 'Anchor Name:' } }, 'charts': { 'static': { 'lang_data_source': 'Data source:', 'lang_chart_format': 'Chart format:', 'lang_data_align': 'Align', 'lang_chart_align_same': 'Consistent with the X-axis Y-axis', 'lang_chart_align_reverse': 'X-axis Y-axis opposite', 'lang_chart_title': 'Title', 'lang_chart_main_title': 'main title:', 'lang_chart_sub_title': 'sub title:', 'lang_chart_x_title': 'X-axis title:', 'lang_chart_y_title': 'Y-axis title:', 'lang_chart_tip': 'Prompt', 'lang_cahrt_tip_prefix': 'prefix:', 'lang_cahrt_tip_description': '仅饼图有效, 当鼠标移动到饼图中相应的块上时,提示框内的文字的前缀', 'lang_chart_data_unit': 'Unit', 'lang_chart_data_unit_title': 'unit:', 'lang_chart_data_unit_description': '显示在每个数据点上的数据的单位, 比如: 温度的单位 ℃', 'lang_chart_type': 'Chart type:', 'lang_prev_btn': 'Previous', 'lang_next_btn': 'Next' } }, emotion: { 'static': { 'lang_input_choice': 'Choice', 'lang_input_Tuzki': 'Tuzki', 'lang_input_lvdouwa': 'LvDouWa', 'lang_input_BOBO': 'BOBO', 'lang_input_babyCat': 'BabyCat', 'lang_input_bubble': 'Bubble', 'lang_input_youa': 'YouA' } }, gmap: { 'static': { 'lang_input_address': 'Address:', 'lang_input_search': 'Search', 'address': { value: 'Beijing' } }, searchError: 'Unable to locate the address!' }, help: { 'static': { 'lang_input_about': 'About', 'lang_input_shortcuts': 'Shortcuts', 'lang_input_introduction': 'UEditor is developed by Baidu Co.ltd. It is lightweight, customizable , focusing on user experience and etc. , UEditor is based on open source BSD license , allowing free use and redistribution.', 'lang_Txt_shortcuts': 'Shortcuts', 'lang_Txt_func': 'Function', 'lang_Txt_bold': 'Bold', 'lang_Txt_copy': 'Copy', 'lang_Txt_cut': 'Cut', 'lang_Txt_Paste': 'Paste', 'lang_Txt_undo': 'Undo', 'lang_Txt_redo': 'Redo', 'lang_Txt_italic': 'Italic', 'lang_Txt_underline': 'Underline', 'lang_Txt_selectAll': 'Select All', 'lang_Txt_visualEnter': 'Submit', 'lang_Txt_fullscreen': 'Fullscreen' } }, insertframe: { 'static': { 'lang_input_address': 'Address:', 'lang_input_width': 'Width:', 'lang_input_height': 'height:', 'lang_input_isScroll': 'Enable scrollbars:', 'lang_input_frameborder': 'Show frame border:', 'lang_input_alignMode': 'Alignment:', 'align': { title: 'Alignment', options: ['Default', 'Left', 'Right', 'Center'] } }, 'enterAddress': 'Please enter an address!' }, link: { 'static': { 'lang_input_text': 'Text:', 'lang_input_url': 'URL:', 'lang_input_title': 'Title:', 'lang_input_target': 'open in new window:' }, 'validLink': 'Supports only effective when a link is selected', 'httpPrompt': 'The hyperlink you enter should start with "http|https|ftp://"!' }, map: { 'static': { lang_city: 'City', lang_address: 'Address', city: { value: 'Beijing' }, lang_search: 'Search', lang_dynamicmap: 'Dynamic map' }, cityMsg: 'Please enter the city name!', errorMsg: "Can't find the place!" }, searchreplace: { 'static': { lang_tab_search: 'Search', lang_tab_replace: 'Replace', lang_search1: 'Search', lang_search2: 'Search', lang_replace: 'Replace', lang_searchReg: 'Support regular expression ,which starts and ends with a slash ,for example "/expression/"', lang_searchReg1: 'Support regular expression ,which starts and ends with a slash ,for example "/expression/"', lang_case_sensitive1: 'Case sense', lang_case_sensitive2: 'Case sense', nextFindBtn: { value: 'Next' }, preFindBtn: { value: 'Preview' }, nextReplaceBtn: { value: 'Next' }, preReplaceBtn: { value: 'Preview' }, repalceBtn: { value: 'Replace' }, repalceAllBtn: { value: 'Replace all' } }, getEnd: 'Has the search to the bottom!', getStart: 'Has the search to the top!', countMsg: 'Altogether replaced {#count} character(s)!' }, snapscreen: { 'static': { lang_showMsg: 'You should install the UEditor screenshots program first!', lang_download: 'Download!', lang_step1: 'Step1:Download the program and then run it', lang_step2: 'Step2:After complete install,try to click the button again' } }, spechars: { 'static': {}, tsfh: 'Special', lmsz: 'Roman', szfh: 'Numeral', rwfh: 'Japanese', xlzm: 'The Greek', ewzm: 'Russian', pyzm: 'Phonetic', yyyb: 'English', zyzf: 'Others' }, 'edittable': { 'static': { 'lang_tableStyle': 'Table style', 'lang_insertCaption': 'Add table header row', 'lang_insertTitle': 'Add table title row', 'lang_insertTitleCol': 'Add table title col', 'lang_tableSize': 'Automatically adjust table size', 'lang_autoSizeContent': 'Adaptive by form text', 'lang_orderbycontent': 'Table of contents sortable', 'lang_autoSizePage': 'Page width adaptive', 'lang_example': 'Example', 'lang_borderStyle': 'Table Border', 'lang_color': 'Color:' }, captionName: 'Caption', titleName: 'Title', cellsName: 'text', errorMsg: 'There are merged cells, can not sort.' }, 'edittip': { 'static': { lang_delRow: 'Delete entire row', lang_delCol: 'Delete entire col' } }, 'edittd': { 'static': { lang_tdBkColor: 'Background Color:' } }, 'formula': { 'static': { } }, wordimage: { 'static': { lang_resave: 'The re-save step', uploadBtn: { src: 'upload.png', alt: 'Upload' }, clipboard: { style: 'background: url(copy.png) -153px -1px no-repeat;' }, lang_step: ' 1. Click top button to copy the url and then open the dialog to paste it. 2. Open after choose photos uploaded process.' }, fileType: 'Image', flashError: 'Flash initialization failed!', netError: 'Network error! Please try again!', copySuccess: 'URL has been copied!', 'flashI18n': { lang: encodeURI('{"UploadingState":"totalNum: ${a},uploadComplete: ${b}", "BeforeUpload":"waitingNum: ${a}", "ExceedSize":"Size exceed${a}", "ErrorInPreview":"Preview failed", "DefaultDescription":"Description", "LoadingImage":"Loading..."}'), uploadingTF: encodeURI('{"font":"Arial", "size":12, "color":"0x000", "bold":"true", "italic":"false", "underline":"false"}'), imageTF: encodeURI('{"font":"Arial", "size":11, "color":"red", "bold":"false", "italic":"false", "underline":"false"}'), textEncoding: 'utf-8', addImageSkinURL: 'addImage.png', allDeleteBtnUpSkinURL: 'allDeleteBtnUpSkin.png', allDeleteBtnHoverSkinURL: 'allDeleteBtnHoverSkin.png', rotateLeftBtnEnableSkinURL: 'rotateLeftEnable.png', rotateLeftBtnDisableSkinURL: 'rotateLeftDisable.png', rotateRightBtnEnableSkinURL: 'rotateRightEnable.png', rotateRightBtnDisableSkinURL: 'rotateRightDisable.png', deleteBtnEnableSkinURL: 'deleteEnable.png', deleteBtnDisableSkinURL: 'deleteDisable.png', backgroundURL: '', listBackgroundURL: '', buttonURL: 'button.png' } }, 'autosave': { 'success': 'Local conservation success' } }; ================================================ FILE: yshop-drink-vue3/public/UEditor/lang/zh-cn/zh-cn.js ================================================ /** * Created with JetBrains PhpStorm. * User: taoqili * Date: 12-6-12 * Time: 下午5:02 * To change this template use File | Settings | File Templates. */ UE.I18N['zh-cn'] = { 'labelMap':{ 'anchor':'锚点', 'undo':'撤销', 'redo':'重做', 'bold':'加粗', 'indent':'首行缩进', 'snapscreen':'截图', 'italic':'斜体', 'underline':'下划线', 'strikethrough':'删除线', 'subscript':'下标','fontborder':'字符边框', 'superscript':'上标', 'formatmatch':'格式刷', 'source':'源代码', 'blockquote':'引用', 'pasteplain':'纯文本粘贴模式', 'selectall':'全选', 'print':'打印', 'preview':'预览', 'horizontal':'分隔线', 'removeformat':'清除格式', 'time':'时间', 'date':'日期', 'unlink':'取消链接', 'insertrow':'前插入行', 'insertcol':'前插入列', 'mergeright':'右合并单元格', 'mergedown':'下合并单元格', 'deleterow':'删除行', 'deletecol':'删除列', 'splittorows':'拆分成行', 'splittocols':'拆分成列', 'splittocells':'完全拆分单元格','deletecaption':'删除表格标题','inserttitle':'插入标题', 'mergecells':'合并多个单元格', 'deletetable':'删除表格', 'cleardoc':'清空文档','insertparagraphbeforetable':"表格前插入行",'insertcode':'代码语言', 'fontfamily':'字体', 'fontsize':'字号', 'paragraph':'段落格式', 'simpleupload':'单图上传', 'insertimage':'多图上传','edittable':'表格属性','edittd':'单元格属性', 'link':'超链接', 'emotion':'表情', 'spechars':'特殊字符', 'searchreplace':'查询替换', 'map':'Baidu地图', 'gmap':'Google地图', 'insertvideo':'视频', 'help':'帮助', 'justifyleft':'居左对齐', 'justifyright':'居右对齐', 'justifycenter':'居中对齐', 'justifyjustify':'两端对齐', 'forecolor':'字体颜色', 'backcolor':'背景色', 'insertorderedlist':'有序列表', 'insertunorderedlist':'无序列表', 'fullscreen':'全屏', 'directionalityltr':'从左向右输入', 'directionalityrtl':'从右向左输入', 'rowspacingtop':'段前距', 'rowspacingbottom':'段后距', 'pagebreak':'分页', 'insertframe':'插入Iframe', 'imagenone':'默认', 'imageleft':'左浮动', 'imageright':'右浮动', 'attachment':'附件', 'imagecenter':'居中', 'wordimage':'图片转存', 'lineheight':'行间距','edittip' :'编辑提示','customstyle':'自定义标题', 'autotypeset':'自动排版', 'webapp':'百度应用','touppercase':'字母大写', 'tolowercase':'字母小写','background':'背景','template':'模板','scrawl':'涂鸦', 'music':'音乐','inserttable':'插入表格','drafts': '从草稿箱加载', 'charts': '图表' }, 'insertorderedlist':{ 'num':'1,2,3...', 'num1':'1),2),3)...', 'num2':'(1),(2),(3)...', 'cn':'一,二,三....', 'cn1':'一),二),三)....', 'cn2':'(一),(二),(三)....', 'decimal':'1,2,3...', 'lower-alpha':'a,b,c...', 'lower-roman':'i,ii,iii...', 'upper-alpha':'A,B,C...', 'upper-roman':'I,II,III...' }, 'insertunorderedlist':{ 'circle':'○ 大圆圈', 'disc':'● 小黑点', 'square':'■ 小方块 ', 'dash' :'— 破折号', 'dot':' 。 小圆圈' }, 'paragraph':{'p':'段落', 'h1':'标题 1', 'h2':'标题 2', 'h3':'标题 3', 'h4':'标题 4', 'h5':'标题 5', 'h6':'标题 6'}, 'fontfamily':{ 'songti':'宋体', 'kaiti':'楷体', 'heiti':'黑体', 'lishu':'隶书', 'yahei':'微软雅黑', 'andaleMono':'andale mono', 'arial': 'arial', 'arialBlack':'arial black', 'comicSansMs':'comic sans ms', 'impact':'impact', 'timesNewRoman':'times new roman' }, 'customstyle':{ 'tc':'标题居中', 'tl':'标题居左', 'im':'强调', 'hi':'明显强调' }, 'autoupload': { 'exceedSizeError': '文件大小超出限制', 'exceedTypeError': '文件格式不允许', 'jsonEncodeError': '服务器返回格式错误', 'loading':"正在上传...", 'loadError':"上传错误", 'errorLoadConfig': '后端配置项没有正常加载,上传插件不能正常使用!' }, 'simpleupload':{ 'exceedSizeError': '文件大小超出限制', 'exceedTypeError': '文件格式不允许', 'jsonEncodeError': '服务器返回格式错误', 'loading':"正在上传...", 'loadError':"上传错误", 'errorLoadConfig': '后端配置项没有正常加载,上传插件不能正常使用!' }, 'elementPathTip':"元素路径", 'wordCountTip':"字数统计", 'wordCountMsg':'当前已输入{#count}个字符, 您还可以输入{#leave}个字符。 ', 'wordOverFlowMsg':'字数超出最大允许值,服务器可能拒绝保存!', 'ok':"确认", 'cancel':"取消", 'closeDialog':"关闭对话框", 'tableDrag':"表格拖动必须引入uiUtils.js文件!", 'autofloatMsg':"工具栏浮动依赖编辑器UI,您首先需要引入UI文件!", 'loadconfigError': '获取后台配置项请求出错,上传功能将不能正常使用!', 'loadconfigFormatError': '后台配置项返回格式出错,上传功能将不能正常使用!', 'loadconfigHttpError': '请求后台配置项http错误,上传功能将不能正常使用!', 'snapScreen_plugin':{ 'browserMsg':"仅支持IE浏览器!", 'callBackErrorMsg':"服务器返回数据有误,请检查配置项之后重试。", 'uploadErrorMsg':"截图上传失败,请检查服务器端环境! " }, 'insertcode':{ 'as3':'ActionScript 3', 'bash':'Bash/Shell', 'cpp':'C/C++', 'css':'CSS', 'cf':'ColdFusion', 'c#':'C#', 'delphi':'Delphi', 'diff':'Diff', 'erlang':'Erlang', 'groovy':'Groovy', 'html':'HTML', 'java':'Java', 'jfx':'JavaFX', 'js':'JavaScript', 'pl':'Perl', 'php':'PHP', 'plain':'Plain Text', 'ps':'PowerShell', 'python':'Python', 'ruby':'Ruby', 'scala':'Scala', 'sql':'SQL', 'vb':'Visual Basic', 'xml':'XML' }, 'confirmClear':"确定清空当前文档么?", 'contextMenu':{ 'delete':"删除", 'selectall':"全选", 'deletecode':"删除代码", 'cleardoc':"清空文档", 'confirmclear':"确定清空当前文档么?", 'unlink':"删除超链接", 'paragraph':"段落格式", 'edittable':"表格属性", 'aligntd':"单元格对齐方式", 'aligntable':'表格对齐方式', 'tableleft':'左浮动', 'tablecenter':'居中显示', 'tableright':'右浮动', 'edittd':"单元格属性", 'setbordervisible':'设置表格边线可见', 'justifyleft':'左对齐', 'justifyright':'右对齐', 'justifycenter':'居中对齐', 'justifyjustify':'两端对齐', 'table':"表格", 'inserttable':'插入表格', 'deletetable':"删除表格", 'insertparagraphbefore':"前插入段落", 'insertparagraphafter':'后插入段落', 'deleterow':"删除当前行", 'deletecol':"删除当前列", 'insertrow':"前插入行", 'insertcol':"左插入列", 'insertrownext':'后插入行', 'insertcolnext':'右插入列', 'insertcaption':'插入表格名称', 'deletecaption':'删除表格名称', 'inserttitle':'插入表格标题行', 'deletetitle':'删除表格标题行', 'inserttitlecol':'插入表格标题列', 'deletetitlecol':'删除表格标题列', 'averageDiseRow':'平均分布各行', 'averageDisCol':'平均分布各列', 'mergeright':"向右合并", 'mergeleft':"向左合并", 'mergedown':"向下合并", 'mergecells':"合并单元格", 'splittocells':"完全拆分单元格", 'splittocols':"拆分成列", 'splittorows':"拆分成行", 'tablesort':'表格排序', 'enablesort':'设置表格可排序', 'disablesort':'取消表格可排序', 'reversecurrent':'逆序当前', 'orderbyasc':'按ASCII字符升序', 'reversebyasc':'按ASCII字符降序', 'orderbynum':'按数值大小升序', 'reversebynum':'按数值大小降序', 'borderbk':'边框底纹', 'setcolor':'表格隔行变色', 'unsetcolor':'取消表格隔行变色', 'setbackground':'选区背景隔行', 'unsetbackground':'取消选区背景', 'redandblue':'红蓝相间', 'threecolorgradient':'三色渐变', 'copy':"复制(Ctrl + c)", 'copymsg': "浏览器不支持,请使用 'Ctrl + c'", 'paste':"粘贴(Ctrl + v)", 'pastemsg': "浏览器不支持,请使用 'Ctrl + v'" }, 'copymsg': "浏览器不支持,请使用 'Ctrl + c'", 'pastemsg': "浏览器不支持,请使用 'Ctrl + v'", 'anthorMsg':"链接", 'clearColor':'清空颜色', 'standardColor':'标准颜色', 'themeColor':'主题颜色', 'property':'属性', 'default':'默认', 'modify':'修改', 'justifyleft':'左对齐', 'justifyright':'右对齐', 'justifycenter':'居中', 'justify':'默认', 'clear':'清除', 'anchorMsg':'锚点', 'delete':'删除', 'clickToUpload':"点击上传", 'unset':'尚未设置语言文件', 't_row':'行', 't_col':'列', 'more':'更多', 'pasteOpt':'粘贴选项', 'pasteSourceFormat':"保留源格式", 'tagFormat':'只保留标签', 'pasteTextFormat':'只保留文本', 'autoTypeSet':{ 'mergeLine':"合并空行", 'delLine':"清除空行", 'removeFormat':"清除格式", 'indent':"首行缩进", 'alignment':"对齐方式", 'imageFloat':"图片浮动", 'removeFontsize':"清除字号", 'removeFontFamily':"清除字体", 'removeHtml':"清除冗余HTML代码", 'pasteFilter':"粘贴过滤", 'run':"执行", 'symbol':'符号转换', 'bdc2sb':'全角转半角', 'tobdc':'半角转全角' }, 'background':{ 'static':{ 'lang_background_normal':'背景设置', 'lang_background_local':'在线图片', 'lang_background_set':'选项', 'lang_background_none':'无背景色', 'lang_background_colored':'有背景色', 'lang_background_color':'颜色设置', 'lang_background_netimg':'网络图片', 'lang_background_align':'对齐方式', 'lang_background_position':'精确定位', 'repeatType':{'options':["居中", "横向重复", "纵向重复", "平铺","自定义"]} }, 'noUploadImage':"当前未上传过任何图片!", 'toggleSelect':"单击可切换选中状态\n原图尺寸: " }, //===============dialog i18N======================= 'insertimage':{ 'static':{ 'lang_tab_remote':"插入图片", //节点 'lang_tab_upload':"本地上传", 'lang_tab_online':"在线管理", 'lang_tab_search':"图片搜索", 'lang_input_url':"地 址:", 'lang_input_size':"大 小:", 'lang_input_width':"宽度", 'lang_input_height':"高度", 'lang_input_border':"边 框:", 'lang_input_vhspace':"边 距:", 'lang_input_title':"描 述:", 'lang_input_align':'图片浮动方式:', 'lang_imgLoading':" 图片加载中……", 'lang_start_upload':"开始上传", 'lock':{'title':"锁定宽高比例"}, //属性 'searchType':{'title':"图片类型", 'options':["新闻", "壁纸", "表情", "头像"]}, //select的option 'searchTxt':{'value':"请输入搜索关键词"}, 'searchBtn':{'value':"百度一下"}, 'searchReset':{'value':"清空搜索"}, 'noneAlign':{'title':'无浮动'}, 'leftAlign':{'title':'左浮动'}, 'rightAlign':{'title':'右浮动'}, 'centerAlign':{'title':'居中独占一行'} }, 'uploadSelectFile':'点击选择图片', 'uploadAddFile':'继续添加', 'uploadStart':'开始上传', 'uploadPause':'暂停上传', 'uploadContinue':'继续上传', 'uploadRetry':'重试上传', 'uploadDelete':'删除', 'uploadTurnLeft':'向左旋转', 'uploadTurnRight':'向右旋转', 'uploadPreview':'预览中', 'uploadNoPreview':'不能预览', 'updateStatusReady': '选中_张图片,共_KB。', 'updateStatusConfirm': '已成功上传_张照片,_张照片上传失败', 'updateStatusFinish': '共_张(_KB),_张上传成功', 'updateStatusError': ',_张上传失败。', 'errorNotSupport': 'WebUploader 不支持您的浏览器!如果你使用的是IE浏览器,请尝试升级 flash 播放器。', 'errorLoadConfig': '后端配置项没有正常加载,上传插件不能正常使用!', 'errorExceedSize':'文件大小超出', 'errorFileType':'文件格式不允许', 'errorInterrupt':'文件传输中断', 'errorUploadRetry':'上传失败,请重试', 'errorHttp':'http请求错误', 'errorServerUpload':'服务器返回出错', 'remoteLockError':"宽高不正确,不能所定比例", 'numError':"请输入正确的长度或者宽度值!例如:123,400", 'imageUrlError':"不允许的图片格式或者图片域!", 'imageLoadError':"图片加载失败!请检查链接地址或网络状态!", 'searchRemind':"请输入搜索关键词", 'searchLoading':"图片加载中,请稍后……", 'searchRetry':" :( ,抱歉,没有找到图片!请重试一次!" }, 'attachment':{ 'static':{ 'lang_tab_upload': '上传附件', 'lang_tab_online': '在线附件', 'lang_start_upload':"开始上传", 'lang_drop_remind':"可以将文件拖到这里,单次最多可选100个文件" }, 'uploadSelectFile':'点击选择文件', 'uploadAddFile':'继续添加', 'uploadStart':'开始上传', 'uploadPause':'暂停上传', 'uploadContinue':'继续上传', 'uploadRetry':'重试上传', 'uploadDelete':'删除', 'uploadTurnLeft':'向左旋转', 'uploadTurnRight':'向右旋转', 'uploadPreview':'预览中', 'updateStatusReady': '选中_个文件,共_KB。', 'updateStatusConfirm': '已成功上传_个文件,_个文件上传失败', 'updateStatusFinish': '共_个(_KB),_个上传成功', 'updateStatusError': ',_张上传失败。', 'errorNotSupport': 'WebUploader 不支持您的浏览器!如果你使用的是IE浏览器,请尝试升级 flash 播放器。', 'errorLoadConfig': '后端配置项没有正常加载,上传插件不能正常使用!', 'errorExceedSize':'文件大小超出', 'errorFileType':'文件格式不允许', 'errorInterrupt':'文件传输中断', 'errorUploadRetry':'上传失败,请重试', 'errorHttp':'http请求错误', 'errorServerUpload':'服务器返回出错' }, 'insertvideo':{ 'static':{ 'lang_tab_insertV':"插入视频", 'lang_tab_searchV':"搜索视频", 'lang_tab_uploadV':"上传视频", 'lang_video_url':"视频网址", 'lang_video_size':"视频尺寸", 'lang_videoW':"宽度", 'lang_videoH':"高度", 'lang_alignment':"对齐方式", 'videoSearchTxt':{'value':"请输入搜索关键字!"}, 'videoType':{'options':["全部", "热门", "娱乐", "搞笑", "体育", "科技", "综艺"]}, 'videoSearchBtn':{'value':"百度一下"}, 'videoSearchReset':{'value':"清空结果"}, 'lang_input_fileStatus':' 当前未上传文件', 'startUpload':{'style':"background:url(upload.png) no-repeat;"}, 'lang_upload_size':"视频尺寸", 'lang_upload_width':"宽度", 'lang_upload_height':"高度", 'lang_upload_alignment':"对齐方式", 'lang_format_advice':"建议使用mp4格式." }, 'numError':"请输入正确的数值,如123,400", 'floatLeft':"左浮动", 'floatRight':"右浮动", '"default"':"默认", 'block':"独占一行", 'urlError':"输入的视频地址有误,请检查后再试!", 'loading':"  视频加载中,请等待……", 'clickToSelect':"点击选中", 'goToSource':'访问源视频', 'noVideo':"    抱歉,找不到对应的视频,请重试!", 'browseFiles':'浏览文件', 'uploadSuccess':'上传成功!', 'delSuccessFile':'从成功队列中移除', 'delFailSaveFile':'移除保存失败文件', 'statusPrompt':' 个文件已上传! ', 'flashVersionError':'当前Flash版本过低,请更新FlashPlayer后重试!', 'flashLoadingError':'Flash加载失败!请检查路径或网络状态', 'fileUploadReady':'等待上传……', 'delUploadQueue':'从上传队列中移除', 'limitPrompt1':'单次不能选择超过', 'limitPrompt2':'个文件!请重新选择!', 'delFailFile':'移除失败文件', 'fileSizeLimit':'文件大小超出限制!', 'emptyFile':'空文件无法上传!', 'fileTypeError':'文件类型不允许!', 'unknownError':'未知错误!', 'fileUploading':'上传中,请等待……', 'cancelUpload':'取消上传', 'netError':'网络错误', 'failUpload':'上传失败!', 'serverIOError':'服务器IO错误!', 'noAuthority':'无权限!', 'fileNumLimit':'上传个数限制', 'failCheck':'验证失败,本次上传被跳过!', 'fileCanceling':'取消中,请等待……', 'stopUploading':'上传已停止……', 'uploadSelectFile':'点击选择文件', 'uploadAddFile':'继续添加', 'uploadStart':'开始上传', 'uploadPause':'暂停上传', 'uploadContinue':'继续上传', 'uploadRetry':'重试上传', 'uploadDelete':'删除', 'uploadTurnLeft':'向左旋转', 'uploadTurnRight':'向右旋转', 'uploadPreview':'预览中', 'updateStatusReady': '选中_个文件,共_KB。', 'updateStatusConfirm': '成功上传_个,_个失败', 'updateStatusFinish': '共_个(_KB),_个成功上传', 'updateStatusError': ',_张上传失败。', 'errorNotSupport': 'WebUploader 不支持您的浏览器!如果你使用的是IE浏览器,请尝试升级 flash 播放器。', 'errorLoadConfig': '后端配置项没有正常加载,上传插件不能正常使用!', 'errorExceedSize':'文件大小超出', 'errorFileType':'文件格式不允许', 'errorInterrupt':'文件传输中断', 'errorUploadRetry':'上传失败,请重试', 'errorHttp':'http请求错误', 'errorServerUpload':'服务器返回出错' }, 'webapp':{ 'tip1':"本功能由百度APP提供,如看到此页面,请各位站长首先申请百度APPKey!", 'tip2':"申请完成之后请至ueditor.config.js中配置获得的appkey! ", 'applyFor':"点此申请", 'anthorApi':"百度API" }, 'template':{ 'static':{ 'lang_template_bkcolor':'背景颜色', 'lang_template_clear' : '保留原有内容', 'lang_template_select' : '选择模板' }, 'blank':"空白文档", 'blog':"博客文章", 'resume':"个人简历", 'richText':"图文混排", 'sciPapers':"科技论文" }, 'scrawl':{ 'static':{ 'lang_input_previousStep':"上一步", 'lang_input_nextsStep':"下一步", 'lang_input_clear':'清空', 'lang_input_addPic':'添加背景', 'lang_input_ScalePic':'缩放背景', 'lang_input_removePic':'删除背景', 'J_imgTxt':{title:'添加背景图片'} }, 'noScarwl':"尚未作画,白纸一张~", 'scrawlUpLoading':"涂鸦上传中,别急哦~", 'continueBtn':"继续", 'imageError':"糟糕,图片读取失败了!", 'backgroundUploading':'背景图片上传中,别急哦~' }, 'music':{ 'static':{ 'lang_input_tips':"输入歌手/歌曲/专辑,搜索您感兴趣的音乐!", 'J_searchBtn':{value:'搜索歌曲'} }, 'emptyTxt':'未搜索到相关音乐结果,请换一个关键词试试。', 'chapter':'歌曲', 'singer':'歌手', 'special':'专辑', 'listenTest':'试听' }, 'anchor':{ 'static':{ 'lang_input_anchorName':'锚点名字:' } }, 'charts':{ 'static':{ 'lang_data_source':'数据源:', 'lang_chart_format': '图表格式:', 'lang_data_align': '数据对齐方式', 'lang_chart_align_same': '数据源与图表X轴Y轴一致', 'lang_chart_align_reverse': '数据源与图表X轴Y轴相反', 'lang_chart_title': '图表标题', 'lang_chart_main_title': '主标题:', 'lang_chart_sub_title': '子标题:', 'lang_chart_x_title': 'X轴标题:', 'lang_chart_y_title': 'Y轴标题:', 'lang_chart_tip': '提示文字', 'lang_cahrt_tip_prefix': '提示文字前缀:', 'lang_cahrt_tip_description': '仅饼图有效, 当鼠标移动到饼图中相应的块上时,提示框内的文字的前缀', 'lang_chart_data_unit': '数据单位', 'lang_chart_data_unit_title': '单位:', 'lang_chart_data_unit_description': '显示在每个数据点上的数据的单位, 比如: 温度的单位 ℃', 'lang_chart_type': '图表类型:', 'lang_prev_btn': '上一个', 'lang_next_btn': '下一个' } }, 'emotion':{ 'static':{ 'lang_input_choice':'精选', 'lang_input_Tuzki':'兔斯基', 'lang_input_BOBO':'BOBO', 'lang_input_lvdouwa':'绿豆蛙', 'lang_input_babyCat':'baby猫', 'lang_input_bubble':'泡泡', 'lang_input_youa':'有啊' } }, 'gmap':{ 'static':{ 'lang_input_address':'地址', 'lang_input_search':'搜索', 'address':{value:"北京"} }, searchError:'无法定位到该地址!' }, 'help':{ 'static':{ 'lang_input_about':'关于UEditor', 'lang_input_shortcuts':'快捷键', 'lang_input_introduction':'UEditor是由百度web前端研发部开发的所见即所得富文本web编辑器,具有轻量,可定制,注重用户体验等特点。开源基于BSD协议,允许自由使用和修改代码。', 'lang_Txt_shortcuts':'快捷键', 'lang_Txt_func':'功能', 'lang_Txt_bold':'给选中字设置为加粗', 'lang_Txt_copy':'复制选中内容', 'lang_Txt_cut':'剪切选中内容', 'lang_Txt_Paste':'粘贴', 'lang_Txt_undo':'重新执行上次操作', 'lang_Txt_redo':'撤销上一次操作', 'lang_Txt_italic':'给选中字设置为斜体', 'lang_Txt_underline':'给选中字加下划线', 'lang_Txt_selectAll':'全部选中', 'lang_Txt_visualEnter':'软回车', 'lang_Txt_fullscreen':'全屏' } }, 'insertframe':{ 'static':{ 'lang_input_address':'地址:', 'lang_input_width':'宽度:', 'lang_input_height':'高度:', 'lang_input_isScroll':'允许滚动条:', 'lang_input_frameborder':'显示框架边框:', 'lang_input_alignMode':'对齐方式:', 'align':{title:"对齐方式", options:["默认", "左对齐", "右对齐", "居中"]} }, 'enterAddress':'请输入地址!' }, 'link':{ 'static':{ 'lang_input_text':'文本内容:', 'lang_input_url':'链接地址:', 'lang_input_title':'标题:', 'lang_input_target':'是否在新窗口打开:' }, 'validLink':'只支持选中一个链接时生效', 'httpPrompt':'您输入的超链接中不包含http等协议名称,默认将为您添加http://前缀' }, 'map':{ 'static':{ lang_city:"城市", lang_address:"地址", city:{value:"北京"}, lang_search:"搜索", lang_dynamicmap:"插入动态地图" }, cityMsg:"请选择城市", errorMsg:"抱歉,找不到该位置!" }, 'searchreplace':{ 'static':{ lang_tab_search:"查找", lang_tab_replace:"替换", lang_search1:"查找", lang_search2:"查找", lang_replace:"替换", lang_searchReg:'支持正则表达式,添加前后斜杠标示为正则表达式,例如“/表达式/”', lang_searchReg1:'支持正则表达式,添加前后斜杠标示为正则表达式,例如“/表达式/”', lang_case_sensitive1:"区分大小写", lang_case_sensitive2:"区分大小写", nextFindBtn:{value:"下一个"}, preFindBtn:{value:"上一个"}, nextReplaceBtn:{value:"下一个"}, preReplaceBtn:{value:"上一个"}, repalceBtn:{value:"替换"}, repalceAllBtn:{value:"全部替换"} }, getEnd:"已经搜索到文章末尾!", getStart:"已经搜索到文章头部", countMsg:"总共替换了{#count}处!" }, 'snapscreen':{ 'static':{ lang_showMsg:"截图功能需要首先安装UEditor截图插件! ", lang_download:"点此下载", lang_step1:"第一步,下载UEditor截图插件并运行安装。", lang_step2:"第二步,插件安装完成后即可使用,如不生效,请重启浏览器后再试!" } }, 'spechars':{ 'static':{}, tsfh:"特殊字符", lmsz:"罗马字符", szfh:"数学字符", rwfh:"日文字符", xlzm:"希腊字母", ewzm:"俄文字符", pyzm:"拼音字母", yyyb:"英语音标", zyzf:"其他" }, 'edittable':{ 'static':{ 'lang_tableStyle':'表格样式', 'lang_insertCaption':'添加表格名称行', 'lang_insertTitle':'添加表格标题行', 'lang_insertTitleCol':'添加表格标题列', 'lang_orderbycontent':"使表格内容可排序", 'lang_tableSize':'自动调整表格尺寸', 'lang_autoSizeContent':'按表格文字自适应', 'lang_autoSizePage':'按页面宽度自适应', 'lang_example':'示例', 'lang_borderStyle':'表格边框', 'lang_color':'颜色:' }, captionName:'表格名称', titleName:'标题', cellsName:'内容', errorMsg:'有合并单元格,不可排序' }, 'edittip':{ 'static':{ lang_delRow:'删除整行', lang_delCol:'删除整列' } }, 'edittd':{ 'static':{ lang_tdBkColor:'背景颜色:' } }, 'formula':{ 'static':{ } }, 'wordimage':{ 'static':{ lang_resave:"转存步骤", uploadBtn:{src:"upload.png",alt:"上传"}, clipboard:{style:"background: url(copy.png) -153px -1px no-repeat;"}, lang_step:"1、点击顶部复制按钮,将地址复制到剪贴板;2、点击添加照片按钮,在弹出的对话框中使用Ctrl+V粘贴地址;3、点击打开后选择图片上传流程。" }, 'fileType':"图片", 'flashError':"FLASH初始化失败,请检查FLASH插件是否正确安装!", 'netError':"网络连接错误,请重试!", 'copySuccess':"图片地址已经复制!", 'flashI18n':{} //留空默认中文 }, 'autosave': { 'saving':'保存中...', 'success':'本地保存成功' } }; ================================================ FILE: yshop-drink-vue3/public/UEditor/themes/default/css/ueditor.css ================================================ /*基础UI构建 */ /* common layer */ .edui-default .edui-box { border: none; padding: 0; margin: 0; overflow: hidden; } .edui-default a.edui-box { display: block; text-decoration: none; color: black; } .edui-default a.edui-box:hover { text-decoration: none; } .edui-default a.edui-box:active { text-decoration: none; } .edui-default table.edui-box { border-collapse: collapse; } .edui-default ul.edui-box { list-style-type: none; } div.edui-box { position: relative; display: -moz-inline-box !important; display: inline-block !important; vertical-align: top; } .edui-default .edui-clearfix { zoom: 1 } .edui-default .edui-clearfix:after { content: '\20'; display: block; clear: both; } * html div.edui-box { display: inline !important; } *:first-child+html div.edui-box { display: inline !important; } /* control layout */ .edui-default .edui-button-body, .edui-splitbutton-body, .edui-menubutton-body, .edui-combox-body { position: relative; } .edui-default .edui-popup { position: absolute; -webkit-user-select: none; -moz-user-select: none; } .edui-default .edui-popup .edui-shadow { position: absolute; z-index: -1; } .edui-default .edui-popup .edui-bordereraser { position: absolute; overflow: hidden; } .edui-default .edui-tablepicker .edui-canvas { position: relative; } .edui-default .edui-tablepicker .edui-canvas .edui-overlay { position: absolute; } .edui-default .edui-dialog-modalmask, .edui-dialog-dragmask { position: absolute; left: 0; top: 0; width: 100%; height: 100%; } .edui-default .edui-toolbar { position: relative; } /* * default theme */ .edui-default .edui-label { cursor: default; } .edui-default span.edui-clickable { color: blue; cursor: pointer; text-decoration: underline; } .edui-default span.edui-unclickable { color: gray; cursor: default; } /* 工具栏 */ .edui-default .edui-toolbar { cursor: default; -webkit-user-select: none; -moz-user-select: none; padding: 1px; overflow: hidden; /*全屏下单独一行不占位*/ zoom: 1; width:auto; height:auto; } .edui-default .edui-toolbar .edui-button, .edui-default .edui-toolbar .edui-splitbutton, .edui-default .edui-toolbar .edui-menubutton, .edui-default .edui-toolbar .edui-combox { margin: 1px; } /*UI工具栏、编辑区域、底部*/ .edui-default .edui-editor { z-index: 1 !important; border: 1px solid #d4d4d4; background-color: white; position: relative; overflow: visible; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; } .edui-editor div{ width:auto; height:auto; } .edui-default .edui-editor-toolbarbox { position: relative; zoom: 1; -webkit-box-shadow:0 1px 4px rgba(204, 204, 204, 0.6); -moz-box-shadow:0 1px 4px rgba(204, 204, 204, 0.6); box-shadow:0 1px 4px rgba(204, 204, 204, 0.6); border-top-left-radius:2px; border-top-right-radius:2px; } .edui-default .edui-editor-toolbarboxouter { border-bottom: 1px solid #d4d4d4; background-color: #fafafa; background-image: -moz-linear-gradient(top, #ffffff, #f2f2f2); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f2f2f2)); background-image: -webkit-linear-gradient(top, #ffffff, #f2f2f2); background-image: -o-linear-gradient(top, #ffffff, #f2f2f2); background-image: linear-gradient(to bottom, #ffffff, #f2f2f2); background-repeat: repeat-x; /*border: 1px solid #d4d4d4;*/ -webkit-border-radius: 4px 4px 0 0; -moz-border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0); *zoom: 1; -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); } .edui-default .edui-editor-toolbarboxinner { padding: 2px; } .edui-default .edui-editor-iframeholder { position: relative; /*for fix ie6 toolbarmsg under iframe bug. relative -> static */ /*_position: static !important;* } .edui-default .edui-editor-iframeholder textarea { font-family: consolas, "Courier New", "lucida console", monospace; font-size: 12px; line-height: 18px; } .edui-default .edui-editor-bottombar { /*border-top: 1px solid #ccc;*/ /*height: 20px;*/ /*width: 40%;*/ /*float: left;*/ /*overflow: hidden;*/ } .edui-default .edui-editor-bottomContainer { overflow: hidden; } .edui-default .edui-editor-bottomContainer table { width: 100%; height: 0; overflow: hidden; border-spacing: 0; } .edui-default .edui-editor-bottomContainer td { white-space: nowrap; border-top: 1px solid #ccc; line-height: 20px; font-size: 12px; font-family: Arial, Helvetica, Tahoma, Verdana, Sans-Serif; } .edui-default .edui-editor-wordcount { text-align: right; margin-right: 5px; color: #aaa; } .edui-default .edui-editor-scale { width: 12px; } .edui-default .edui-editor-scale .edui-editor-icon { float: right; width: 100%; height: 12px; margin-top: 10px; background: url(../images/scale.png) no-repeat; cursor: se-resize; } .edui-default .edui-editor-breadcrumb { margin: 2px 0 0 3px; } .edui-default .edui-editor-breadcrumb span { cursor: pointer; text-decoration: underline; color: blue; } .edui-default .edui-toolbar .edui-for-fullscreen { float: right; } .edui-default .edui-bubble .edui-popup-content { border: 1px solid #DCAC6C; background-color: #fff6d9; padding: 5px; font-size: 10pt; font-family: "宋体"; } .edui-default .edui-bubble .edui-shadow { /*box-shadow: 1px 1px 3px #818181;*/ /*-webkit-box-shadow: 2px 2px 3px #818181;*/ /*-moz-box-shadow: 2px 2px 3px #818181;*/ /*filter: progid:DXImageTransform.Microsoft.Blur(PixelRadius = '2', MakeShadow = 'true', ShadowOpacity = '0.5');*/ } .edui-default .edui-editor-toolbarmsg { background-color: #FFF6D9; border-bottom: 1px solid #ccc; position: absolute; bottom: -25px; left: 0; z-index: 1009; width: 99.9%; } .edui-default .edui-editor-toolbarmsg-upload { font-size: 14px; color: blue; width: 100px; height: 16px; line-height: 16px; cursor: pointer; position: absolute; top: 5px; left: 350px; } .edui-default .edui-editor-toolbarmsg-label { font-size: 12px; line-height: 16px; padding: 4px; } .edui-default .edui-editor-toolbarmsg-close { float: right; width: 20px; height: 16px; line-height: 16px; cursor: pointer; color: red; } /*可选中菜单按钮*/ .edui-default .edui-list .edui-bordereraser { display: none; } .edui-default .edui-listitem { padding: 1px; white-space: nowrap; } .edui-default .edui-list .edui-state-hover { position: relative; background-color: #fff5d4; border: 1px solid #dcac6c; padding: 0; } .edui-default .edui-for-fontfamily .edui-listitem-label { min-width: 130px; _width: 120px; font-size: 12px; height: 22px; line-height: 22px; padding-left: 5px; } .edui-default .edui-for-insertcode .edui-listitem-label { min-width: 120px; _width: 120px; font-size: 12px; height: 22px; line-height: 22px; padding-left: 5px; } .edui-default .edui-for-underline .edui-listitem-label { min-width: 120px; _width: 120px; padding: 3px 5px; font-size: 12px; } .edui-default .edui-for-fontsize .edui-listitem-label { min-width: 120px; _width: 120px; padding: 3px 5px; } .edui-default .edui-for-paragraph .edui-listitem-label { min-width: 200px; _width: 200px; padding: 2px 5px; } .edui-default .edui-for-rowspacingtop .edui-listitem-label, .edui-default .edui-for-rowspacingbottom .edui-listitem-label { min-width: 53px; _width: 53px; padding: 2px 5px; } .edui-default .edui-for-lineheight .edui-listitem-label { min-width: 53px; _width: 53px; padding: 2px 5px; } .edui-default .edui-for-customstyle .edui-listitem-label { min-width: 200px; _width: 200px; width: 200px !important; padding: 2px 5px; } /* 可选中按钮弹出菜单*/ .edui-default .edui-menu { z-index: 3000; } .edui-default .edui-menu .edui-popup-content { padding: 3px; } .edui-default .edui-menu-body { _width: 150px; min-width: 170px; background: url("../images/sparator_v.png") repeat-y 25px; } .edui-default .edui-menuitem-body { } .edui-default .edui-menuitem { height: 20px; cursor: default; vertical-align: top; } .edui-default .edui-menuitem .edui-icon { width: 20px !important; height: 20px !important; background: url(../images/icons.png) 0 -4000px; background: url(../images/icons.gif) 0 -4000px\9; } .edui-default .edui-menuitem .edui-label { font-size: 12px; line-height: 20px; height: 20px; padding-left: 10px; } .edui-default .edui-state-checked .edui-menuitem-body { background: url("../images/icons-all.gif") no-repeat 6px -205px; } .edui-default .edui-state-disabled .edui-menuitem-label { color: gray; } /*不可选中菜单按钮 */ .edui-default .edui-toolbar .edui-combox-body .edui-button-body { width: 60px; font-size: 12px; height: 20px; line-height: 20px; padding-left: 5px; white-space: nowrap; margin: 0 3px 0 0; } .edui-default .edui-toolbar .edui-combox-body .edui-arrow { background: url(../images/icons.png) -741px 0; _background: url(../images/icons.gif) -741px 0; height: 20px; width: 9px; } .edui-default .edui-toolbar .edui-combox .edui-combox-body { border: 1px solid #CCC; background-color: white; border-radius: 2px; -webkit-border-radius: 2px; -moz-border-radius: 2px; } .edui-default .edui-toolbar .edui-combox-body .edui-splitborder { display: none; } .edui-default .edui-toolbar .edui-combox-body .edui-arrow { border-left: 1px solid #CCC; } .edui-default .edui-toolbar .edui-state-hover .edui-combox-body { background-color: #fff5d4; border: 1px solid #dcac6c; } .edui-default .edui-toolbar .edui-state-hover .edui-combox-body .edui-arrow { border-left: 1px solid #dcac6c; } .edui-default .edui-toolbar .edui-state-checked .edui-combox-body { background-color: #FFE69F; border: 1px solid #DCAC6C; } .edui-toolbar .edui-state-checked .edui-combox-body .edui-arrow { border-left: 1px solid #DCAC6C; } .edui-toolbar .edui-state-disabled .edui-combox-body { background-color: #F0F0EE; opacity: 0.3; filter: alpha(opacity = 30); } .edui-toolbar .edui-state-opened .edui-combox-body { background-color: white; border: 1px solid gray; } /*普通按钮样式及状态*/ .edui-default .edui-toolbar .edui-button .edui-icon, .edui-default .edui-toolbar .edui-menubutton .edui-icon, .edui-default .edui-toolbar .edui-splitbutton .edui-icon { height: 20px !important; width: 20px !important; background-image: url(../images/icons.png); background-image: url(../images/icons.gif) \9; } .edui-default .edui-toolbar .edui-button .edui-button-wrap { padding: 1px; position: relative; } .edui-default .edui-toolbar .edui-button .edui-state-hover .edui-button-wrap { background-color: #fff5d4; padding: 0; border: 1px solid #dcac6c; } .edui-default .edui-toolbar .edui-button .edui-state-checked .edui-button-wrap { background-color: #ffe69f; padding: 0; border: 1px solid #dcac6c; border-radius: 2px; -webkit-border-radius: 2px; -moz-border-radius: 2px; } .edui-default .edui-toolbar .edui-button .edui-state-active .edui-button-wrap { background-color: #ffffff; padding: 0; border: 1px solid gray; } .edui-default .edui-toolbar .edui-state-disabled .edui-label { color: #ccc; } .edui-default .edui-toolbar .edui-state-disabled .edui-icon { opacity: 0.3; filter: alpha(opacity = 30); } /* toolbar icons */ .edui-default .edui-for-undo .edui-icon { background-position: -160px 0; } .edui-default .edui-for-redo .edui-icon { background-position: -100px 0; } .edui-default .edui-for-bold .edui-icon { background-position: 0 0; } .edui-default .edui-for-italic .edui-icon { background-position: -60px 0; } .edui-default .edui-for-fontborder .edui-icon { background-position:-160px -40px; } .edui-default .edui-for-underline .edui-icon { background-position: -140px 0; } .edui-default .edui-for-strikethrough .edui-icon { background-position: -120px 0; } .edui-default .edui-for-subscript .edui-icon { background-position: -600px 0; } .edui-default .edui-for-superscript .edui-icon { background-position: -620px 0; } .edui-default .edui-for-blockquote .edui-icon { background-position: -220px 0; } .edui-default .edui-for-forecolor .edui-icon { background-position: -720px 0; } .edui-default .edui-for-backcolor .edui-icon { background-position: -760px 0; } .edui-default .edui-for-inserttable .edui-icon { background-position: -580px -20px; } .edui-default .edui-for-autotypeset .edui-icon { background-position: -640px -40px; } .edui-default .edui-for-justifyleft .edui-icon { background-position: -460px 0; } .edui-default .edui-for-justifycenter .edui-icon { background-position: -420px 0; } .edui-default .edui-for-justifyright .edui-icon { background-position: -480px 0; } .edui-default .edui-for-justifyjustify .edui-icon { background-position: -440px 0; } .edui-default .edui-for-insertorderedlist .edui-icon { background-position: -80px 0; } .edui-default .edui-for-insertunorderedlist .edui-icon { background-position: -20px 0; } .edui-default .edui-for-lineheight .edui-icon { background-position: -725px -40px; } .edui-default .edui-for-rowspacingbottom .edui-icon { background-position: -745px -40px; } .edui-default .edui-for-rowspacingtop .edui-icon { background-position: -765px -40px; } .edui-default .edui-for-horizontal .edui-icon { background-position: -360px 0; } .edui-default .edui-for-link .edui-icon { background-position: -500px 0; } .edui-default .edui-for-code .edui-icon { background-position: -440px -40px; } .edui-default .edui-for-insertimage .edui-icon { background-position: -726px -77px; } .edui-default .edui-for-insertframe .edui-icon { background-position: -240px -40px; } .edui-default .edui-for-emoticon .edui-icon { background-position: -60px -20px; } .edui-default .edui-for-spechars .edui-icon { background-position: -240px 0; } .edui-default .edui-for-help .edui-icon { background-position: -340px 0; } .edui-default .edui-for-print .edui-icon { background-position: -440px -20px; } .edui-default .edui-for-preview .edui-icon { background-position: -420px -20px; } .edui-default .edui-for-selectall .edui-icon { background-position: -400px -20px; } .edui-default .edui-for-searchreplace .edui-icon { background-position: -520px -20px; } .edui-default .edui-for-map .edui-icon { background-position: -40px -40px; } .edui-default .edui-for-gmap .edui-icon { background-position: -260px -40px; } .edui-default .edui-for-insertvideo .edui-icon { background-position: -320px -20px; } .edui-default .edui-for-time .edui-icon { background-position: -160px -20px; } .edui-default .edui-for-date .edui-icon { background-position: -140px -20px; } .edui-default .edui-for-cut .edui-icon { background-position: -680px 0; } .edui-default .edui-for-copy .edui-icon { background-position: -700px 0; } .edui-default .edui-for-paste .edui-icon { background-position: -560px 0; } .edui-default .edui-for-formatmatch .edui-icon { background-position: -40px 0; } .edui-default .edui-for-pasteplain .edui-icon { background-position: -360px -20px; } .edui-default .edui-for-directionalityltr .edui-icon { background-position: -20px -20px; } .edui-default .edui-for-directionalityrtl .edui-icon { background-position: -40px -20px; } .edui-default .edui-for-source .edui-icon { background-position: -261px -0px; } .edui-default .edui-for-removeformat .edui-icon { background-position: -580px 0; } .edui-default .edui-for-unlink .edui-icon { background-position: -640px 0; } .edui-default .edui-for-touppercase .edui-icon { background-position: -786px 0; } .edui-default .edui-for-tolowercase .edui-icon { background-position: -806px 0; } .edui-default .edui-for-insertrow .edui-icon { background-position: -478px -76px; } .edui-default .edui-for-insertrownext .edui-icon { background-position: -498px -76px; } .edui-default .edui-for-insertcol .edui-icon { background-position: -455px -76px; } .edui-default .edui-for-insertcolnext .edui-icon { background-position: -429px -76px; } .edui-default .edui-for-mergeright .edui-icon { background-position: -60px -40px; } .edui-default .edui-for-mergedown .edui-icon { background-position: -80px -40px; } .edui-default .edui-for-splittorows .edui-icon { background-position: -100px -40px; } .edui-default .edui-for-splittocols .edui-icon { background-position: -120px -40px; } .edui-default .edui-for-insertparagraphbeforetable .edui-icon { background-position: -140px -40px; } .edui-default .edui-for-deleterow .edui-icon { background-position: -660px -20px; } .edui-default .edui-for-deletecol .edui-icon { background-position: -640px -20px; } .edui-default .edui-for-splittocells .edui-icon { background-position: -800px -20px; } .edui-default .edui-for-mergecells .edui-icon { background-position: -760px -20px; } .edui-default .edui-for-deletetable .edui-icon { background-position: -620px -20px; } .edui-default .edui-for-cleardoc .edui-icon { background-position: -520px 0; } .edui-default .edui-for-fullscreen .edui-icon { background-position: -100px -20px; } .edui-default .edui-for-anchor .edui-icon { background-position: -200px 0; } .edui-default .edui-for-pagebreak .edui-icon { background-position: -460px -40px; } .edui-default .edui-for-imagenone .edui-icon { background-position: -480px -40px; } .edui-default .edui-for-imageleft .edui-icon { background-position: -500px -40px; } .edui-default .edui-for-wordimage .edui-icon { background-position: -660px -40px; } .edui-default .edui-for-imageright .edui-icon { background-position: -520px -40px; } .edui-default .edui-for-imagecenter .edui-icon { background-position: -540px -40px; } .edui-default .edui-for-indent .edui-icon { background-position: -400px 0; } .edui-default .edui-for-outdent .edui-icon { background-position: -540px 0; } .edui-default .edui-for-webapp .edui-icon { background-position: -601px -40px } .edui-default .edui-for-table .edui-icon { background-position: -580px -20px; } .edui-default .edui-for-edittable .edui-icon { background-position: -420px -40px; } .edui-default .edui-for-template .edui-icon { background-position: -339px -40px; } .edui-default .edui-for-delete .edui-icon { background-position: -360px -40px; } .edui-default .edui-for-attachment .edui-icon { background-position: -620px -40px; } .edui-default .edui-for-edittd .edui-icon { background-position: -700px -40px; } .edui-default .edui-for-snapscreen .edui-icon { background-position: -581px -40px } .edui-default .edui-for-scrawl .edui-icon { background-position: -801px -41px } .edui-default .edui-for-background .edui-icon { background-position: -680px -40px; } .edui-default .edui-for-music .edui-icon { background-position: -18px -40px } .edui-default .edui-for-formula .edui-icon { background-position: -200px -40px } .edui-default .edui-for-aligntd .edui-icon { background-position: -236px -76px; } .edui-default .edui-for-insertparagraphtrue .edui-icon { background-position: -625px -76px; } .edui-default .edui-for-insertparagraph .edui-icon { background-position: -602px -76px; } .edui-default .edui-for-insertcaption .edui-icon { background-position: -336px -76px; } .edui-default .edui-for-deletecaption .edui-icon { background-position: -362px -76px; } .edui-default .edui-for-inserttitle .edui-icon { background-position: -286px -76px; } .edui-default .edui-for-deletetitle .edui-icon { background-position: -311px -76px; } .edui-default .edui-for-aligntable .edui-icon { background-position: -440px 0; } .edui-default .edui-for-tablealignment-left .edui-icon { background-position: -460px 0; } .edui-default .edui-for-tablealignment-center .edui-icon { background-position: -420px 0; } .edui-default .edui-for-tablealignment-right .edui-icon { background-position: -480px 0; } .edui-default .edui-for-drafts .edui-icon { background-position: -560px 0; } .edui-default .edui-for-charts .edui-icon { background: url( ../images/charts.png ) no-repeat 2px 3px!important; } .edui-default .edui-for-inserttitlecol .edui-icon { background-position: -673px -76px; } .edui-default .edui-for-deletetitlecol .edui-icon { background-position: -698px -76px; } .edui-default .edui-for-simpleupload .edui-icon { background-position: -380px 0px; } /*splitbutton*/ .edui-default .edui-toolbar .edui-splitbutton-body .edui-arrow, .edui-default .edui-toolbar .edui-menubutton-body .edui-arrow { background: url(../images/icons.png) -741px 0; _background: url(../images/icons.gif) -741px 0; height: 20px; width: 9px; } .edui-default .edui-toolbar .edui-splitbutton .edui-splitbutton-body, .edui-default .edui-toolbar .edui-menubutton .edui-menubutton-body { padding: 1px; } .edui-default .edui-toolbar .edui-splitborder { width: 1px; height: 20px; } .edui-default .edui-toolbar .edui-state-hover .edui-splitborder { width: 1px; border-left: 0px solid #dcac6c; } .edui-default .edui-toolbar .edui-state-active .edui-splitborder { width: 0; border-left: 1px solid gray; } .edui-default .edui-toolbar .edui-state-opened .edui-splitborder { width: 1px; border: 0; } .edui-default .edui-toolbar .edui-splitbutton .edui-state-hover .edui-splitbutton-body, .edui-default .edui-toolbar .edui-menubutton .edui-state-hover .edui-menubutton-body { background-color: #fff5d4; border: 1px solid #dcac6c; padding: 0; } .edui-default .edui-toolbar .edui-splitbutton .edui-state-checked .edui-splitbutton-body, .edui-default .edui-toolbar .edui-menubutton .edui-state-checked .edui-menubutton-body { background-color: #FFE69F; border: 1px solid #DCAC6C; padding: 0; } .edui-default .edui-toolbar .edui-splitbutton .edui-state-active .edui-splitbutton-body, .edui-default .edui-toolbar .edui-menubutton .edui-state-active .edui-menubutton-body { background-color: #ffffff; border: 1px solid gray; padding: 0; } .edui-default .edui-state-disabled .edui-arrow { opacity: 0.3; _filter: alpha(opacity = 30); } .edui-default .edui-toolbar .edui-splitbutton .edui-state-opened .edui-splitbutton-body, .edui-default .edui-toolbar .edui-menubutton .edui-state-opened .edui-menubutton-body { background-color: white; border: 1px solid gray; padding: 0; } .edui-default .edui-for-insertorderedlist .edui-bordereraser, .edui-default .edui-for-lineheight .edui-bordereraser, .edui-default .edui-for-rowspacingtop .edui-bordereraser, .edui-default .edui-for-rowspacingbottom .edui-bordereraser, .edui-default .edui-for-insertunorderedlist .edui-bordereraser { background-color: white; } /* 解决嵌套导致的图标问题 */ .edui-default .edui-for-insertorderedlist .edui-popup-body .edui-icon, .edui-default .edui-for-lineheight .edui-popup-body .edui-icon, .edui-default .edui-for-rowspacingtop .edui-popup-body .edui-icon, .edui-default .edui-for-rowspacingbottom .edui-popup-body .edui-icon, .edui-default .edui-for-insertunorderedlist .edui-popup-body .edui-icon { /*background-position: 0 -40px;*/ background-image: none ; } /* 弹出菜单 */ .edui-default .edui-popup { z-index: 3000; background-color: #ffffff; width:auto; height:auto; } .edui-default .edui-popup .edui-shadow { left: 0; top: 0; width: 100%; height: 100%; } .edui-default .edui-popup-content { border:1px solid #ccc; border: 1px solid rgba(0, 0, 0, 0.2); *border-right-width: 2px; *border-bottom-width: 2px; -webkit-border-radius: 6px; -moz-border-radius: 6px; border-radius: 6px; -webkit-box-shadow: 0 3px 4px rgba(0, 0, 0, 0.2); -moz-box-shadow: 0 3px 4px rgba(0, 0, 0, 0.2); box-shadow: 0 3px 4px rgba(0, 0, 0, 0.2); -webkit-background-clip: padding-box; -moz-background-clip: padding; background-clip: padding-box; padding: 5px; background:#ffffff; } .edui-default .edui-popup .edui-bordereraser { background-color: white; height: 3px; } .edui-default .edui-menu .edui-bordereraser { height: 3px; } .edui-default .edui-anchor-topleft .edui-bordereraser { left: 1px; top: -2px; } .edui-default .edui-anchor-topright .edui-bordereraser { right: 1px; top: -2px; } .edui-default .edui-anchor-bottomleft .edui-bordereraser { left: 0; bottom: -6px; height: 7px; border-left: 1px solid gray; border-right: 1px solid gray; } .edui-default .edui-anchor-bottomright .edui-bordereraser { right: 0; bottom: -6px; height: 7px; border-left: 1px solid gray; border-right: 1px solid gray; } .edui-popup div{ width:auto; height:auto; } .edui-default .edui-editor-messageholder { display: block; width: 150px; height: auto; border: 0; margin: 0; padding: 0; position: absolute; top: 28px; right: 3px; } .edui-default .edui-message{ min-height: 10px; text-shadow: 0 1px 0 rgba(255,255,255,0.5); padding: 0; margin-bottom: 3px; position: relative; } .edui-default .edui-message-body{ border-radius: 3px; padding: 8px 15px 8px 8px; color: #c09853; background-color: #fcf8e3; border: 1px solid #fbeed5; } .edui-default .edui-message-type-info{ color: #3a87ad; background-color: #d9edf7; border-color: #bce8f1 } .edui-default .edui-message-type-success{ color: #468847; background-color: #dff0d8; border-color: #d6e9c6 } .edui-default .edui-message-type-danger, .edui-default .edui-message-type-error{ color: #b94a48; background-color: #f2dede; border-color: #eed3d7 } .edui-default .edui-message .edui-message-closer { display: block; width: 16px; height: 16px; line-height: 16px; position: absolute; top: 0; right: 0; padding: 0; cursor: pointer; background: transparent; border: 0; float: right; font-size: 20px; font-weight: bold; color: #999; text-shadow: 0 1px 0 #fff; font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; } .edui-default .edui-message .edui-message-content { font-size: 10pt; word-wrap: break-word; word-break: normal; } /* 弹出对话框按钮和对话框大小 */ .edui-default .edui-dialog { z-index: 2000; position: absolute; } .edui-dialog div{ width:auto; } .edui-default .edui-dialog-wrap { margin-right: 6px; margin-bottom: 6px; } .edui-default .edui-dialog-fullscreen-flag { margin-right: 0; margin-bottom: 0; } .edui-default .edui-dialog-body { position: relative; padding:2px 0 0 2px; _zoom: 1; } .edui-default .edui-dialog-fullscreen-flag .edui-dialog-body { padding: 0; } .edui-default .edui-dialog-shadow { position: absolute; z-index: -1; left: 0; top: 0; width: 100%; height: 100%; background-color: #ffffff; border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, 0.2); *border-right-width: 2px; *border-bottom-width: 2px; -webkit-border-radius: 6px; -moz-border-radius: 6px; border-radius: 6px; -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); -webkit-background-clip: padding-box; -moz-background-clip: padding; background-clip: padding-box; } .edui-default .edui-dialog-foot { background-color: white; } .edui-default .edui-dialog-titlebar { height: 26px; border-bottom: 1px solid #c6c6c6; background: url(../images/dialog-title-bg.png) repeat-x bottom; position: relative; cursor: move; } .edui-default .edui-dialog-caption { font-weight: bold; font-size: 12px; line-height: 26px; padding-left: 5px; } .edui-default .edui-dialog-draghandle { height: 26px; } .edui-default .edui-dialog-closebutton { position: absolute !important; right: 5px; top: 3px; } .edui-default .edui-dialog-closebutton .edui-button-body { height: 20px; width: 20px; cursor: pointer; background: url("../images/icons-all.gif") no-repeat 0 -59px; } .edui-default .edui-dialog-closebutton .edui-state-hover .edui-button-body { background: url("../images/icons-all.gif") no-repeat 0 -89px; } .edui-default .edui-dialog-foot { height: 40px; } .edui-default .edui-dialog-buttons { position: absolute; right: 0; } .edui-default .edui-dialog-buttons .edui-button { margin-right: 10px; } .edui-default .edui-dialog-buttons .edui-button .edui-button-body { background: url("../images/icons-all.gif") no-repeat; height: 24px; width: 96px; font-size: 12px; line-height: 24px; text-align: center; cursor: default; } .edui-default .edui-dialog-buttons .edui-button .edui-state-hover .edui-button-body { background: url("../images/icons-all.gif") no-repeat 0 -30px; } .edui-default .edui-dialog iframe { border: 0; padding: 0; margin: 0; vertical-align: top; } .edui-default .edui-dialog-modalmask { opacity: 0.3; filter: alpha(opacity = 30); background-color: #ccc; position: absolute; /*z-index: 1999;*/ } .edui-default .edui-dialog-dragmask { position: absolute; /*z-index: 2001;*/ background-color: transparent; cursor: move; } .edui-default .edui-dialog-content { position: relative; } .edui-default .dialogcontmask { cursor: move; visibility: hidden; display: block; position: absolute; width: 100%; height: 100%; opacity: 0; filter: alpha(opacity = 0); } /*link-dialog*/ .edui-default .edui-for-link .edui-dialog-content { width: 420px; height: 200px; overflow: hidden; } /*background-dialog*/ .edui-default .edui-for-background .edui-dialog-content { width: 440px; height: 280px; overflow: hidden; } /*template-dialog*/ .edui-default .edui-for-template .edui-dialog-content { width: 630px; height: 390px; overflow: hidden; } /*scrawl-dialog*/ .edui-default .edui-for-scrawl .edui-dialog-content { width: 515px; *width: 506px; height: 360px; } /*spechars-dialog*/ .edui-default .edui-for-spechars .edui-dialog-content { width: 620px; height: 500px; *width: 630px; *height: 570px; } /*image-dialog*/ .edui-default .edui-for-insertimage .edui-dialog-content { width: 650px; height: 400px; overflow: hidden; } /*webapp-dialog*/ .edui-default .edui-for-webapp .edui-dialog-content { width: 560px; _width: 565px; height: 450px; overflow: hidden; } /*image-insertframe*/ .edui-default .edui-for-insertframe .edui-dialog-content { width: 350px; height: 200px; overflow: hidden; } /*wordImage-dialog*/ .edui-default .edui-for-wordimage .edui-dialog-content { width: 620px; height: 380px; overflow: hidden; } /*attachment-dialog*/ .edui-default .edui-for-attachment .edui-dialog-content { width: 650px; height: 400px; overflow: hidden; } /*map-dialog*/ .edui-default .edui-for-map .edui-dialog-content { width: 550px; height: 400px; } /*gmap-dialog*/ .edui-default .edui-for-gmap .edui-dialog-content { width: 550px; height: 400px; } /*video-dialog*/ .edui-default .edui-for-insertvideo .edui-dialog-content { width: 590px; height: 390px; } /*anchor-dialog*/ .edui-default .edui-for-anchor .edui-dialog-content { width: 320px; height: 60px; overflow: hidden; } /*searchreplace-dialog*/ .edui-default .edui-for-searchreplace .edui-dialog-content { width: 400px; height: 220px; } /*help-dialog*/ .edui-default .edui-for-help .edui-dialog-content { width: 400px; height: 420px; } /*edittable-dialog*/ .edui-default .edui-for-edittable .edui-dialog-content { width: 540px; _width:590px; height: 335px; } /*edittip-dialog*/ .edui-default .edui-for-edittip .edui-dialog-content { width: 225px; height: 60px; } /*edittd-dialog*/ .edui-default .edui-for-edittd .edui-dialog-content { width: 240px; height: 50px; } /*snapscreen-dialog*/ .edui-default .edui-for-snapscreen .edui-dialog-content { width: 400px; height: 220px; } /*music-dialog*/ .edui-default .edui-for-music .edui-dialog-content { width: 515px; height: 360px; } /*段落弹出菜单*/ .edui-default .edui-for-paragraph .edui-listitem-label { font-family: Tahoma, Verdana, Arial, Helvetica; } .edui-default .edui-for-paragraph .edui-listitem-label .edui-for-p { font-size: 22px; line-height: 27px; } .edui-default .edui-for-paragraph .edui-listitem-label .edui-for-h1 { font-weight: bolder; font-size: 32px; line-height: 36px; } .edui-default .edui-for-paragraph .edui-listitem-label .edui-for-h2 { font-weight: bolder; font-size: 27px; line-height: 29px; } .edui-default .edui-for-paragraph .edui-listitem-label .edui-for-h3 { font-weight: bolder; font-size: 19px; line-height: 23px; } .edui-default .edui-for-paragraph .edui-listitem-label .edui-for-h4 { font-weight: bolder; font-size: 16px; line-height: 19px } .edui-default .edui-for-paragraph .edui-listitem-label .edui-for-h5 { font-weight: bolder; font-size: 13px; line-height: 16px; } .edui-default .edui-for-paragraph .edui-listitem-label .edui-for-h6 { font-weight: bolder; font-size: 12px; line-height: 14px; } /* 表格弹出菜单 */ .edui-default .edui-for-inserttable .edui-splitborder { display: none } .edui-default .edui-for-inserttable .edui-splitbutton-body .edui-arrow { width: 0 } .edui-default .edui-toolbar .edui-for-inserttable .edui-state-active .edui-splitborder{ border-left: 1px solid transparent; } .edui-default .edui-tablepicker .edui-infoarea { height: 14px; line-height: 14px; font-size: 12px; width: 220px; margin-bottom: 3px; clear: both; } .edui-default .edui-tablepicker .edui-infoarea .edui-label { float: left; } .edui-default .edui-dialog-buttons .edui-label { line-height: 24px; } .edui-default .edui-tablepicker .edui-infoarea .edui-clickable { float: right; } .edui-default .edui-tablepicker .edui-pickarea { background: url("../images/unhighlighted.gif") repeat; height: 220px; width: 220px; } .edui-default .edui-tablepicker .edui-pickarea .edui-overlay { background: url("../images/highlighted.gif") repeat; } /* 颜色弹出菜单 */ .edui-default .edui-colorpicker-topbar { height: 27px; width: 200px; /*border-bottom: 1px gray dashed;*/ } .edui-default .edui-colorpicker-preview { height: 20px; border: 1px inset black; margin-left: 1px; width: 128px; float: left; } .edui-default .edui-colorpicker-nocolor { float: right; margin-right: 1px; font-size: 12px; line-height: 14px; height: 14px; border: 1px solid #333; padding: 3px 5px; cursor: pointer; } .edui-default .edui-colorpicker-tablefirstrow { height: 30px; } .edui-default .edui-colorpicker-colorcell { width: 14px; height: 14px; display: block; margin: 0; cursor: pointer; } .edui-default .edui-colorpicker-colorcell:hover { width: 14px; height: 14px; margin: 0; } .edui-default .edui-colorpicker-advbtn{ display: block; text-align: center; cursor: pointer; height:20px; } .arrow_down{ background: white url('../images/arrow_down.png') no-repeat center; } .arrow_up{ background: white url('../images/arrow_up.png') no-repeat center; } /*高级的样式*/ .edui-colorpicker-adv{ position: relative; overflow: hidden; height: 180px; display: none; } .edui-colorpicker-plant, .edui-colorpicker-hue { border: solid 1px #666; } .edui-colorpicker-pad { width: 150px; height: 150px; left: 14px; top: 13px; position: absolute; background: red; overflow: hidden; cursor: crosshair; } .edui-colorpicker-cover{ position: absolute; top: 0; left: 0; width: 150px; height: 150px; background: url("../images/tangram-colorpicker.png") -160px -200px; } .edui-colorpicker-padDot{ position: absolute; top: 0; left: 0; width: 11px; height: 11px; overflow: hidden; background: url(../images/tangram-colorpicker.png) 0px -200px repeat-x; z-index: 1000; } .edui-colorpicker-sliderMain { position: absolute; left: 171px; top: 13px; width: 19px; height: 152px; background: url(../images/tangram-colorpicker.png) -179px -12px no-repeat; } .edui-colorpicker-slider { width: 100%; height: 100%; cursor: pointer; } .edui-colorpicker-thumb{ position: absolute; top: 0; cursor: pointer; height: 3px; left: -1px; right: -1px; border: 1px solid black; background: white; opacity: .8; } /*自动排版弹出菜单*/ .edui-default .edui-autotypesetpicker .edui-autotypesetpicker-body { font-size: 12px; margin-bottom: 3px; clear: both; } .edui-default .edui-autotypesetpicker-body table { border-collapse: separate; border-spacing: 2px; } .edui-default .edui-autotypesetpicker-body td { font-size: 12px; word-wrap:break-word; } .edui-default .edui-autotypesetpicker-body td input { margin: 3px 3px 3px 4px; *margin: 1px 0 0 0; } /*自动排版弹出菜单*/ .edui-default .edui-cellalignpicker .edui-cellalignpicker-body { width: 70px; font-size: 12px; cursor: default; } .edui-default .edui-cellalignpicker-body table { border-collapse: separate; border-spacing: 0; } .edui-default .edui-cellalignpicker-body td{ padding: 1px; } .edui-default .edui-cellalignpicker-body .edui-icon{ height: 20px; width: 20px; padding: 1px; background-image: url(../images/table-cell-align.png); } .edui-default .edui-cellalignpicker-body .edui-left{ background-position: 0 0; } .edui-default .edui-cellalignpicker-body .edui-center{ background-position: -25px 0; } .edui-default .edui-cellalignpicker-body .edui-right{ background-position: -51px 0; } .edui-default .edui-cellalignpicker-body td.edui-state-hover .edui-left{ background-position: -73px 0; } .edui-default .edui-cellalignpicker-body td.edui-state-hover .edui-center{ background-position: -98px 0; } .edui-default .edui-cellalignpicker-body td.edui-state-hover .edui-right{ background-position: -124px 0; } .edui-default .edui-cellalignpicker-body td.edui-cellalign-selected .edui-left { background-position: -146px 0; background-color: #f1f4f5; } .edui-default .edui-cellalignpicker-body td.edui-cellalign-selected .edui-center { background-position: -245px 0; } .edui-default .edui-cellalignpicker-body td.edui-cellalign-selected .edui-right { background-position: -271px 0; } /*分隔线*/ .edui-default .edui-toolbar .edui-separator { width: 2px; height: 20px; margin: 2px 4px 2px 3px; background: url(../images/icons.png) -181px 0; background: url(../images/icons.gif) -181px 0 \9; } /*颜色按钮 */ .edui-default .edui-toolbar .edui-colorbutton .edui-colorlump { position: absolute; overflow: hidden; bottom: 1px; left: 1px; width: 18px; height: 4px; } /*表情按钮及弹出菜单*/ /*去除了表情的下拉箭头*/ .edui-default .edui-for-emotion .edui-icon { background-position: -60px -20px; } .edui-default .edui-for-emotion .edui-popup-content iframe { width: 514px; height: 380px; overflow: hidden; } .edui-default .edui-for-emotion .edui-popup-content { position: relative; z-index: 555 } .edui-default .edui-for-emotion .edui-splitborder { display: none } .edui-default .edui-for-emotion .edui-splitbutton-body .edui-arrow { width: 0 } .edui-default .edui-toolbar .edui-for-emotion .edui-state-active .edui-splitborder { border-left: 1px solid transparent; } /*contextmenu*/ .edui-default .edui-hassubmenu .edui-arrow { height: 20px; width: 20px; float: right; background: url("../images/icons-all.gif") no-repeat 10px -233px; } .edui-default .edui-menu-body .edui-menuitem { padding: 1px; } .edui-default .edui-menuseparator { margin: 2px 0; height: 1px; overflow: hidden; } .edui-default .edui-menuseparator-inner { border-bottom: 1px solid #e2e3e3; margin-left: 29px; margin-right: 1px; } .edui-default .edui-menu-body .edui-state-hover { padding: 0 !important; background-color: #fff5d4; border: 1px solid #dcac6c; } /*弹出菜单*/ .edui-default .edui-shortcutmenu { padding: 2px; width: 190px; height: 50px; background-color: #fff; border: 1px solid #ccc; border-radius: 5px; } /*粘贴弹出菜单*/ .edui-default .edui-wordpastepop .edui-popup-content{ border: none; padding: 0; width: 54px; height: 21px; } .edui-default .edui-pasteicon { width: 100%; height: 100%; background-image: url('../images/wordpaste.png'); background-position: 0 0; } .edui-default .edui-pasteicon.edui-state-opened { background-position: 0 -34px; } .edui-default .edui-pastecontainer { position: relative; visibility: hidden; width: 97px; background: #fff; border: 1px solid #ccc; } .edui-default .edui-pastecontainer .edui-title { font-weight: bold; background: #F8F8FF; height: 25px; line-height: 25px; font-size: 12px; padding-left: 5px; } .edui-default .edui-pastecontainer .edui-button { overflow: hidden; margin: 3px 0; } .edui-default .edui-pastecontainer .edui-button .edui-richtxticon, .edui-default .edui-pastecontainer .edui-button .edui-tagicon, .edui-default .edui-pastecontainer .edui-button .edui-plaintxticon{ float: left; cursor: pointer; width: 29px; height: 29px; margin-left: 5px; background-image: url('../images/wordpaste.png'); background-repeat: no-repeat; } .edui-default .edui-pastecontainer .edui-button .edui-richtxticon { margin-left: 0; background-position: -109px 0; } .edui-default .edui-pastecontainer .edui-button .edui-tagicon { background-position: -148px 1px; } .edui-default .edui-pastecontainer .edui-button .edui-plaintxticon { background-position: -72px 0; } .edui-default .edui-pastecontainer .edui-button .edui-state-hover .edui-richtxticon { background-position: -109px -34px; } .edui-default .edui-pastecontainer .edui-button .edui-state-hover .edui-tagicon{ background-position: -148px -34px; } .edui-default .edui-pastecontainer .edui-button .edui-state-hover .edui-plaintxticon{ background-position: -72px -34px; } ================================================ FILE: yshop-drink-vue3/public/UEditor/themes/default/dialogbase.css ================================================ /*弹出对话框页面样式组件 */ /*reset */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; outline: 0; font-size: 100%; } body { line-height: 1; } ol, ul { list-style: none; } blockquote, q { quotes: none; } ins { text-decoration: none; } del { text-decoration: line-through; } table { border-collapse: collapse; border-spacing: 0; } /*module */ body { background-color: #fff; font: 12px/1.5 sans-serif, "宋体", "Arial Narrow", HELVETICA; color: #646464; } /*tab*/ .tabhead { position: relative; z-index: 10; } .tabhead span { display: inline-block; padding: 0 5px; height: 30px; border: 1px solid #ccc; background: url("images/dialog-title-bg.png") repeat-x; text-align: center; line-height: 30px; cursor: pointer; *margin-right: 5px; } .tabhead span.focus { height: 31px; border-bottom: none; background: #fff; } .tabbody { position: relative; top: -1px; margin: 0 auto; border: 1px solid #ccc; } /*button*/ a.button { display: block; text-align: center; line-height: 24px; text-decoration: none; height: 24px; width: 95px; border: 0; color: #838383; background: url(../../themes/default/images/icons-all.gif) no-repeat; } a.button:hover { background-position: 0 -30px; } ================================================ FILE: yshop-drink-vue3/public/UEditor/themes/iframe.css ================================================ /*可以在这里添加你自己的css*/ ================================================ FILE: yshop-drink-vue3/public/UEditor/third-party/SyntaxHighlighter/shCore.js ================================================ // XRegExp 1.5.1 // (c) 2007-2012 Steven Levithan // MIT License // // Provides an augmented, extensible, cross-browser implementation of regular expressions, // including support for additional syntax, flags, and methods var XRegExp; if (XRegExp) { // Avoid running twice, since that would break references to native globals throw Error("can't load XRegExp twice in the same frame"); } // Run within an anonymous function to protect variables and avoid new globals (function (undefined) { //--------------------------------- // Constructor //--------------------------------- // Accepts a pattern and flags; returns a new, extended `RegExp` object. Differs from a native // regular expression in that additional syntax and flags are supported and cross-browser // syntax inconsistencies are ameliorated. `XRegExp(/regex/)` clones an existing regex and // converts to type XRegExp XRegExp = function (pattern, flags) { var output = [], currScope = XRegExp.OUTSIDE_CLASS, pos = 0, context, tokenResult, match, chr, regex; if (XRegExp.isRegExp(pattern)) { if (flags !== undefined) throw TypeError("can't supply flags when constructing one RegExp from another"); return clone(pattern); } // Tokens become part of the regex construction process, so protect against infinite // recursion when an XRegExp is constructed within a token handler or trigger if (isInsideConstructor) throw Error("can't call the XRegExp constructor within token definition functions"); flags = flags || ""; context = { // `this` object for custom tokens hasNamedCapture: false, captureNames: [], hasFlag: function (flag) {return flags.indexOf(flag) > -1;}, setFlag: function (flag) {flags += flag;} }; while (pos < pattern.length) { // Check for custom tokens at the current position tokenResult = runTokens(pattern, pos, currScope, context); if (tokenResult) { output.push(tokenResult.output); pos += (tokenResult.match[0].length || 1); } else { // Check for native multicharacter metasequences (excluding character classes) at // the current position if (match = nativ.exec.call(nativeTokens[currScope], pattern.slice(pos))) { output.push(match[0]); pos += match[0].length; } else { chr = pattern.charAt(pos); if (chr === "[") currScope = XRegExp.INSIDE_CLASS; else if (chr === "]") currScope = XRegExp.OUTSIDE_CLASS; // Advance position one character output.push(chr); pos++; } } } regex = RegExp(output.join(""), nativ.replace.call(flags, flagClip, "")); regex._xregexp = { source: pattern, captureNames: context.hasNamedCapture ? context.captureNames : null }; return regex; }; //--------------------------------- // Public properties //--------------------------------- XRegExp.version = "1.5.1"; // Token scope bitflags XRegExp.INSIDE_CLASS = 1; XRegExp.OUTSIDE_CLASS = 2; //--------------------------------- // Private variables //--------------------------------- var replacementToken = /\$(?:(\d\d?|[$&`'])|{([$\w]+)})/g, flagClip = /[^gimy]+|([\s\S])(?=[\s\S]*\1)/g, // Nonnative and duplicate flags quantifier = /^(?:[?*+]|{\d+(?:,\d*)?})\??/, isInsideConstructor = false, tokens = [], // Copy native globals for reference ("native" is an ES3 reserved keyword) nativ = { exec: RegExp.prototype.exec, test: RegExp.prototype.test, match: String.prototype.match, replace: String.prototype.replace, split: String.prototype.split }, compliantExecNpcg = nativ.exec.call(/()??/, "")[1] === undefined, // check `exec` handling of nonparticipating capturing groups compliantLastIndexIncrement = function () { var x = /^/g; nativ.test.call(x, ""); return !x.lastIndex; }(), hasNativeY = RegExp.prototype.sticky !== undefined, nativeTokens = {}; // `nativeTokens` match native multicharacter metasequences only (including deprecated octals, // excluding character classes) nativeTokens[XRegExp.INSIDE_CLASS] = /^(?:\\(?:[0-3][0-7]{0,2}|[4-7][0-7]?|x[\dA-Fa-f]{2}|u[\dA-Fa-f]{4}|c[A-Za-z]|[\s\S]))/; nativeTokens[XRegExp.OUTSIDE_CLASS] = /^(?:\\(?:0(?:[0-3][0-7]{0,2}|[4-7][0-7]?)?|[1-9]\d*|x[\dA-Fa-f]{2}|u[\dA-Fa-f]{4}|c[A-Za-z]|[\s\S])|\(\?[:=!]|[?*+]\?|{\d+(?:,\d*)?}\??)/; //--------------------------------- // Public methods //--------------------------------- // Lets you extend or change XRegExp syntax and create custom flags. This is used internally by // the XRegExp library and can be used to create XRegExp plugins. This function is intended for // users with advanced knowledge of JavaScript's regular expression syntax and behavior. It can // be disabled by `XRegExp.freezeTokens` XRegExp.addToken = function (regex, handler, scope, trigger) { tokens.push({ pattern: clone(regex, "g" + (hasNativeY ? "y" : "")), handler: handler, scope: scope || XRegExp.OUTSIDE_CLASS, trigger: trigger || null }); }; // Accepts a pattern and flags; returns an extended `RegExp` object. If the pattern and flag // combination has previously been cached, the cached copy is returned; otherwise the newly // created regex is cached XRegExp.cache = function (pattern, flags) { var key = pattern + "/" + (flags || ""); return XRegExp.cache[key] || (XRegExp.cache[key] = XRegExp(pattern, flags)); }; // Accepts a `RegExp` instance; returns a copy with the `/g` flag set. The copy has a fresh // `lastIndex` (set to zero). If you want to copy a regex without forcing the `global` // property, use `XRegExp(regex)`. Do not use `RegExp(regex)` because it will not preserve // special properties required for named capture XRegExp.copyAsGlobal = function (regex) { return clone(regex, "g"); }; // Accepts a string; returns the string with regex metacharacters escaped. The returned string // can safely be used at any point within a regex to match the provided literal string. Escaped // characters are [ ] { } ( ) * + ? - . , \ ^ $ | # and whitespace XRegExp.escape = function (str) { return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); }; // Accepts a string to search, regex to search with, position to start the search within the // string (default: 0), and an optional Boolean indicating whether matches must start at-or- // after the position or at the specified position only. This function ignores the `lastIndex` // of the provided regex in its own handling, but updates the property for compatibility XRegExp.execAt = function (str, regex, pos, anchored) { var r2 = clone(regex, "g" + ((anchored && hasNativeY) ? "y" : "")), match; r2.lastIndex = pos = pos || 0; match = r2.exec(str); // Run the altered `exec` (required for `lastIndex` fix, etc.) if (anchored && match && match.index !== pos) match = null; if (regex.global) regex.lastIndex = match ? r2.lastIndex : 0; return match; }; // Breaks the unrestorable link to XRegExp's private list of tokens, thereby preventing // syntax and flag changes. Should be run after XRegExp and any plugins are loaded XRegExp.freezeTokens = function () { XRegExp.addToken = function () { throw Error("can't run addToken after freezeTokens"); }; }; // Accepts any value; returns a Boolean indicating whether the argument is a `RegExp` object. // Note that this is also `true` for regex literals and regexes created by the `XRegExp` // constructor. This works correctly for variables created in another frame, when `instanceof` // and `constructor` checks would fail to work as intended XRegExp.isRegExp = function (o) { return Object.prototype.toString.call(o) === "[object RegExp]"; }; // Executes `callback` once per match within `str`. Provides a simpler and cleaner way to // iterate over regex matches compared to the traditional approaches of subverting // `String.prototype.replace` or repeatedly calling `exec` within a `while` loop XRegExp.iterate = function (str, regex, callback, context) { var r2 = clone(regex, "g"), i = -1, match; while (match = r2.exec(str)) { // Run the altered `exec` (required for `lastIndex` fix, etc.) if (regex.global) regex.lastIndex = r2.lastIndex; // Doing this to follow expectations if `lastIndex` is checked within `callback` callback.call(context, match, ++i, str, regex); if (r2.lastIndex === match.index) r2.lastIndex++; } if (regex.global) regex.lastIndex = 0; }; // Accepts a string and an array of regexes; returns the result of using each successive regex // to search within the matches of the previous regex. The array of regexes can also contain // objects with `regex` and `backref` properties, in which case the named or numbered back- // references specified are passed forward to the next regex or returned. E.g.: // var xregexpImgFileNames = XRegExp.matchChain(html, [ // {regex: /]+)>/i, backref: 1}, // tag attributes // {regex: XRegExp('(?ix) \\s src=" (? [^"]+ )'), backref: "src"}, // src attribute values // {regex: XRegExp("^http://xregexp\\.com(/[^#?]+)", "i"), backref: 1}, // xregexp.com paths // /[^\/]+$/ // filenames (strip directory paths) // ]); XRegExp.matchChain = function (str, chain) { return function recurseChain (values, level) { var item = chain[level].regex ? chain[level] : {regex: chain[level]}, regex = clone(item.regex, "g"), matches = [], i; for (i = 0; i < values.length; i++) { XRegExp.iterate(values[i], regex, function (match) { matches.push(item.backref ? (match[item.backref] || "") : match[0]); }); } return ((level === chain.length - 1) || !matches.length) ? matches : recurseChain(matches, level + 1); }([str], 0); }; //--------------------------------- // New RegExp prototype methods //--------------------------------- // Accepts a context object and arguments array; returns the result of calling `exec` with the // first value in the arguments array. the context is ignored but is accepted for congruity // with `Function.prototype.apply` RegExp.prototype.apply = function (context, args) { return this.exec(args[0]); }; // Accepts a context object and string; returns the result of calling `exec` with the provided // string. the context is ignored but is accepted for congruity with `Function.prototype.call` RegExp.prototype.call = function (context, str) { return this.exec(str); }; //--------------------------------- // Overriden native methods //--------------------------------- // Adds named capture support (with backreferences returned as `result.name`), and fixes two // cross-browser issues per ES3: // - Captured values for nonparticipating capturing groups should be returned as `undefined`, // rather than the empty string. // - `lastIndex` should not be incremented after zero-length matches. RegExp.prototype.exec = function (str) { var match, name, r2, origLastIndex; if (!this.global) origLastIndex = this.lastIndex; match = nativ.exec.apply(this, arguments); if (match) { // Fix browsers whose `exec` methods don't consistently return `undefined` for // nonparticipating capturing groups if (!compliantExecNpcg && match.length > 1 && indexOf(match, "") > -1) { r2 = RegExp(this.source, nativ.replace.call(getNativeFlags(this), "g", "")); // Using `str.slice(match.index)` rather than `match[0]` in case lookahead allowed // matching due to characters outside the match nativ.replace.call((str + "").slice(match.index), r2, function () { for (var i = 1; i < arguments.length - 2; i++) { if (arguments[i] === undefined) match[i] = undefined; } }); } // Attach named capture properties if (this._xregexp && this._xregexp.captureNames) { for (var i = 1; i < match.length; i++) { name = this._xregexp.captureNames[i - 1]; if (name) match[name] = match[i]; } } // Fix browsers that increment `lastIndex` after zero-length matches if (!compliantLastIndexIncrement && this.global && !match[0].length && (this.lastIndex > match.index)) this.lastIndex--; } if (!this.global) this.lastIndex = origLastIndex; // Fix IE, Opera bug (last tested IE 9.0.5, Opera 11.61 on Windows) return match; }; // Fix browser bugs in native method RegExp.prototype.test = function (str) { // Use the native `exec` to skip some processing overhead, even though the altered // `exec` would take care of the `lastIndex` fixes var match, origLastIndex; if (!this.global) origLastIndex = this.lastIndex; match = nativ.exec.call(this, str); // Fix browsers that increment `lastIndex` after zero-length matches if (match && !compliantLastIndexIncrement && this.global && !match[0].length && (this.lastIndex > match.index)) this.lastIndex--; if (!this.global) this.lastIndex = origLastIndex; // Fix IE, Opera bug (last tested IE 9.0.5, Opera 11.61 on Windows) return !!match; }; // Adds named capture support and fixes browser bugs in native method String.prototype.match = function (regex) { if (!XRegExp.isRegExp(regex)) regex = RegExp(regex); // Native `RegExp` if (regex.global) { var result = nativ.match.apply(this, arguments); regex.lastIndex = 0; // Fix IE bug return result; } return regex.exec(this); // Run the altered `exec` }; // Adds support for `${n}` tokens for named and numbered backreferences in replacement text, // and provides named backreferences to replacement functions as `arguments[0].name`. Also // fixes cross-browser differences in replacement text syntax when performing a replacement // using a nonregex search value, and the value of replacement regexes' `lastIndex` property // during replacement iterations. Note that this doesn't support SpiderMonkey's proprietary // third (`flags`) parameter String.prototype.replace = function (search, replacement) { var isRegex = XRegExp.isRegExp(search), captureNames, result, str, origLastIndex; // There are too many combinations of search/replacement types/values and browser bugs that // preclude passing to native `replace`, so don't try //if (...) // return nativ.replace.apply(this, arguments); if (isRegex) { if (search._xregexp) captureNames = search._xregexp.captureNames; // Array or `null` if (!search.global) origLastIndex = search.lastIndex; } else { search = search + ""; // Type conversion } if (Object.prototype.toString.call(replacement) === "[object Function]") { result = nativ.replace.call(this + "", search, function () { if (captureNames) { // Change the `arguments[0]` string primitive to a String object which can store properties arguments[0] = new String(arguments[0]); // Store named backreferences on `arguments[0]` for (var i = 0; i < captureNames.length; i++) { if (captureNames[i]) arguments[0][captureNames[i]] = arguments[i + 1]; } } // Update `lastIndex` before calling `replacement` (fix browsers) if (isRegex && search.global) search.lastIndex = arguments[arguments.length - 2] + arguments[0].length; return replacement.apply(null, arguments); }); } else { str = this + ""; // Type conversion, so `args[args.length - 1]` will be a string (given nonstring `this`) result = nativ.replace.call(str, search, function () { var args = arguments; // Keep this function's `arguments` available through closure return nativ.replace.call(replacement + "", replacementToken, function ($0, $1, $2) { // Numbered backreference (without delimiters) or special variable if ($1) { switch ($1) { case "$": return "$"; case "&": return args[0]; case "`": return args[args.length - 1].slice(0, args[args.length - 2]); case "'": return args[args.length - 1].slice(args[args.length - 2] + args[0].length); // Numbered backreference default: // What does "$10" mean? // - Backreference 10, if 10 or more capturing groups exist // - Backreference 1 followed by "0", if 1-9 capturing groups exist // - Otherwise, it's the string "$10" // Also note: // - Backreferences cannot be more than two digits (enforced by `replacementToken`) // - "$01" is equivalent to "$1" if a capturing group exists, otherwise it's the string "$01" // - There is no "$0" token ("$&" is the entire match) var literalNumbers = ""; $1 = +$1; // Type conversion; drop leading zero if (!$1) // `$1` was "0" or "00" return $0; while ($1 > args.length - 3) { literalNumbers = String.prototype.slice.call($1, -1) + literalNumbers; $1 = Math.floor($1 / 10); // Drop the last digit } return ($1 ? args[$1] || "" : "$") + literalNumbers; } // Named backreference or delimited numbered backreference } else { // What does "${n}" mean? // - Backreference to numbered capture n. Two differences from "$n": // - n can be more than two digits // - Backreference 0 is allowed, and is the entire match // - Backreference to named capture n, if it exists and is not a number overridden by numbered capture // - Otherwise, it's the string "${n}" var n = +$2; // Type conversion; drop leading zeros if (n <= args.length - 3) return args[n]; n = captureNames ? indexOf(captureNames, $2) : -1; return n > -1 ? args[n + 1] : $0; } }); }); } if (isRegex) { if (search.global) search.lastIndex = 0; // Fix IE, Safari bug (last tested IE 9.0.5, Safari 5.1.2 on Windows) else search.lastIndex = origLastIndex; // Fix IE, Opera bug (last tested IE 9.0.5, Opera 11.61 on Windows) } return result; }; // A consistent cross-browser, ES3 compliant `split` String.prototype.split = function (s /* separator */, limit) { // If separator `s` is not a regex, use the native `split` if (!XRegExp.isRegExp(s)) return nativ.split.apply(this, arguments); var str = this + "", // Type conversion output = [], lastLastIndex = 0, match, lastLength; // Behavior for `limit`: if it's... // - `undefined`: No limit // - `NaN` or zero: Return an empty array // - A positive number: Use `Math.floor(limit)` // - A negative number: No limit // - Other: Type-convert, then use the above rules if (limit === undefined || +limit < 0) { limit = Infinity; } else { limit = Math.floor(+limit); if (!limit) return []; } // This is required if not `s.global`, and it avoids needing to set `s.lastIndex` to zero // and restore it to its original value when we're done using the regex s = XRegExp.copyAsGlobal(s); while (match = s.exec(str)) { // Run the altered `exec` (required for `lastIndex` fix, etc.) if (s.lastIndex > lastLastIndex) { output.push(str.slice(lastLastIndex, match.index)); if (match.length > 1 && match.index < str.length) Array.prototype.push.apply(output, match.slice(1)); lastLength = match[0].length; lastLastIndex = s.lastIndex; if (output.length >= limit) break; } if (s.lastIndex === match.index) s.lastIndex++; } if (lastLastIndex === str.length) { if (!nativ.test.call(s, "") || lastLength) output.push(""); } else { output.push(str.slice(lastLastIndex)); } return output.length > limit ? output.slice(0, limit) : output; }; //--------------------------------- // Private helper functions //--------------------------------- // Supporting function for `XRegExp`, `XRegExp.copyAsGlobal`, etc. Returns a copy of a `RegExp` // instance with a fresh `lastIndex` (set to zero), preserving properties required for named // capture. Also allows adding new flags in the process of copying the regex function clone (regex, additionalFlags) { if (!XRegExp.isRegExp(regex)) throw TypeError("type RegExp expected"); var x = regex._xregexp; regex = XRegExp(regex.source, getNativeFlags(regex) + (additionalFlags || "")); if (x) { regex._xregexp = { source: x.source, captureNames: x.captureNames ? x.captureNames.slice(0) : null }; } return regex; } function getNativeFlags (regex) { return (regex.global ? "g" : "") + (regex.ignoreCase ? "i" : "") + (regex.multiline ? "m" : "") + (regex.extended ? "x" : "") + // Proposed for ES4; included in AS3 (regex.sticky ? "y" : ""); } function runTokens (pattern, index, scope, context) { var i = tokens.length, result, match, t; // Protect against constructing XRegExps within token handler and trigger functions isInsideConstructor = true; // Must reset `isInsideConstructor`, even if a `trigger` or `handler` throws try { while (i--) { // Run in reverse order t = tokens[i]; if ((scope & t.scope) && (!t.trigger || t.trigger.call(context))) { t.pattern.lastIndex = index; match = t.pattern.exec(pattern); // Running the altered `exec` here allows use of named backreferences, etc. if (match && match.index === index) { result = { output: t.handler.call(context, match, scope), match: match }; break; } } } } catch (err) { throw err; } finally { isInsideConstructor = false; } return result; } function indexOf (array, item, from) { if (Array.prototype.indexOf) // Use the native array method if available return array.indexOf(item, from); for (var i = from || 0; i < array.length; i++) { if (array[i] === item) return i; } return -1; } //--------------------------------- // Built-in tokens //--------------------------------- // Augment XRegExp's regular expression syntax and flags. Note that when adding tokens, the // third (`scope`) argument defaults to `XRegExp.OUTSIDE_CLASS` // Comment pattern: (?# ) XRegExp.addToken( /\(\?#[^)]*\)/, function (match) { // Keep tokens separated unless the following token is a quantifier return nativ.test.call(quantifier, match.input.slice(match.index + match[0].length)) ? "" : "(?:)"; } ); // Capturing group (match the opening parenthesis only). // Required for support of named capturing groups XRegExp.addToken( /\((?!\?)/, function () { this.captureNames.push(null); return "("; } ); // Named capturing group (match the opening delimiter only): (? XRegExp.addToken( /\(\?<([$\w]+)>/, function (match) { this.captureNames.push(match[1]); this.hasNamedCapture = true; return "("; } ); // Named backreference: \k XRegExp.addToken( /\\k<([\w$]+)>/, function (match) { var index = indexOf(this.captureNames, match[1]); // Keep backreferences separate from subsequent literal numbers. Preserve back- // references to named groups that are undefined at this point as literal strings return index > -1 ? "\\" + (index + 1) + (isNaN(match.input.charAt(match.index + match[0].length)) ? "" : "(?:)") : match[0]; } ); // Empty character class: [] or [^] XRegExp.addToken( /\[\^?]/, function (match) { // For cross-browser compatibility with ES3, convert [] to \b\B and [^] to [\s\S]. // (?!) should work like \b\B, but is unreliable in Firefox return match[0] === "[]" ? "\\b\\B" : "[\\s\\S]"; } ); // Mode modifier at the start of the pattern only, with any combination of flags imsx: (?imsx) // Does not support x(?i), (?-i), (?i-m), (?i: ), (?i)(?m), etc. XRegExp.addToken( /^\(\?([imsx]+)\)/, function (match) { this.setFlag(match[1]); return ""; } ); // Whitespace and comments, in free-spacing (aka extended) mode only XRegExp.addToken( /(?:\s+|#.*)+/, function (match) { // Keep tokens separated unless the following token is a quantifier return nativ.test.call(quantifier, match.input.slice(match.index + match[0].length)) ? "" : "(?:)"; }, XRegExp.OUTSIDE_CLASS, function () {return this.hasFlag("x");} ); // Dot, in dotall (aka singleline) mode only XRegExp.addToken( /\./, function () {return "[\\s\\S]";}, XRegExp.OUTSIDE_CLASS, function () {return this.hasFlag("s");} ); //--------------------------------- // Backward compatibility //--------------------------------- // Uncomment the following block for compatibility with XRegExp 1.0-1.2: /* XRegExp.matchWithinChain = XRegExp.matchChain; RegExp.prototype.addFlags = function (s) {return clone(this, s);}; RegExp.prototype.execAll = function (s) {var r = []; XRegExp.iterate(s, this, function (m) {r.push(m);}); return r;}; RegExp.prototype.forEachExec = function (s, f, c) {return XRegExp.iterate(s, this, f, c);}; RegExp.prototype.validate = function (s) {var r = RegExp("^(?:" + this.source + ")$(?!\\s)", getNativeFlags(this)); if (this.global) this.lastIndex = 0; return s.search(r) === 0;}; */ })(); // // Begin anonymous function. This is used to contain local scope variables without polutting global scope. // if (typeof(SyntaxHighlighter) == 'undefined') var SyntaxHighlighter = function() { // CommonJS if (typeof(require) != 'undefined' && typeof(XRegExp) == 'undefined') { XRegExp = require('XRegExp').XRegExp; } // Shortcut object which will be assigned to the SyntaxHighlighter variable. // This is a shorthand for local reference in order to avoid long namespace // references to SyntaxHighlighter.whatever... var sh = { defaults : { /** Additional CSS class names to be added to highlighter elements. */ 'class-name' : '', /** First line number. */ 'first-line' : 1, /** * Pads line numbers. Possible values are: * * false - don't pad line numbers. * true - automaticaly pad numbers with minimum required number of leading zeroes. * [int] - length up to which pad line numbers. */ 'pad-line-numbers' : false, /** Lines to highlight. */ 'highlight' : false, /** Title to be displayed above the code block. */ 'title' : null, /** Enables or disables smart tabs. */ 'smart-tabs' : true, /** Gets or sets tab size. */ 'tab-size' : 4, /** Enables or disables gutter. */ 'gutter' : true, /** Enables or disables toolbar. */ 'toolbar' : true, /** Enables quick code copy and paste from double click. */ 'quick-code' : true, /** Forces code view to be collapsed. */ 'collapse' : false, /** Enables or disables automatic links. */ 'auto-links' : false, /** Gets or sets light mode. Equavalent to turning off gutter and toolbar. */ 'light' : false, 'unindent' : true, 'html-script' : false }, config : { space : ' ', /** Enables use of * * ``` */ findParent:function (node, filterFn, includeSelf) { if (node && !domUtils.isBody(node)) { node = includeSelf ? node : node.parentNode; while (node) { if (!filterFn || filterFn(node) || domUtils.isBody(node)) { return filterFn && !filterFn(node) && domUtils.isBody(node) ? null : node; } node = node.parentNode; } } return null; }, /** * 查找node的节点名为tagName的第一个祖先节点, 查找的起点是node节点的父节点。 * @method findParentByTagName * @param { Node } node 需要查找的节点对象 * @param { Array } tagNames 需要查找的父节点的名称数组 * @warning 查找的终点是到body节点为止 * @return { Node | NULL } 如果找到符合条件的节点, 则返回该节点, 否则返回NULL * @example * ```javascript * var node = UE.dom.domUtils.findParentByTagName( document.getElementsByTagName("div")[0], [ "BODY" ] ); * //output: BODY * console.log( node.tagName ); * ``` */ /** * 查找node的节点名为tagName的祖先节点, 如果includeSelf的值为true,则查找的起点是给定的节点node, * 否则, 起点是node的父节点。 * @method findParentByTagName * @param { Node } node 需要查找的节点对象 * @param { Array } tagNames 需要查找的父节点的名称数组 * @param { Boolean } includeSelf 查找过程是否包含node节点自身 * @warning 查找的终点是到body节点为止 * @return { Node | NULL } 如果找到符合条件的节点, 则返回该节点, 否则返回NULL * @example * ```javascript * var queryTarget = document.getElementsByTagName("div")[0]; * var node = UE.dom.domUtils.findParentByTagName( queryTarget, [ "DIV" ], true ); * //output: true * console.log( queryTarget === node ); * ``` */ findParentByTagName:function (node, tagNames, includeSelf, excludeFn) { tagNames = utils.listToMap(utils.isArray(tagNames) ? tagNames : [tagNames]); return domUtils.findParent(node, function (node) { return tagNames[node.tagName] && !(excludeFn && excludeFn(node)); }, includeSelf); }, /** * 查找节点node的祖先节点集合, 查找的起点是给定节点的父节点,结果集中不包含给定的节点。 * @method findParents * @param { Node } node 需要查找的节点对象 * @return { Array } 给定节点的祖先节点数组 * @grammar UE.dom.domUtils.findParents(node) => Array //返回一个祖先节点数组集合,不包含自身 * @grammar UE.dom.domUtils.findParents(node,includeSelf) => Array //返回一个祖先节点数组集合,includeSelf指定是否包含自身 * @grammar UE.dom.domUtils.findParents(node,includeSelf,filterFn) => Array //返回一个祖先节点数组集合,filterFn指定过滤条件,返回true的node将被选取 * @grammar UE.dom.domUtils.findParents(node,includeSelf,filterFn,closerFirst) => Array //返回一个祖先节点数组集合,closerFirst为true的话,node的直接父亲节点是数组的第0个 */ /** * 查找节点node的祖先节点集合, 如果includeSelf的值为true, * 则返回的结果集中允许出现当前给定的节点, 否则, 该节点不会出现在其结果集中。 * @method findParents * @param { Node } node 需要查找的节点对象 * @param { Boolean } includeSelf 查找的结果中是否允许包含当前查找的节点对象 * @return { Array } 给定节点的祖先节点数组 */ findParents:function (node, includeSelf, filterFn, closerFirst) { var parents = includeSelf && ( filterFn && filterFn(node) || !filterFn ) ? [node] : []; while (node = domUtils.findParent(node, filterFn)) { parents.push(node); } return closerFirst ? parents : parents.reverse(); }, /** * 在节点node后面插入新节点newNode * @method insertAfter * @param { Node } node 目标节点 * @param { Node } newNode 新插入的节点, 该节点将置于目标节点之后 * @return { Node } 新插入的节点 */ insertAfter:function (node, newNode) { return node.nextSibling ? node.parentNode.insertBefore(newNode, node.nextSibling): node.parentNode.appendChild(newNode); }, /** * 删除节点node及其下属的所有节点 * @method remove * @param { Node } node 需要删除的节点对象 * @return { Node } 返回刚删除的节点对象 * @example * ```html *
    *
    你好
    *
    * * ``` */ /** * 删除节点node,并根据keepChildren的值决定是否保留子节点 * @method remove * @param { Node } node 需要删除的节点对象 * @param { Boolean } keepChildren 是否需要保留子节点 * @return { Node } 返回刚删除的节点对象 * @example * ```html *
    *
    你好
    *
    * * ``` */ remove:function (node, keepChildren) { var parent = node.parentNode, child; if (parent) { if (keepChildren && node.hasChildNodes()) { while (child = node.firstChild) { parent.insertBefore(child, node); } } parent.removeChild(node); } return node; }, /** * 取得node节点的下一个兄弟节点, 如果该节点其后没有兄弟节点, 则递归查找其父节点之后的第一个兄弟节点, * 直到找到满足条件的节点或者递归到BODY节点之后才会结束。 * @method getNextDomNode * @param { Node } node 需要获取其后的兄弟节点的节点对象 * @return { Node | NULL } 如果找满足条件的节点, 则返回该节点, 否则返回NULL * @example * ```html * *
    * *
    * xxx * * * ``` * @example * ```html * *
    * * xxx *
    * xxx * * * ``` */ /** * 取得node节点的下一个兄弟节点, 如果startFromChild的值为ture,则先获取其子节点, * 如果有子节点则直接返回第一个子节点;如果没有子节点或者startFromChild的值为false, * 则执行getNextDomNode(Node node)的查找过程。 * @method getNextDomNode * @param { Node } node 需要获取其后的兄弟节点的节点对象 * @param { Boolean } startFromChild 查找过程是否从其子节点开始 * @return { Node | NULL } 如果找满足条件的节点, 则返回该节点, 否则返回NULL * @see UE.dom.domUtils.getNextDomNode(Node) */ getNextDomNode:function (node, startFromChild, filterFn, guard) { return getDomNode(node, 'firstChild', 'nextSibling', startFromChild, filterFn, guard); }, getPreDomNode:function (node, startFromChild, filterFn, guard) { return getDomNode(node, 'lastChild', 'previousSibling', startFromChild, filterFn, guard); }, /** * 检测节点node是否属是UEditor定义的bookmark节点 * @method isBookmarkNode * @private * @param { Node } node 需要检测的节点对象 * @return { Boolean } 是否是bookmark节点 * @example * ```html * * * ``` */ isBookmarkNode:function (node) { return node.nodeType == 1 && node.id && /^_baidu_bookmark_/i.test(node.id); }, /** * 获取节点node所属的window对象 * @method getWindow * @param { Node } node 节点对象 * @return { Window } 当前节点所属的window对象 * @example * ```javascript * //output: true * console.log( UE.dom.domUtils.getWindow( document.body ) === window ); * ``` */ getWindow:function (node) { var doc = node.ownerDocument || node; return doc.defaultView || doc.parentWindow; }, /** * 获取离nodeA与nodeB最近的公共的祖先节点 * @method getCommonAncestor * @param { Node } nodeA 第一个节点 * @param { Node } nodeB 第二个节点 * @remind 如果给定的两个节点是同一个节点, 将直接返回该节点。 * @return { Node | NULL } 如果未找到公共节点, 返回NULL, 否则返回最近的公共祖先节点。 * @example * ```javascript * var commonAncestor = UE.dom.domUtils.getCommonAncestor( document.body, document.body.firstChild ); * //output: true * console.log( commonAncestor.tagName.toLowerCase() === 'body' ); * ``` */ getCommonAncestor:function (nodeA, nodeB) { if (nodeA === nodeB) return nodeA; var parentsA = [nodeA] , parentsB = [nodeB], parent = nodeA, i = -1; while (parent = parent.parentNode) { if (parent === nodeB) { return parent; } parentsA.push(parent); } parent = nodeB; while (parent = parent.parentNode) { if (parent === nodeA) return parent; parentsB.push(parent); } parentsA.reverse(); parentsB.reverse(); while (i++, parentsA[i] === parentsB[i]) { } return i == 0 ? null : parentsA[i - 1]; }, /** * 清除node节点左右连续为空的兄弟inline节点 * @method clearEmptySibling * @param { Node } node 执行的节点对象, 如果该节点的左右连续的兄弟节点是空的inline节点, * 则这些兄弟节点将被删除 * @grammar UE.dom.domUtils.clearEmptySibling(node,ignoreNext) //ignoreNext指定是否忽略右边空节点 * @grammar UE.dom.domUtils.clearEmptySibling(node,ignoreNext,ignorePre) //ignorePre指定是否忽略左边空节点 * @example * ```html * *
    * * * * xxx * * * * ``` */ /** * 清除node节点左右连续为空的兄弟inline节点, 如果ignoreNext的值为true, * 则忽略对右边兄弟节点的操作。 * @method clearEmptySibling * @param { Node } node 执行的节点对象, 如果该节点的左右连续的兄弟节点是空的inline节点, * @param { Boolean } ignoreNext 是否忽略忽略对右边的兄弟节点的操作 * 则这些兄弟节点将被删除 * @see UE.dom.domUtils.clearEmptySibling(Node) */ /** * 清除node节点左右连续为空的兄弟inline节点, 如果ignoreNext的值为true, * 则忽略对右边兄弟节点的操作, 如果ignorePre的值为true,则忽略对左边兄弟节点的操作。 * @method clearEmptySibling * @param { Node } node 执行的节点对象, 如果该节点的左右连续的兄弟节点是空的inline节点, * @param { Boolean } ignoreNext 是否忽略忽略对右边的兄弟节点的操作 * @param { Boolean } ignorePre 是否忽略忽略对左边的兄弟节点的操作 * 则这些兄弟节点将被删除 * @see UE.dom.domUtils.clearEmptySibling(Node) */ clearEmptySibling:function (node, ignoreNext, ignorePre) { function clear(next, dir) { var tmpNode; while (next && !domUtils.isBookmarkNode(next) && (domUtils.isEmptyInlineElement(next) //这里不能把空格算进来会吧空格干掉,出现文字间的空格丢掉了 || !new RegExp('[^\t\n\r' + domUtils.fillChar + ']').test(next.nodeValue) )) { tmpNode = next[dir]; domUtils.remove(next); next = tmpNode; } } !ignoreNext && clear(node.nextSibling, 'nextSibling'); !ignorePre && clear(node.previousSibling, 'previousSibling'); }, /** * 将一个文本节点textNode拆分成两个文本节点,offset指定拆分位置 * @method split * @param { Node } textNode 需要拆分的文本节点对象 * @param { int } offset 需要拆分的位置, 位置计算从0开始 * @return { Node } 拆分后形成的新节点 * @example * ```html *
    abcdef
    * * ``` */ split:function (node, offset) { var doc = node.ownerDocument; if (browser.ie && offset == node.nodeValue.length) { var next = doc.createTextNode(''); return domUtils.insertAfter(node, next); } var retval = node.splitText(offset); //ie8下splitText不会跟新childNodes,我们手动触发他的更新 if (browser.ie8) { var tmpNode = doc.createTextNode(''); domUtils.insertAfter(retval, tmpNode); domUtils.remove(tmpNode); } return retval; }, /** * 检测文本节点textNode是否为空节点(包括空格、换行、占位符等字符) * @method isWhitespace * @param { Node } node 需要检测的节点对象 * @return { Boolean } 检测的节点是否为空 * @example * ```html *
    * *
    * * ``` */ isWhitespace:function (node) { return !new RegExp('[^ \t\n\r' + domUtils.fillChar + ']').test(node.nodeValue); }, /** * 获取元素element相对于viewport的位置坐标 * @method getXY * @param { Node } element 需要计算位置的节点对象 * @return { Object } 返回形如{x:left,y:top}的一个key-value映射对象, 其中键x代表水平偏移距离, * y代表垂直偏移距离。 * * @example * ```javascript * var location = UE.dom.domUtils.getXY( document.getElementById("test") ); * //output: test的坐标为: 12, 24 * console.log( 'test的坐标为: ', location.x, ',', location.y ); * ``` */ getXY:function (element) { var x = 0, y = 0; while (element.offsetParent) { y += element.offsetTop; x += element.offsetLeft; element = element.offsetParent; } return { 'x':x, 'y':y}; }, /** * 为元素element绑定原生DOM事件,type为事件类型,handler为处理函数 * @method on * @param { Node } element 需要绑定事件的节点对象 * @param { String } type 绑定的事件类型 * @param { Function } handler 事件处理器 * @example * ```javascript * UE.dom.domUtils.on(document.body,"click",function(e){ * //e为事件对象,this为被点击元素对戏那个 * }); * ``` */ /** * 为元素element绑定原生DOM事件,type为事件类型,handler为处理函数 * @method on * @param { Node } element 需要绑定事件的节点对象 * @param { Array } type 绑定的事件类型数组 * @param { Function } handler 事件处理器 * @example * ```javascript * UE.dom.domUtils.on(document.body,["click","mousedown"],function(evt){ * //evt为事件对象,this为被点击元素对象 * }); * ``` */ on:function (element, type, handler) { var types = utils.isArray(type) ? type : utils.trim(type).split(/\s+/), k = types.length; if (k) while (k--) { type = types[k]; if (element.addEventListener) { element.addEventListener(type, handler, false); } else { if (!handler._d) { handler._d = { els : [] }; } var key = type + handler.toString(),index = utils.indexOf(handler._d.els,element); if (!handler._d[key] || index == -1) { if(index == -1){ handler._d.els.push(element); } if(!handler._d[key]){ handler._d[key] = function (evt) { return handler.call(evt.srcElement, evt || window.event); }; } element.attachEvent('on' + type, handler._d[key]); } } } element = null; }, /** * 解除DOM事件绑定 * @method un * @param { Node } element 需要解除事件绑定的节点对象 * @param { String } type 需要接触绑定的事件类型 * @param { Function } handler 对应的事件处理器 * @example * ```javascript * UE.dom.domUtils.un(document.body,"click",function(evt){ * //evt为事件对象,this为被点击元素对象 * }); * ``` */ /** * 解除DOM事件绑定 * @method un * @param { Node } element 需要解除事件绑定的节点对象 * @param { Array } type 需要接触绑定的事件类型数组 * @param { Function } handler 对应的事件处理器 * @example * ```javascript * UE.dom.domUtils.un(document.body, ["click","mousedown"],function(evt){ * //evt为事件对象,this为被点击元素对象 * }); * ``` */ un:function (element, type, handler) { var types = utils.isArray(type) ? type : utils.trim(type).split(/\s+/), k = types.length; if (k) while (k--) { type = types[k]; if (element.removeEventListener) { element.removeEventListener(type, handler, false); } else { var key = type + handler.toString(); try{ element.detachEvent('on' + type, handler._d ? handler._d[key] : handler); }catch(e){} if (handler._d && handler._d[key]) { var index = utils.indexOf(handler._d.els,element); if(index!=-1){ handler._d.els.splice(index,1); } handler._d.els.length == 0 && delete handler._d[key]; } } } }, /** * 比较节点nodeA与节点nodeB是否具有相同的标签名、属性名以及属性值 * @method isSameElement * @param { Node } nodeA 需要比较的节点 * @param { Node } nodeB 需要比较的节点 * @return { Boolean } 两个节点是否具有相同的标签名、属性名以及属性值 * @example * ```html * ssss * bbbbb * ssss * bbbbb * * * ``` */ isSameElement:function (nodeA, nodeB) { if (nodeA.tagName != nodeB.tagName) { return false; } var thisAttrs = nodeA.attributes, otherAttrs = nodeB.attributes; if (!ie && thisAttrs.length != otherAttrs.length) { return false; } var attrA, attrB, al = 0, bl = 0; for (var i = 0; attrA = thisAttrs[i++];) { if (attrA.nodeName == 'style') { if (attrA.specified) { al++; } if (domUtils.isSameStyle(nodeA, nodeB)) { continue; } else { return false; } } if (ie) { if (attrA.specified) { al++; attrB = otherAttrs.getNamedItem(attrA.nodeName); } else { continue; } } else { attrB = nodeB.attributes[attrA.nodeName]; } if (!attrB.specified || attrA.nodeValue != attrB.nodeValue) { return false; } } // 有可能attrB的属性包含了attrA的属性之外还有自己的属性 if (ie) { for (i = 0; attrB = otherAttrs[i++];) { if (attrB.specified) { bl++; } } if (al != bl) { return false; } } return true; }, /** * 判断节点nodeA与节点nodeB的元素的style属性是否一致 * @method isSameStyle * @param { Node } nodeA 需要比较的节点 * @param { Node } nodeB 需要比较的节点 * @return { Boolean } 两个节点是否具有相同的style属性值 * @example * ```html * ssss * bbbbb * ssss * bbbbb * * * ``` */ isSameStyle:function (nodeA, nodeB) { var styleA = nodeA.style.cssText.replace(/( ?; ?)/g, ';').replace(/( ?: ?)/g, ':'), styleB = nodeB.style.cssText.replace(/( ?; ?)/g, ';').replace(/( ?: ?)/g, ':'); if (browser.opera) { styleA = nodeA.style; styleB = nodeB.style; if (styleA.length != styleB.length) return false; for (var p in styleA) { if (/^(\d+|csstext)$/i.test(p)) { continue; } if (styleA[p] != styleB[p]) { return false; } } return true; } if (!styleA || !styleB) { return styleA == styleB; } styleA = styleA.split(';'); styleB = styleB.split(';'); if (styleA.length != styleB.length) { return false; } for (var i = 0, ci; ci = styleA[i++];) { if (utils.indexOf(styleB, ci) == -1) { return false; } } return true; }, /** * 检查节点node是否为block元素 * @method isBlockElm * @param { Node } node 需要检测的节点对象 * @return { Boolean } 是否是block元素节点 * @warning 该方法的判断规则如下: 如果该元素原本是block元素, 则不论该元素当前的css样式是什么都会返回true; * 否则,检测该元素的css样式, 如果该元素当前是block元素, 则返回true。 其余情况下都返回false。 * @example * ```html * * *
    * * * ``` */ isBlockElm:function (node) { return node.nodeType == 1 && (dtd.$block[node.tagName] || styleBlock[domUtils.getComputedStyle(node, 'display')]) && !dtd.$nonChild[node.tagName]; }, /** * 检测node节点是否为body节点 * @method isBody * @param { Element } node 需要检测的dom元素 * @return { Boolean } 给定的元素是否是body元素 * @example * ```javascript * //output: true * console.log( UE.dom.domUtils.isBody( document.body ) ); * ``` */ isBody:function (node) { return node && node.nodeType == 1 && node.tagName.toLowerCase() == 'body'; }, /** * 以node节点为分界,将该节点的指定祖先节点parent拆分成两个独立的节点, * 拆分形成的两个节点之间是node节点 * @method breakParent * @param { Node } node 作为分界的节点对象 * @param { Node } parent 该节点必须是node节点的祖先节点, 且是block节点。 * @return { Node } 给定的node分界节点 * @example * ```javascript * * var node = document.createElement("span"), * wrapNode = document.createElement( "div" ), * parent = document.createElement("p"); * * parent.appendChild( node ); * wrapNode.appendChild( parent ); * * //拆分前 * //output:

    * console.log( wrapNode.innerHTML ); * * * UE.dom.domUtils.breakParent( node, parent ); * //拆分后 * //output:

    * console.log( wrapNode.innerHTML ); * * ``` */ breakParent:function (node, parent) { var tmpNode, parentClone = node, clone = node, leftNodes, rightNodes; do { parentClone = parentClone.parentNode; if (leftNodes) { tmpNode = parentClone.cloneNode(false); tmpNode.appendChild(leftNodes); leftNodes = tmpNode; tmpNode = parentClone.cloneNode(false); tmpNode.appendChild(rightNodes); rightNodes = tmpNode; } else { leftNodes = parentClone.cloneNode(false); rightNodes = leftNodes.cloneNode(false); } while (tmpNode = clone.previousSibling) { leftNodes.insertBefore(tmpNode, leftNodes.firstChild); } while (tmpNode = clone.nextSibling) { rightNodes.appendChild(tmpNode); } clone = parentClone; } while (parent !== parentClone); tmpNode = parent.parentNode; tmpNode.insertBefore(leftNodes, parent); tmpNode.insertBefore(rightNodes, parent); tmpNode.insertBefore(node, rightNodes); domUtils.remove(parent); return node; }, /** * 检查节点node是否是空inline节点 * @method isEmptyInlineElement * @param { Node } node 需要检测的节点对象 * @return { Number } 如果给定的节点是空的inline节点, 则返回1, 否则返回0。 * @example * ```html * => 1 * => 1 * => 1 * xx => 0 * ``` */ isEmptyInlineElement:function (node) { if (node.nodeType != 1 || !dtd.$removeEmpty[ node.tagName ]) { return 0; } node = node.firstChild; while (node) { //如果是创建的bookmark就跳过 if (domUtils.isBookmarkNode(node)) { return 0; } if (node.nodeType == 1 && !domUtils.isEmptyInlineElement(node) || node.nodeType == 3 && !domUtils.isWhitespace(node) ) { return 0; } node = node.nextSibling; } return 1; }, /** * 删除node节点下首尾两端的空白文本子节点 * @method trimWhiteTextNode * @param { Element } node 需要执行删除操作的元素对象 * @example * ```javascript * var node = document.createElement("div"); * * node.appendChild( document.createTextNode( "" ) ); * * node.appendChild( document.createElement("div") ); * * node.appendChild( document.createTextNode( "" ) ); * * //3 * console.log( node.childNodes.length ); * * UE.dom.domUtils.trimWhiteTextNode( node ); * * //1 * console.log( node.childNodes.length ); * ``` */ trimWhiteTextNode:function (node) { function remove(dir) { var child; while ((child = node[dir]) && child.nodeType == 3 && domUtils.isWhitespace(child)) { node.removeChild(child); } } remove('firstChild'); remove('lastChild'); }, /** * 合并node节点下相同的子节点 * @name mergeChild * @desc * UE.dom.domUtils.mergeChild(node,tagName) //tagName要合并的子节点的标签 * @example *

    xxaaxx

    * ==> UE.dom.domUtils.mergeChild(node,'span') *

    xxaaxx

    */ mergeChild:function (node, tagName, attrs) { var list = domUtils.getElementsByTagName(node, node.tagName.toLowerCase()); for (var i = 0, ci; ci = list[i++];) { if (!ci.parentNode || domUtils.isBookmarkNode(ci)) { continue; } //span单独处理 if (ci.tagName.toLowerCase() == 'span') { if (node === ci.parentNode) { domUtils.trimWhiteTextNode(node); if (node.childNodes.length == 1) { node.style.cssText = ci.style.cssText + ";" + node.style.cssText; domUtils.remove(ci, true); continue; } } ci.style.cssText = node.style.cssText + ';' + ci.style.cssText; if (attrs) { var style = attrs.style; if (style) { style = style.split(';'); for (var j = 0, s; s = style[j++];) { ci.style[utils.cssStyleToDomStyle(s.split(':')[0])] = s.split(':')[1]; } } } if (domUtils.isSameStyle(ci, node)) { domUtils.remove(ci, true); } continue; } if (domUtils.isSameElement(node, ci)) { domUtils.remove(ci, true); } } }, /** * 原生方法getElementsByTagName的封装 * @method getElementsByTagName * @param { Node } node 目标节点对象 * @param { String } tagName 需要查找的节点的tagName, 多个tagName以空格分割 * @return { Array } 符合条件的节点集合 */ getElementsByTagName:function (node, name,filter) { if(filter && utils.isString(filter)){ var className = filter; filter = function(node){return domUtils.hasClass(node,className)} } name = utils.trim(name).replace(/[ ]{2,}/g,' ').split(' '); var arr = []; for(var n = 0,ni;ni=name[n++];){ var list = node.getElementsByTagName(ni); for (var i = 0, ci; ci = list[i++];) { if(!filter || filter(ci)) arr.push(ci); } } return arr; }, /** * 将节点node提取到父节点上 * @method mergeToParent * @param { Element } node 需要提取的元素对象 * @example * ```html *
    *
    * *
    *
    * * * ``` */ mergeToParent:function (node) { var parent = node.parentNode; while (parent && dtd.$removeEmpty[parent.tagName]) { if (parent.tagName == node.tagName || parent.tagName == 'A') {//针对a标签单独处理 domUtils.trimWhiteTextNode(parent); //span需要特殊处理 不处理这样的情况 xxxxxxxxx if (parent.tagName == 'SPAN' && !domUtils.isSameStyle(parent, node) || (parent.tagName == 'A' && node.tagName == 'SPAN')) { if (parent.childNodes.length > 1 || parent !== node.parentNode) { node.style.cssText = parent.style.cssText + ";" + node.style.cssText; parent = parent.parentNode; continue; } else { parent.style.cssText += ";" + node.style.cssText; //trace:952 a标签要保持下划线 if (parent.tagName == 'A') { parent.style.textDecoration = 'underline'; } } } if (parent.tagName != 'A') { parent === node.parentNode && domUtils.remove(node, true); break; } } parent = parent.parentNode; } }, /** * 合并节点node的左右兄弟节点 * @method mergeSibling * @param { Element } node 需要合并的目标节点 * @example * ```html * xxxxoooxxxx * * * ``` */ /** * 合并节点node的左右兄弟节点, 可以根据给定的条件选择是否忽略合并左节点。 * @method mergeSibling * @param { Element } node 需要合并的目标节点 * @param { Boolean } ignorePre 是否忽略合并左节点 * @example * ```html * xxxxoooxxxx * * * ``` */ /** * 合并节点node的左右兄弟节点,可以根据给定的条件选择是否忽略合并左右节点。 * @method mergeSibling * @param { Element } node 需要合并的目标节点 * @param { Boolean } ignorePre 是否忽略合并左节点 * @param { Boolean } ignoreNext 是否忽略合并右节点 * @remind 如果同时忽略左右节点, 则该操作什么也不会做 * @example * ```html * xxxxoooxxxx * * * ``` */ mergeSibling:function (node, ignorePre, ignoreNext) { function merge(rtl, start, node) { var next; if ((next = node[rtl]) && !domUtils.isBookmarkNode(next) && next.nodeType == 1 && domUtils.isSameElement(node, next)) { while (next.firstChild) { if (start == 'firstChild') { node.insertBefore(next.lastChild, node.firstChild); } else { node.appendChild(next.firstChild); } } domUtils.remove(next); } } !ignorePre && merge('previousSibling', 'firstChild', node); !ignoreNext && merge('nextSibling', 'lastChild', node); }, /** * 设置节点node及其子节点不会被选中 * @method unSelectable * @param { Element } node 需要执行操作的dom元素 * @remind 执行该操作后的节点, 将不能被鼠标选中 * @example * ```javascript * UE.dom.domUtils.unSelectable( document.body ); * ``` */ unSelectable:ie && browser.ie9below || browser.opera ? function (node) { //for ie9 node.onselectstart = function () { return false; }; node.onclick = node.onkeyup = node.onkeydown = function () { return false; }; node.unselectable = 'on'; node.setAttribute("unselectable", "on"); for (var i = 0, ci; ci = node.all[i++];) { switch (ci.tagName.toLowerCase()) { case 'iframe' : case 'textarea' : case 'input' : case 'select' : break; default : ci.unselectable = 'on'; node.setAttribute("unselectable", "on"); } } } : function (node) { node.style.MozUserSelect = node.style.webkitUserSelect = node.style.msUserSelect = node.style.KhtmlUserSelect = 'none'; }, /** * 删除节点node上的指定属性名称的属性 * @method removeAttributes * @param { Node } node 需要删除属性的节点对象 * @param { String } attrNames 可以是空格隔开的多个属性名称,该操作将会依次删除相应的属性 * @example * ```html *
    * xxxxx *
    * * * ``` */ /** * 删除节点node上的指定属性名称的属性 * @method removeAttributes * @param { Node } node 需要删除属性的节点对象 * @param { Array } attrNames 需要删除的属性名数组 * @example * ```html *
    * xxxxx *
    * * * ``` */ removeAttributes:function (node, attrNames) { attrNames = utils.isArray(attrNames) ? attrNames : utils.trim(attrNames).replace(/[ ]{2,}/g,' ').split(' '); for (var i = 0, ci; ci = attrNames[i++];) { ci = attrFix[ci] || ci; switch (ci) { case 'className': node[ci] = ''; break; case 'style': node.style.cssText = ''; var val = node.getAttributeNode('style'); !browser.ie && val && node.removeAttributeNode(val); } node.removeAttribute(ci); } }, /** * 在doc下创建一个标签名为tag,属性为attrs的元素 * @method createElement * @param { DomDocument } doc 新创建的元素属于该document节点创建 * @param { String } tagName 需要创建的元素的标签名 * @param { Object } attrs 新创建的元素的属性key-value集合 * @return { Element } 新创建的元素对象 * @example * ```javascript * var ele = UE.dom.domUtils.createElement( document, 'div', { * id: 'test' * } ); * * //output: DIV * console.log( ele.tagName ); * * //output: test * console.log( ele.id ); * * ``` */ createElement:function (doc, tag, attrs) { return domUtils.setAttributes(doc.createElement(tag), attrs) }, /** * 为节点node添加属性attrs,attrs为属性键值对 * @method setAttributes * @param { Element } node 需要设置属性的元素对象 * @param { Object } attrs 需要设置的属性名-值对 * @return { Element } 设置属性的元素对象 * @example * ```html * * * * */ setAttributes:function (node, attrs) { for (var attr in attrs) { if(attrs.hasOwnProperty(attr)){ var value = attrs[attr]; switch (attr) { case 'class': //ie下要这样赋值,setAttribute不起作用 node.className = value; break; case 'style' : node.style.cssText = node.style.cssText + ";" + value; break; case 'innerHTML': node[attr] = value; break; case 'value': node.value = value; break; default: node.setAttribute(attrFix[attr] || attr, value); } } } return node; }, /** * 获取元素element经过计算后的样式值 * @method getComputedStyle * @param { Element } element 需要获取样式的元素对象 * @param { String } styleName 需要获取的样式名 * @return { String } 获取到的样式值 * @example * ```html * * * * * * ``` */ getComputedStyle:function (element, styleName) { //一下的属性单独处理 var pros = 'width height top left'; if(pros.indexOf(styleName) > -1){ return element['offset' + styleName.replace(/^\w/,function(s){return s.toUpperCase()})] + 'px'; } //忽略文本节点 if (element.nodeType == 3) { element = element.parentNode; } //ie下font-size若body下定义了font-size,则从currentStyle里会取到这个font-size. 取不到实际值,故此修改. if (browser.ie && browser.version < 9 && styleName == 'font-size' && !element.style.fontSize && !dtd.$empty[element.tagName] && !dtd.$nonChild[element.tagName]) { var span = element.ownerDocument.createElement('span'); span.style.cssText = 'padding:0;border:0;font-family:simsun;'; span.innerHTML = '.'; element.appendChild(span); var result = span.offsetHeight; element.removeChild(span); span = null; return result + 'px'; } try { var value = domUtils.getStyle(element, styleName) || (window.getComputedStyle ? domUtils.getWindow(element).getComputedStyle(element, '').getPropertyValue(styleName) : ( element.currentStyle || element.style )[utils.cssStyleToDomStyle(styleName)]); } catch (e) { return ""; } return utils.transUnitToPx(utils.fixColor(styleName, value)); }, /** * 删除元素element指定的className * @method removeClasses * @param { Element } ele 需要删除class的元素节点 * @param { String } classNames 需要删除的className, 多个className之间以空格分开 * @example * ```html * xxx * * * ``` */ /** * 删除元素element指定的className * @method removeClasses * @param { Element } ele 需要删除class的元素节点 * @param { Array } classNames 需要删除的className数组 * @example * ```html * xxx * * * ``` */ removeClasses:function (elm, classNames) { classNames = utils.isArray(classNames) ? classNames : utils.trim(classNames).replace(/[ ]{2,}/g,' ').split(' '); for(var i = 0,ci,cls = elm.className;ci=classNames[i++];){ cls = cls.replace(new RegExp('\\b' + ci + '\\b'),'') } cls = utils.trim(cls).replace(/[ ]{2,}/g,' '); if(cls){ elm.className = cls; }else{ domUtils.removeAttributes(elm,['class']); } }, /** * 给元素element添加className * @method addClass * @param { Node } ele 需要增加className的元素 * @param { String } classNames 需要添加的className, 多个className之间以空格分割 * @remind 相同的类名不会被重复添加 * @example * ```html * * * * ``` */ /** * 判断元素element是否包含给定的样式类名className * @method hasClass * @param { Node } ele 需要检测的元素 * @param { Array } classNames 需要检测的className数组 * @return { Boolean } 元素是否包含所有给定的className * @example * ```html * * * * ``` */ hasClass:function (element, className) { if(utils.isRegExp(className)){ return className.test(element.className) } className = utils.trim(className).replace(/[ ]{2,}/g,' ').split(' '); for(var i = 0,ci,cls = element.className;ci=className[i++];){ if(!new RegExp('\\b' + ci + '\\b','i').test(cls)){ return false; } } return i - 1 == className.length; }, /** * 阻止事件默认行为 * @method preventDefault * @param { Event } evt 需要阻止默认行为的事件对象 * @example * ```javascript * UE.dom.domUtils.preventDefault( evt ); * ``` */ preventDefault:function (evt) { evt.preventDefault ? evt.preventDefault() : (evt.returnValue = false); }, /** * 删除元素element指定的样式 * @method removeStyle * @param { Element } element 需要删除样式的元素 * @param { String } styleName 需要删除的样式名 * @example * ```html * * * * ``` */ removeStyle:function (element, name) { if(browser.ie ){ //针对color先单独处理一下 if(name == 'color'){ name = '(^|;)' + name; } element.style.cssText = element.style.cssText.replace(new RegExp(name + '[^:]*:[^;]+;?','ig'),'') }else{ if (element.style.removeProperty) { element.style.removeProperty (name); }else { element.style.removeAttribute (utils.cssStyleToDomStyle(name)); } } if (!element.style.cssText) { domUtils.removeAttributes(element, ['style']); } }, /** * 获取元素element的style属性的指定值 * @method getStyle * @param { Element } element 需要获取属性值的元素 * @param { String } styleName 需要获取的style的名称 * @warning 该方法仅获取元素style属性中所标明的值 * @return { String } 该元素包含指定的style属性值 * @example * ```html *
    * * * ``` */ getStyle:function (element, name) { var value = element.style[ utils.cssStyleToDomStyle(name) ]; return utils.fixColor(name, value); }, /** * 为元素element设置样式属性值 * @method setStyle * @param { Element } element 需要设置样式的元素 * @param { String } styleName 样式名 * @param { String } styleValue 样式值 * @example * ```html *
    * * * ``` */ setStyle:function (element, name, value) { element.style[utils.cssStyleToDomStyle(name)] = value; if(!utils.trim(element.style.cssText)){ this.removeAttributes(element,'style') } }, /** * 为元素element设置多个样式属性值 * @method setStyles * @param { Element } element 需要设置样式的元素 * @param { Object } styles 样式名值对 * @example * ```html *
    * * * ``` */ setStyles:function (element, styles) { for (var name in styles) { if (styles.hasOwnProperty(name)) { domUtils.setStyle(element, name, styles[name]); } } }, /** * 删除_moz_dirty属性 * @private * @method removeDirtyAttr */ removeDirtyAttr:function (node) { for (var i = 0, ci, nodes = node.getElementsByTagName('*'); ci = nodes[i++];) { ci.removeAttribute('_moz_dirty'); } node.removeAttribute('_moz_dirty'); }, /** * 获取子节点的数量 * @method getChildCount * @param { Element } node 需要检测的元素 * @return { Number } 给定的node元素的子节点数量 * @example * ```html *
    * *
    * * * ``` */ /** * 根据给定的过滤规则, 获取符合条件的子节点的数量 * @method getChildCount * @param { Element } node 需要检测的元素 * @param { Function } fn 过滤器, 要求对符合条件的子节点返回true, 反之则要求返回false * @return { Number } 符合过滤条件的node元素的子节点数量 * @example * ```html *
    * *
    * * * ``` */ getChildCount:function (node, fn) { var count = 0, first = node.firstChild; fn = fn || function () { return 1; }; while (first) { if (fn(first)) { count++; } first = first.nextSibling; } return count; }, /** * 判断给定节点是否为空节点 * @method isEmptyNode * @param { Node } node 需要检测的节点对象 * @return { Boolean } 节点是否为空 * @example * ```javascript * UE.dom.domUtils.isEmptyNode( document.body ); * ``` */ isEmptyNode:function (node) { return !node.firstChild || domUtils.getChildCount(node, function (node) { return !domUtils.isBr(node) && !domUtils.isBookmarkNode(node) && !domUtils.isWhitespace(node) }) == 0 }, clearSelectedArr:function (nodes) { var node; while (node = nodes.pop()) { domUtils.removeAttributes(node, ['class']); } }, /** * 将显示区域滚动到指定节点的位置 * @method scrollToView * @param {Node} node 节点 * @param {window} win window对象 * @param {Number} offsetTop 距离上方的偏移量 */ scrollToView:function (node, win, offsetTop) { var getViewPaneSize = function () { var doc = win.document, mode = doc.compatMode == 'CSS1Compat'; return { width:( mode ? doc.documentElement.clientWidth : doc.body.clientWidth ) || 0, height:( mode ? doc.documentElement.clientHeight : doc.body.clientHeight ) || 0 }; }, getScrollPosition = function (win) { if ('pageXOffset' in win) { return { x:win.pageXOffset || 0, y:win.pageYOffset || 0 }; } else { var doc = win.document; return { x:doc.documentElement.scrollLeft || doc.body.scrollLeft || 0, y:doc.documentElement.scrollTop || doc.body.scrollTop || 0 }; } }; var winHeight = getViewPaneSize().height, offset = winHeight * -1 + offsetTop; offset += (node.offsetHeight || 0); var elementPosition = domUtils.getXY(node); offset += elementPosition.y; var currentScroll = getScrollPosition(win).y; // offset += 50; if (offset > currentScroll || offset < currentScroll - winHeight) { win.scrollTo(0, offset + (offset < 0 ? -20 : 20)); } }, /** * 判断给定节点是否为br * @method isBr * @param { Node } node 需要判断的节点对象 * @return { Boolean } 给定的节点是否是br节点 */ isBr:function (node) { return node.nodeType == 1 && node.tagName == 'BR'; }, /** * 判断给定的节点是否是一个“填充”节点 * @private * @method isFillChar * @param { Node } node 需要判断的节点 * @param { Boolean } isInStart 是否从节点内容的开始位置匹配 * @returns { Boolean } 节点是否是填充节点 */ isFillChar:function (node,isInStart) { if(node.nodeType != 3) return false; var text = node.nodeValue; if(isInStart){ return new RegExp('^' + domUtils.fillChar).test(text) } return !text.replace(new RegExp(domUtils.fillChar,'g'), '').length }, isStartInblock:function (range) { var tmpRange = range.cloneRange(), flag = 0, start = tmpRange.startContainer, tmp; if(start.nodeType == 1 && start.childNodes[tmpRange.startOffset]){ start = start.childNodes[tmpRange.startOffset]; var pre = start.previousSibling; while(pre && domUtils.isFillChar(pre)){ start = pre; pre = pre.previousSibling; } } if(this.isFillChar(start,true) && tmpRange.startOffset == 1){ tmpRange.setStartBefore(start); start = tmpRange.startContainer; } while (start && domUtils.isFillChar(start)) { tmp = start; start = start.previousSibling } if (tmp) { tmpRange.setStartBefore(tmp); start = tmpRange.startContainer; } if (start.nodeType == 1 && domUtils.isEmptyNode(start) && tmpRange.startOffset == 1) { tmpRange.setStart(start, 0).collapse(true); } while (!tmpRange.startOffset) { start = tmpRange.startContainer; if (domUtils.isBlockElm(start) || domUtils.isBody(start)) { flag = 1; break; } var pre = tmpRange.startContainer.previousSibling, tmpNode; if (!pre) { tmpRange.setStartBefore(tmpRange.startContainer); } else { while (pre && domUtils.isFillChar(pre)) { tmpNode = pre; pre = pre.previousSibling; } if (tmpNode) { tmpRange.setStartBefore(tmpNode); } else { tmpRange.setStartBefore(tmpRange.startContainer); } } } return flag && !domUtils.isBody(tmpRange.startContainer) ? 1 : 0; }, /** * 判断给定的元素是否是一个空元素 * @method isEmptyBlock * @param { Element } node 需要判断的元素 * @return { Boolean } 是否是空元素 * @example * ```html *
    * * * ``` */ /** * 根据指定的判断规则判断给定的元素是否是一个空元素 * @method isEmptyBlock * @param { Element } node 需要判断的元素 * @param { RegExp } reg 对内容执行判断的正则表达式对象 * @return { Boolean } 是否是空元素 */ isEmptyBlock:function (node,reg) { // HaoChuan9421 if(!node){ return; } if(node.nodeType != 1) return 0; reg = reg || new RegExp('[ \xa0\t\r\n' + domUtils.fillChar + ']', 'g'); if (node[browser.ie ? 'innerText' : 'textContent'].replace(reg, '').length > 0) { return 0; } for (var n in dtd.$isNotEmpty) { if (node.getElementsByTagName(n).length) { return 0; } } return 1; }, /** * 移动元素使得该元素的位置移动指定的偏移量的距离 * @method setViewportOffset * @param { Element } element 需要设置偏移量的元素 * @param { Object } offset 偏移量, 形如{ left: 100, top: 50 }的一个键值对, 表示该元素将在 * 现有的位置上向水平方向偏移offset.left的距离, 在竖直方向上偏移 * offset.top的距离 * @example * ```html *
    * * * ``` */ setViewportOffset:function (element, offset) { var left = parseInt(element.style.left) | 0; var top = parseInt(element.style.top) | 0; var rect = element.getBoundingClientRect(); var offsetLeft = offset.left - rect.left; var offsetTop = offset.top - rect.top; if (offsetLeft) { element.style.left = left + offsetLeft + 'px'; } if (offsetTop) { element.style.top = top + offsetTop + 'px'; } }, /** * 用“填充字符”填充节点 * @method fillNode * @private * @param { DomDocument } doc 填充的节点所在的docment对象 * @param { Node } node 需要填充的节点对象 * @example * ```html *
    * * * ``` */ fillNode:function (doc, node) { var tmpNode = browser.ie ? doc.createTextNode(domUtils.fillChar) : doc.createElement('br'); node.innerHTML = ''; node.appendChild(tmpNode); }, /** * 把节点src的所有子节点追加到另一个节点tag上去 * @method moveChild * @param { Node } src 源节点, 该节点下的所有子节点将被移除 * @param { Node } tag 目标节点, 从源节点移除的子节点将被追加到该节点下 * @example * ```html *
    * *
    *
    *
    *
    * * * ``` */ /** * 把节点src的所有子节点移动到另一个节点tag上去, 可以通过dir参数控制附加的行为是“追加”还是“插入顶部” * @method moveChild * @param { Node } src 源节点, 该节点下的所有子节点将被移除 * @param { Node } tag 目标节点, 从源节点移除的子节点将被附加到该节点下 * @param { Boolean } dir 附加方式, 如果为true, 则附加进去的节点将被放到目标节点的顶部, 反之,则放到末尾 * @example * ```html *
    * *
    *
    *
    *
    * * * ``` */ moveChild:function (src, tag, dir) { while (src.firstChild) { if (dir && tag.firstChild) { tag.insertBefore(src.lastChild, tag.firstChild); } else { tag.appendChild(src.firstChild); } } }, /** * 判断节点的标签上是否不存在任何属性 * @method hasNoAttributes * @private * @param { Node } node 需要检测的节点对象 * @return { Boolean } 节点是否不包含任何属性 * @example * ```html *
    xxxx
    * * * ``` */ hasNoAttributes:function (node) { return browser.ie ? /^<\w+\s*?>/.test(node.outerHTML) : node.attributes.length == 0; }, /** * 检测节点是否是UEditor所使用的辅助节点 * @method isCustomeNode * @private * @param { Node } node 需要检测的节点 * @remind 辅助节点是指编辑器要完成工作临时添加的节点, 在输出的时候将会从编辑器内移除, 不会影响最终的结果。 * @return { Boolean } 给定的节点是否是一个辅助节点 */ isCustomeNode:function (node) { return node.nodeType == 1 && node.getAttribute('_ue_custom_node_'); }, /** * 检测节点的标签是否是给定的标签 * @method isTagNode * @param { Node } node 需要检测的节点对象 * @param { String } tagName 标签 * @return { Boolean } 节点的标签是否是给定的标签 * @example * ```html *
    * * * ``` */ isTagNode:function (node, tagNames) { return node.nodeType == 1 && new RegExp('\\b' + node.tagName + '\\b','i').test(tagNames) }, /** * 给定一个节点数组,在通过指定的过滤器过滤后, 获取其中满足过滤条件的第一个节点 * @method filterNodeList * @param { Array } nodeList 需要过滤的节点数组 * @param { Function } fn 过滤器, 对符合条件的节点, 执行结果返回true, 反之则返回false * @return { Node | NULL } 如果找到符合过滤条件的节点, 则返回该节点, 否则返回NULL * @example * ```javascript * var divNodes = document.getElementsByTagName("div"); * divNodes = [].slice.call( divNodes, 0 ); * * //output: null * console.log( UE.dom.domUtils.filterNodeList( divNodes, function ( node ) { * return node.tagName.toLowerCase() !== 'div'; * } ) ); * ``` */ /** * 给定一个节点数组nodeList和一组标签名tagNames, 获取其中能够匹配标签名的节点集合中的第一个节点 * @method filterNodeList * @param { Array } nodeList 需要过滤的节点数组 * @param { String } tagNames 需要匹配的标签名, 多个标签名之间用空格分割 * @return { Node | NULL } 如果找到标签名匹配的节点, 则返回该节点, 否则返回NULL * @example * ```javascript * var divNodes = document.getElementsByTagName("div"); * divNodes = [].slice.call( divNodes, 0 ); * * //output: null * console.log( UE.dom.domUtils.filterNodeList( divNodes, 'a span' ) ); * ``` */ /** * 给定一个节点数组,在通过指定的过滤器过滤后, 如果参数forAll为true, 则会返回所有满足过滤 * 条件的节点集合, 否则, 返回满足条件的节点集合中的第一个节点 * @method filterNodeList * @param { Array } nodeList 需要过滤的节点数组 * @param { Function } fn 过滤器, 对符合条件的节点, 执行结果返回true, 反之则返回false * @param { Boolean } forAll 是否返回整个节点数组, 如果该参数为false, 则返回节点集合中的第一个节点 * @return { Array | Node | NULL } 如果找到符合过滤条件的节点, 则根据参数forAll的值决定返回满足 * 过滤条件的节点数组或第一个节点, 否则返回NULL * @example * ```javascript * var divNodes = document.getElementsByTagName("div"); * divNodes = [].slice.call( divNodes, 0 ); * * //output: 3(假定有3个div) * console.log( divNodes.length ); * * var nodes = UE.dom.domUtils.filterNodeList( divNodes, function ( node ) { * return node.tagName.toLowerCase() === 'div'; * }, true ); * * //output: 3 * console.log( nodes.length ); * * var node = UE.dom.domUtils.filterNodeList( divNodes, function ( node ) { * return node.tagName.toLowerCase() === 'div'; * }, false ); * * //output: div * console.log( node.nodeName ); * ``` */ filterNodeList : function(nodelist,filter,forAll){ var results = []; if(!utils .isFunction(filter)){ var str = filter; filter = function(n){ return utils.indexOf(utils.isArray(str) ? str:str.split(' '), n.tagName.toLowerCase()) != -1 }; } utils.each(nodelist,function(n){ filter(n) && results.push(n) }); return results.length == 0 ? null : results.length == 1 || !forAll ? results[0] : results }, /** * 查询给定的range选区是否在给定的node节点内,且在该节点的最末尾 * @method isInNodeEndBoundary * @param { UE.dom.Range } rng 需要判断的range对象, 该对象的startContainer不能为NULL * @param node 需要检测的节点对象 * @return { Number } 如果给定的选取range对象是在node内部的最末端, 则返回1, 否则返回0 */ isInNodeEndBoundary : function (rng,node){ var start = rng.startContainer; if(start.nodeType == 3 && rng.startOffset != start.nodeValue.length){ return 0; } if(start.nodeType == 1 && rng.startOffset != start.childNodes.length){ return 0; } while(start !== node){ if(start.nextSibling){ return 0 }; start = start.parentNode; } return 1; }, isBoundaryNode : function (node,dir){ var tmp; while(!domUtils.isBody(node)){ tmp = node; node = node.parentNode; if(tmp !== node[dir]){ return false; } } return true; }, fillHtml : browser.ie11below ? ' ' : '
    ' }; var fillCharReg = new RegExp(domUtils.fillChar, 'g'); // core/Range.js /** * Range封装 * @file * @module UE.dom * @class Range * @since 1.2.6.1 */ /** * dom操作封装 * @unfile * @module UE.dom */ /** * Range实现类,本类是UEditor底层核心类,封装不同浏览器之间的Range操作。 * @unfile * @module UE.dom * @class Range */ (function () { var guid = 0, fillChar = domUtils.fillChar, fillData; /** * 更新range的collapse状态 * @param {Range} range range对象 */ function updateCollapse(range) { range.collapsed = range.startContainer && range.endContainer && range.startContainer === range.endContainer && range.startOffset == range.endOffset; } function selectOneNode(rng){ return !rng.collapsed && rng.startContainer.nodeType == 1 && rng.startContainer === rng.endContainer && rng.endOffset - rng.startOffset == 1 } function setEndPoint(toStart, node, offset, range) { //如果node是自闭合标签要处理 if (node.nodeType == 1 && (dtd.$empty[node.tagName] || dtd.$nonChild[node.tagName])) { offset = domUtils.getNodeIndex(node) + (toStart ? 0 : 1); node = node.parentNode; } if (toStart) { range.startContainer = node; range.startOffset = offset; if (!range.endContainer) { range.collapse(true); } } else { range.endContainer = node; range.endOffset = offset; if (!range.startContainer) { range.collapse(false); } } updateCollapse(range); return range; } function execContentsAction(range, action) { //调整边界 //range.includeBookmark(); var start = range.startContainer, end = range.endContainer, startOffset = range.startOffset, endOffset = range.endOffset, doc = range.document, frag = doc.createDocumentFragment(), tmpStart, tmpEnd; if (start.nodeType == 1) { start = start.childNodes[startOffset] || (tmpStart = start.appendChild(doc.createTextNode(''))); } if (end.nodeType == 1) { end = end.childNodes[endOffset] || (tmpEnd = end.appendChild(doc.createTextNode(''))); } if (start === end && start.nodeType == 3) { frag.appendChild(doc.createTextNode(start.substringData(startOffset, endOffset - startOffset))); //is not clone if (action) { start.deleteData(startOffset, endOffset - startOffset); range.collapse(true); } return frag; } var current, currentLevel, clone = frag, startParents = domUtils.findParents(start, true), endParents = domUtils.findParents(end, true); for (var i = 0; startParents[i] == endParents[i];) { i++; } for (var j = i, si; si = startParents[j]; j++) { current = si.nextSibling; if (si == start) { if (!tmpStart) { if (range.startContainer.nodeType == 3) { clone.appendChild(doc.createTextNode(start.nodeValue.slice(startOffset))); //is not clone if (action) { start.deleteData(startOffset, start.nodeValue.length - startOffset); } } else { clone.appendChild(!action ? start.cloneNode(true) : start); } } } else { currentLevel = si.cloneNode(false); clone.appendChild(currentLevel); } while (current) { if (current === end || current === endParents[j]) { break; } si = current.nextSibling; clone.appendChild(!action ? current.cloneNode(true) : current); current = si; } clone = currentLevel; } clone = frag; if (!startParents[i]) { clone.appendChild(startParents[i - 1].cloneNode(false)); clone = clone.firstChild; } for (var j = i, ei; ei = endParents[j]; j++) { current = ei.previousSibling; if (ei == end) { if (!tmpEnd && range.endContainer.nodeType == 3) { clone.appendChild(doc.createTextNode(end.substringData(0, endOffset))); //is not clone if (action) { end.deleteData(0, endOffset); } } } else { currentLevel = ei.cloneNode(false); clone.appendChild(currentLevel); } //如果两端同级,右边第一次已经被开始做了 if (j != i || !startParents[i]) { while (current) { if (current === start) { break; } ei = current.previousSibling; clone.insertBefore(!action ? current.cloneNode(true) : current, clone.firstChild); current = ei; } } clone = currentLevel; } if (action) { range.setStartBefore(!endParents[i] ? endParents[i - 1] : !startParents[i] ? startParents[i - 1] : endParents[i]).collapse(true); } tmpStart && domUtils.remove(tmpStart); tmpEnd && domUtils.remove(tmpEnd); return frag; } /** * 创建一个跟document绑定的空的Range实例 * @constructor * @param { Document } document 新建的选区所属的文档对象 */ /** * @property { Node } startContainer 当前Range的开始边界的容器节点, 可以是一个元素节点或者是文本节点 */ /** * @property { Node } startOffset 当前Range的开始边界容器节点的偏移量, 如果是元素节点, * 该值就是childNodes中的第几个节点, 如果是文本节点就是文本内容的第几个字符 */ /** * @property { Node } endContainer 当前Range的结束边界的容器节点, 可以是一个元素节点或者是文本节点 */ /** * @property { Node } endOffset 当前Range的结束边界容器节点的偏移量, 如果是元素节点, * 该值就是childNodes中的第几个节点, 如果是文本节点就是文本内容的第几个字符 */ /** * @property { Boolean } collapsed 当前Range是否闭合 * @default true * @remind Range是闭合的时候, startContainer === endContainer && startOffset === endOffset */ /** * @property { Document } document 当前Range所属的Document对象 * @remind 不同range的的document属性可以是不同的 */ var Range = dom.Range = function (document) { var me = this; me.startContainer = me.startOffset = me.endContainer = me.endOffset = null; me.document = document; me.collapsed = true; }; /** * 删除fillData * @param doc * @param excludeNode */ function removeFillData(doc, excludeNode) { try { if (fillData && domUtils.inDoc(fillData, doc)) { if (!fillData.nodeValue.replace(fillCharReg, '').length) { var tmpNode = fillData.parentNode; domUtils.remove(fillData); while (tmpNode && domUtils.isEmptyInlineElement(tmpNode) && //safari的contains有bug (browser.safari ? !(domUtils.getPosition(tmpNode,excludeNode) & domUtils.POSITION_CONTAINS) : !tmpNode.contains(excludeNode)) ) { fillData = tmpNode.parentNode; domUtils.remove(tmpNode); tmpNode = fillData; } } else { fillData.nodeValue = fillData.nodeValue.replace(fillCharReg, ''); } } } catch (e) { } } /** * @param node * @param dir */ function mergeSibling(node, dir) { var tmpNode; node = node[dir]; while (node && domUtils.isFillChar(node)) { tmpNode = node[dir]; domUtils.remove(node); node = tmpNode; } } Range.prototype = { /** * 克隆选区的内容到一个DocumentFragment里 * @method cloneContents * @return { DocumentFragment | NULL } 如果选区是闭合的将返回null, 否则, 返回包含所clone内容的DocumentFragment元素 * @example * ```html * * * xx[xxx]x * * * * ``` */ cloneContents:function () { return this.collapsed ? null : execContentsAction(this, 0); }, /** * 删除当前选区范围中的所有内容 * @method deleteContents * @remind 执行完该操作后, 当前Range对象变成了闭合状态 * @return { UE.dom.Range } 当前操作的Range对象 * @example * ```html * * * xx[xxx]x * * * * ``` */ deleteContents:function () { var txt; if (!this.collapsed) { execContentsAction(this, 1); } if (browser.webkit) { txt = this.startContainer; if (txt.nodeType == 3 && !txt.nodeValue.length) { this.setStartBefore(txt).collapse(true); domUtils.remove(txt); } } return this; }, /** * 将当前选区的内容提取到一个DocumentFragment里 * @method extractContents * @remind 执行该操作后, 选区将变成闭合状态 * @warning 执行该操作后, 原来选区所选中的内容将从dom树上剥离出来 * @return { DocumentFragment } 返回包含所提取内容的DocumentFragment对象 * @example * ```html * * * xx[xxx]x * * * */ extractContents:function () { return this.collapsed ? null : execContentsAction(this, 2); }, /** * 设置Range的开始容器节点和偏移量 * @method setStart * @remind 如果给定的节点是元素节点,那么offset指的是其子元素中索引为offset的元素, * 如果是文本节点,那么offset指的是其文本内容的第offset个字符 * @remind 如果提供的容器节点是一个不能包含子元素的节点, 则该选区的开始容器将被设置 * 为该节点的父节点, 此时, 其距离开始容器的偏移量也变成了该节点在其父节点 * 中的索引 * @param { Node } node 将被设为当前选区开始边界容器的节点对象 * @param { int } offset 选区的开始位置偏移量 * @return { UE.dom.Range } 当前range对象 * @example * ```html * * xxxxxxxxxxxxx[xxx] * * * ``` * @example * ```html * * xxx[xx]x * * * ``` */ setStart:function (node, offset) { return setEndPoint(true, node, offset, this); }, /** * 设置Range的结束容器和偏移量 * @method setEnd * @param { Node } node 作为当前选区结束边界容器的节点对象 * @param { int } offset 结束边界的偏移量 * @see UE.dom.Range:setStart(Node,int) * @return { UE.dom.Range } 当前range对象 */ setEnd:function (node, offset) { return setEndPoint(false, node, offset, this); }, /** * 将Range开始位置设置到node节点之后 * @method setStartAfter * @remind 该操作将会把给定节点的父节点作为range的开始容器, 且偏移量是该节点在其父节点中的位置索引+1 * @param { Node } node 选区的开始边界将紧接着该节点之后 * @return { UE.dom.Range } 当前range对象 * @example * ```html * * xxxxxxx[xxxx] * * * ``` */ setStartAfter:function (node) { return this.setStart(node.parentNode, domUtils.getNodeIndex(node) + 1); }, /** * 将Range开始位置设置到node节点之前 * @method setStartBefore * @remind 该操作将会把给定节点的父节点作为range的开始容器, 且偏移量是该节点在其父节点中的位置索引 * @param { Node } node 新的选区开始位置在该节点之前 * @see UE.dom.Range:setStartAfter(Node) * @return { UE.dom.Range } 当前range对象 */ setStartBefore:function (node) { return this.setStart(node.parentNode, domUtils.getNodeIndex(node)); }, /** * 将Range结束位置设置到node节点之后 * @method setEndAfter * @remind 该操作将会把给定节点的父节点作为range的结束容器, 且偏移量是该节点在其父节点中的位置索引+1 * @param { Node } node 目标节点 * @see UE.dom.Range:setStartAfter(Node) * @return { UE.dom.Range } 当前range对象 * @example * ```html * * [xxxxxxx]xxxx * * * ``` */ setEndAfter:function (node) { return this.setEnd(node.parentNode, domUtils.getNodeIndex(node) + 1); }, /** * 将Range结束位置设置到node节点之前 * @method setEndBefore * @remind 该操作将会把给定节点的父节点作为range的结束容器, 且偏移量是该节点在其父节点中的位置索引 * @param { Node } node 目标节点 * @see UE.dom.Range:setEndAfter(Node) * @return { UE.dom.Range } 当前range对象 */ setEndBefore:function (node) { return this.setEnd(node.parentNode, domUtils.getNodeIndex(node)); }, /** * 设置Range的开始位置到node节点内的第一个子节点之前 * @method setStartAtFirst * @remind 选区的开始容器将变成给定的节点, 且偏移量为0 * @remind 如果给定的节点是元素节点, 则该节点必须是允许包含子节点的元素。 * @param { Node } node 目标节点 * @see UE.dom.Range:setStartBefore(Node) * @return { UE.dom.Range } 当前range对象 * @example * ```html * * xxxxx[xx]xxxx * * * ``` */ setStartAtFirst:function (node) { return this.setStart(node, 0); }, /** * 设置Range的开始位置到node节点内的最后一个节点之后 * @method setStartAtLast * @remind 选区的开始容器将变成给定的节点, 且偏移量为该节点的子节点数 * @remind 如果给定的节点是元素节点, 则该节点必须是允许包含子节点的元素。 * @param { Node } node 目标节点 * @see UE.dom.Range:setStartAtFirst(Node) * @return { UE.dom.Range } 当前range对象 */ setStartAtLast:function (node) { return this.setStart(node, node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length); }, /** * 设置Range的结束位置到node节点内的第一个节点之前 * @method setEndAtFirst * @param { Node } node 目标节点 * @remind 选区的结束容器将变成给定的节点, 且偏移量为0 * @remind node必须是一个元素节点, 且必须是允许包含子节点的元素。 * @see UE.dom.Range:setStartAtFirst(Node) * @return { UE.dom.Range } 当前range对象 */ setEndAtFirst:function (node) { return this.setEnd(node, 0); }, /** * 设置Range的结束位置到node节点内的最后一个节点之后 * @method setEndAtLast * @param { Node } node 目标节点 * @remind 选区的结束容器将变成给定的节点, 且偏移量为该节点的子节点数量 * @remind node必须是一个元素节点, 且必须是允许包含子节点的元素。 * @see UE.dom.Range:setStartAtFirst(Node) * @return { UE.dom.Range } 当前range对象 */ setEndAtLast:function (node) { return this.setEnd(node, node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length); }, /** * 选中给定节点 * @method selectNode * @remind 此时, 选区的开始容器和结束容器都是该节点的父节点, 其startOffset是该节点在父节点中的位置索引, * 而endOffset为startOffset+1 * @param { Node } node 需要选中的节点 * @return { UE.dom.Range } 当前range对象,此时的range仅包含当前给定的节点对象 * @example * ```html * * xxxxx[xx]xxxx * * * ``` */ selectNode:function (node) { return this.setStartBefore(node).setEndAfter(node); }, /** * 选中给定节点内部的所有节点 * @method selectNodeContents * @remind 此时, 选区的开始容器和结束容器都是该节点, 其startOffset为0, * 而endOffset是该节点的子节点数。 * @param { Node } node 目标节点, 当前range将包含该节点内的所有节点 * @return { UE.dom.Range } 当前range对象, 此时range仅包含给定节点的所有子节点 * @example * ```html * * xxxxx[xx]xxxx * * * ``` */ selectNodeContents:function (node) { return this.setStart(node, 0).setEndAtLast(node); }, /** * clone当前Range对象 * @method cloneRange * @remind 返回的range是一个全新的range对象, 其内部所有属性与当前被clone的range相同。 * @return { UE.dom.Range } 当前range对象的一个副本 */ cloneRange:function () { var me = this; return new Range(me.document).setStart(me.startContainer, me.startOffset).setEnd(me.endContainer, me.endOffset); }, /** * 向当前选区的结束处闭合选区 * @method collapse * @return { UE.dom.Range } 当前range对象 * @example * ```html * * xxxxx[xx]xxxx * * * ``` */ /** * 闭合当前选区,根据给定的toStart参数项决定是向当前选区开始处闭合还是向结束处闭合, * 如果toStart的值为true,则向开始位置闭合, 反之,向结束位置闭合。 * @method collapse * @param { Boolean } toStart 是否向选区开始处闭合 * @return { UE.dom.Range } 当前range对象,此时range对象处于闭合状态 * @see UE.dom.Range:collapse() * @example * ```html * * xxxxx[xx]xxxx * * * ``` */ collapse:function (toStart) { var me = this; if (toStart) { me.endContainer = me.startContainer; me.endOffset = me.startOffset; } else { me.startContainer = me.endContainer; me.startOffset = me.endOffset; } me.collapsed = true; return me; }, /** * 调整range的开始位置和结束位置,使其"收缩"到最小的位置 * @method shrinkBoundary * @return { UE.dom.Range } 当前range对象 * @example * ```html * xxxx[xxxxx] => xxxx[xxxxx] * ``` * * @example * ```html * * x[xx]xxx * * * ``` * * @example * ```html * [xxxxxxxxxxx] => [xxxxxxxxxxx] * ``` */ /** * 调整range的开始位置和结束位置,使其"收缩"到最小的位置, * 如果ignoreEnd的值为true,则忽略对结束位置的调整 * @method shrinkBoundary * @param { Boolean } ignoreEnd 是否忽略对结束位置的调整 * @return { UE.dom.Range } 当前range对象 * @see UE.dom.domUtils.Range:shrinkBoundary() */ shrinkBoundary:function (ignoreEnd) { var me = this, child, collapsed = me.collapsed; function check(node){ return node.nodeType == 1 && !domUtils.isBookmarkNode(node) && !dtd.$empty[node.tagName] && !dtd.$nonChild[node.tagName] } while (me.startContainer.nodeType == 1 //是element && (child = me.startContainer.childNodes[me.startOffset]) //子节点也是element && check(child)) { me.setStart(child, 0); } if (collapsed) { return me.collapse(true); } if (!ignoreEnd) { while (me.endContainer.nodeType == 1//是element && me.endOffset > 0 //如果是空元素就退出 endOffset=0那么endOffst-1为负值,childNodes[endOffset]报错 && (child = me.endContainer.childNodes[me.endOffset - 1]) //子节点也是element && check(child)) { me.setEnd(child, child.childNodes.length); } } return me; }, /** * 获取离当前选区内包含的所有节点最近的公共祖先节点, * @method getCommonAncestor * @remind 返回的公共祖先节点一定不是range自身的容器节点, 但有可能是一个文本节点 * @return { Node } 当前range对象内所有节点的公共祖先节点 * @example * ```html * //选区示例 * xxxx[xxx]xxxxxx * * ``` */ /** * 获取当前选区所包含的所有节点的公共祖先节点, 可以根据给定的参数 includeSelf 决定获取到 * 的公共祖先节点是否可以是当前选区的startContainer或endContainer节点, 如果 includeSelf * 的取值为true, 则返回的节点可以是自身的容器节点, 否则, 则不能是容器节点 * @method getCommonAncestor * @param { Boolean } includeSelf 是否允许获取到的公共祖先节点是当前range对象的容器节点 * @return { Node } 当前range对象内所有节点的公共祖先节点 * @see UE.dom.Range:getCommonAncestor() * @example * ```html * * * * xxxxxxxxx[xxx]xxxxxxxx * * * * * ``` */ /** * 获取当前选区所包含的所有节点的公共祖先节点, 可以根据给定的参数 includeSelf 决定获取到 * 的公共祖先节点是否可以是当前选区的startContainer或endContainer节点, 如果 includeSelf * 的取值为true, 则返回的节点可以是自身的容器节点, 否则, 则不能是容器节点; 同时可以根据 * ignoreTextNode 参数的取值决定是否忽略类型为文本节点的祖先节点。 * @method getCommonAncestor * @param { Boolean } includeSelf 是否允许获取到的公共祖先节点是当前range对象的容器节点 * @param { Boolean } ignoreTextNode 获取祖先节点的过程中是否忽略类型为文本节点的祖先节点 * @return { Node } 当前range对象内所有节点的公共祖先节点 * @see UE.dom.Range:getCommonAncestor() * @see UE.dom.Range:getCommonAncestor(Boolean) * @example * ```html * * * * xxxxxxxx[x]xxxxxxxxxxx * * * * * ``` */ getCommonAncestor:function (includeSelf, ignoreTextNode) { var me = this, start = me.startContainer, end = me.endContainer; if (start === end) { if (includeSelf && selectOneNode(this)) { start = start.childNodes[me.startOffset]; if(start.nodeType == 1) return start; } //只有在上来就相等的情况下才会出现是文本的情况 return ignoreTextNode && start.nodeType == 3 ? start.parentNode : start; } return domUtils.getCommonAncestor(start, end); }, /** * 调整当前Range的开始和结束边界容器,如果是容器节点是文本节点,就调整到包含该文本节点的父节点上 * @method trimBoundary * @remind 该操作有可能会引起文本节点被切开 * @return { UE.dom.Range } 当前range对象 * @example * ```html * * //选区示例 * xxx[xxxxx]xxx * * * ``` */ /** * 调整当前Range的开始和结束边界容器,如果是容器节点是文本节点,就调整到包含该文本节点的父节点上, * 可以根据 ignoreEnd 参数的值决定是否调整对结束边界的调整 * @method trimBoundary * @param { Boolean } ignoreEnd 是否忽略对结束边界的调整 * @return { UE.dom.Range } 当前range对象 * @example * ```html * * //选区示例 * xxx[xxxxx]xxx * * * ``` */ trimBoundary:function (ignoreEnd) { this.txtToElmBoundary(); var start = this.startContainer, offset = this.startOffset, collapsed = this.collapsed, end = this.endContainer; if (start.nodeType == 3) { if (offset == 0) { this.setStartBefore(start); } else { if (offset >= start.nodeValue.length) { this.setStartAfter(start); } else { var textNode = domUtils.split(start, offset); //跟新结束边界 if (start === end) { this.setEnd(textNode, this.endOffset - offset); } else if (start.parentNode === end) { this.endOffset += 1; } this.setStartBefore(textNode); } } if (collapsed) { return this.collapse(true); } } if (!ignoreEnd) { offset = this.endOffset; end = this.endContainer; if (end.nodeType == 3) { if (offset == 0) { this.setEndBefore(end); } else { offset < end.nodeValue.length && domUtils.split(end, offset); this.setEndAfter(end); } } } return this; }, /** * 如果选区在文本的边界上,就扩展选区到文本的父节点上, 如果当前选区是闭合的, 则什么也不做 * @method txtToElmBoundary * @remind 该操作不会修改dom节点 * @return { UE.dom.Range } 当前range对象 */ /** * 如果选区在文本的边界上,就扩展选区到文本的父节点上, 如果当前选区是闭合的, 则根据参数项 * ignoreCollapsed 的值决定是否执行该调整 * @method txtToElmBoundary * @param { Boolean } ignoreCollapsed 是否忽略选区的闭合状态, 如果该参数取值为true, 则 * 不论选区是否闭合, 都会执行该操作, 反之, 则不会对闭合的选区执行该操作 * @return { UE.dom.Range } 当前range对象 */ txtToElmBoundary:function (ignoreCollapsed) { function adjust(r, c) { var container = r[c + 'Container'], offset = r[c + 'Offset']; if (container.nodeType == 3) { if (!offset) { r['set' + c.replace(/(\w)/, function (a) { return a.toUpperCase(); }) + 'Before'](container); } else if (offset >= container.nodeValue.length) { r['set' + c.replace(/(\w)/, function (a) { return a.toUpperCase(); }) + 'After' ](container); } } } if (ignoreCollapsed || !this.collapsed) { adjust(this, 'start'); adjust(this, 'end'); } return this; }, /** * 在当前选区的开始位置前插入节点,新插入的节点会被该range包含 * @method insertNode * @param { Node } node 需要插入的节点 * @remind 插入的节点可以是一个DocumentFragment依次插入多个节点 * @return { UE.dom.Range } 当前range对象 */ insertNode:function (node) { var first = node, length = 1; if (node.nodeType == 11) { first = node.firstChild; length = node.childNodes.length; } this.trimBoundary(true); var start = this.startContainer, offset = this.startOffset; var nextNode = start.childNodes[ offset ]; if (nextNode) { start.insertBefore(node, nextNode); } else { start.appendChild(node); } if (first.parentNode === this.endContainer) { this.endOffset = this.endOffset + length; } return this.setStartBefore(first); }, /** * 闭合选区到当前选区的开始位置, 并且定位光标到闭合后的位置 * @method setCursor * @return { UE.dom.Range } 当前range对象 * @see UE.dom.Range:collapse() */ /** * 闭合选区,可以根据参数toEnd的值控制选区是向前闭合还是向后闭合, 并且定位光标到闭合后的位置。 * @method setCursor * @param { Boolean } toEnd 是否向后闭合, 如果为true, 则闭合选区时, 将向结束容器方向闭合, * 反之,则向开始容器方向闭合 * @return { UE.dom.Range } 当前range对象 * @see UE.dom.Range:collapse(Boolean) */ setCursor:function (toEnd, noFillData) { return this.collapse(!toEnd).select(noFillData); }, /** * 创建当前range的一个书签,记录下当前range的位置,方便当dom树改变时,还能找回原来的选区位置 * @method createBookmark * @param { Boolean } serialize 控制返回的标记位置是对当前位置的引用还是ID,如果该值为true,则 * 返回标记位置的ID, 反之则返回标记位置节点的引用 * @return { Object } 返回一个书签记录键值对, 其包含的key有: start => 开始标记的ID或者引用, * end => 结束标记的ID或引用, id => 当前标记的类型, 如果为true,则表示 * 返回的记录的类型为ID, 反之则为引用 */ createBookmark:function (serialize, same) { var endNode, startNode = this.document.createElement('span'); startNode.style.cssText = 'display:none;line-height:0px;'; startNode.appendChild(this.document.createTextNode('\u200D')); startNode.id = '_baidu_bookmark_start_' + (same ? '' : guid++); if (!this.collapsed) { endNode = startNode.cloneNode(true); endNode.id = '_baidu_bookmark_end_' + (same ? '' : guid++); } this.insertNode(startNode); if (endNode) { this.collapse().insertNode(endNode).setEndBefore(endNode); } this.setStartAfter(startNode); return { start:serialize ? startNode.id : startNode, end:endNode ? serialize ? endNode.id : endNode : null, id:serialize } }, /** * 调整当前range的边界到书签位置,并删除该书签对象所标记的位置内的节点 * @method moveToBookmark * @param { BookMark } bookmark createBookmark所创建的标签对象 * @return { UE.dom.Range } 当前range对象 * @see UE.dom.Range:createBookmark(Boolean) */ moveToBookmark:function (bookmark) { var start = bookmark.id ? this.document.getElementById(bookmark.start) : bookmark.start, end = bookmark.end && bookmark.id ? this.document.getElementById(bookmark.end) : bookmark.end; this.setStartBefore(start); domUtils.remove(start); if (end) { this.setEndBefore(end); domUtils.remove(end); } else { this.collapse(true); } return this; }, /** * 调整range的边界,使其"放大"到最近的父节点 * @method enlarge * @remind 会引起选区的变化 * @return { UE.dom.Range } 当前range对象 */ /** * 调整range的边界,使其"放大"到最近的父节点,根据参数 toBlock 的取值, 可以 * 要求扩大之后的父节点是block节点 * @method enlarge * @param { Boolean } toBlock 是否要求扩大之后的父节点必须是block节点 * @return { UE.dom.Range } 当前range对象 */ enlarge:function (toBlock, stopFn) { var isBody = domUtils.isBody, pre, node, tmp = this.document.createTextNode(''); if (toBlock) { node = this.startContainer; if (node.nodeType == 1) { if (node.childNodes[this.startOffset]) { pre = node = node.childNodes[this.startOffset] } else { node.appendChild(tmp); pre = node = tmp; } } else { pre = node; } while (1) { if (domUtils.isBlockElm(node)) { node = pre; while ((pre = node.previousSibling) && !domUtils.isBlockElm(pre)) { node = pre; } this.setStartBefore(node); break; } pre = node; node = node.parentNode; } node = this.endContainer; if (node.nodeType == 1) { if (pre = node.childNodes[this.endOffset]) { node.insertBefore(tmp, pre); } else { node.appendChild(tmp); } pre = node = tmp; } else { pre = node; } while (1) { if (domUtils.isBlockElm(node)) { node = pre; while ((pre = node.nextSibling) && !domUtils.isBlockElm(pre)) { node = pre; } this.setEndAfter(node); break; } pre = node; node = node.parentNode; } if (tmp.parentNode === this.endContainer) { this.endOffset--; } domUtils.remove(tmp); } // 扩展边界到最大 if (!this.collapsed) { while (this.startOffset == 0) { if (stopFn && stopFn(this.startContainer)) { break; } if (isBody(this.startContainer)) { break; } this.setStartBefore(this.startContainer); } while (this.endOffset == (this.endContainer.nodeType == 1 ? this.endContainer.childNodes.length : this.endContainer.nodeValue.length)) { if (stopFn && stopFn(this.endContainer)) { break; } if (isBody(this.endContainer)) { break; } this.setEndAfter(this.endContainer); } } return this; }, enlargeToBlockElm:function(ignoreEnd){ while(!domUtils.isBlockElm(this.startContainer)){ this.setStartBefore(this.startContainer); } if(!ignoreEnd){ while(!domUtils.isBlockElm(this.endContainer)){ this.setEndAfter(this.endContainer); } } return this; }, /** * 调整Range的边界,使其"缩小"到最合适的位置 * @method adjustmentBoundary * @return { UE.dom.Range } 当前range对象 * @see UE.dom.Range:shrinkBoundary() */ adjustmentBoundary:function () { if (!this.collapsed) { while (!domUtils.isBody(this.startContainer) && this.startOffset == this.startContainer[this.startContainer.nodeType == 3 ? 'nodeValue' : 'childNodes'].length && this.startContainer[this.startContainer.nodeType == 3 ? 'nodeValue' : 'childNodes'].length ) { this.setStartAfter(this.startContainer); } while (!domUtils.isBody(this.endContainer) && !this.endOffset && this.endContainer[this.endContainer.nodeType == 3 ? 'nodeValue' : 'childNodes'].length ) { this.setEndBefore(this.endContainer); } } return this; }, /** * 给range选区中的内容添加给定的inline标签 * @method applyInlineStyle * @param { String } tagName 需要添加的标签名 * @example * ```html *

    xxxx[xxxx]x

    ==> range.applyInlineStyle("strong") ==>

    xxxx[xxxx]x

    * ``` */ /** * 给range选区中的内容添加给定的inline标签, 并且为标签附加上一些初始化属性。 * @method applyInlineStyle * @param { String } tagName 需要添加的标签名 * @param { Object } attrs 跟随新添加的标签的属性 * @return { UE.dom.Range } 当前选区 * @example * ```html *

    xxxx[xxxx]x

    * * ==> * * * range.applyInlineStyle("strong",{"style":"font-size:12px"}) * * ==> * *

    xxxx[xxxx]x

    * ``` */ applyInlineStyle:function (tagName, attrs, list) { if (this.collapsed)return this; this.trimBoundary().enlarge(false, function (node) { return node.nodeType == 1 && domUtils.isBlockElm(node) }).adjustmentBoundary(); var bookmark = this.createBookmark(), end = bookmark.end, filterFn = function (node) { return node.nodeType == 1 ? node.tagName.toLowerCase() != 'br' : !domUtils.isWhitespace(node); }, current = domUtils.getNextDomNode(bookmark.start, false, filterFn), node, pre, range = this.cloneRange(); while (current && (domUtils.getPosition(current, end) & domUtils.POSITION_PRECEDING)) { if (current.nodeType == 3 || dtd[tagName][current.tagName]) { range.setStartBefore(current); node = current; while (node && (node.nodeType == 3 || dtd[tagName][node.tagName]) && node !== end) { pre = node; node = domUtils.getNextDomNode(node, node.nodeType == 1, null, function (parent) { return dtd[tagName][parent.tagName]; }); } var frag = range.setEndAfter(pre).extractContents(), elm; if (list && list.length > 0) { var level, top; top = level = list[0].cloneNode(false); for (var i = 1, ci; ci = list[i++];) { level.appendChild(ci.cloneNode(false)); level = level.firstChild; } elm = level; } else { elm = range.document.createElement(tagName); } if (attrs) { domUtils.setAttributes(elm, attrs); } elm.appendChild(frag); range.insertNode(list ? top : elm); //处理下滑线在a上的情况 var aNode; if (tagName == 'span' && attrs.style && /text\-decoration/.test(attrs.style) && (aNode = domUtils.findParentByTagName(elm, 'a', true))) { domUtils.setAttributes(aNode, attrs); domUtils.remove(elm, true); elm = aNode; } else { domUtils.mergeSibling(elm); domUtils.clearEmptySibling(elm); } //去除子节点相同的 domUtils.mergeChild(elm, attrs); current = domUtils.getNextDomNode(elm, false, filterFn); domUtils.mergeToParent(elm); if (node === end) { break; } } else { current = domUtils.getNextDomNode(current, true, filterFn); } } return this.moveToBookmark(bookmark); }, /** * 移除当前选区内指定的inline标签,但保留其中的内容 * @method removeInlineStyle * @param { String } tagName 需要移除的标签名 * @return { UE.dom.Range } 当前的range对象 * @example * ```html * xx[xxxxyyyzz]z => range.removeInlineStyle(["em"]) => xx[xxxxyyyzz]z * ``` */ /** * 移除当前选区内指定的一组inline标签,但保留其中的内容 * @method removeInlineStyle * @param { Array } tagNameArr 需要移除的标签名的数组 * @return { UE.dom.Range } 当前的range对象 * @see UE.dom.Range:removeInlineStyle(String) */ removeInlineStyle:function (tagNames) { if (this.collapsed)return this; tagNames = utils.isArray(tagNames) ? tagNames : [tagNames]; this.shrinkBoundary().adjustmentBoundary(); var start = this.startContainer, end = this.endContainer; while (1) { if (start.nodeType == 1) { if (utils.indexOf(tagNames, start.tagName.toLowerCase()) > -1) { break; } if (start.tagName.toLowerCase() == 'body') { start = null; break; } } start = start.parentNode; } while (1) { if (end.nodeType == 1) { if (utils.indexOf(tagNames, end.tagName.toLowerCase()) > -1) { break; } if (end.tagName.toLowerCase() == 'body') { end = null; break; } } end = end.parentNode; } var bookmark = this.createBookmark(), frag, tmpRange; if (start) { tmpRange = this.cloneRange().setEndBefore(bookmark.start).setStartBefore(start); frag = tmpRange.extractContents(); tmpRange.insertNode(frag); domUtils.clearEmptySibling(start, true); start.parentNode.insertBefore(bookmark.start, start); } if (end) { tmpRange = this.cloneRange().setStartAfter(bookmark.end).setEndAfter(end); frag = tmpRange.extractContents(); tmpRange.insertNode(frag); domUtils.clearEmptySibling(end, false, true); end.parentNode.insertBefore(bookmark.end, end.nextSibling); } var current = domUtils.getNextDomNode(bookmark.start, false, function (node) { return node.nodeType == 1; }), next; while (current && current !== bookmark.end) { next = domUtils.getNextDomNode(current, true, function (node) { return node.nodeType == 1; }); if (utils.indexOf(tagNames, current.tagName.toLowerCase()) > -1) { domUtils.remove(current, true); } current = next; } return this.moveToBookmark(bookmark); }, /** * 获取当前选中的自闭合的节点 * @method getClosedNode * @return { Node | NULL } 如果当前选中的是自闭合节点, 则返回该节点, 否则返回NULL */ getClosedNode:function () { var node; if (!this.collapsed) { var range = this.cloneRange().adjustmentBoundary().shrinkBoundary(); if (selectOneNode(range)) { var child = range.startContainer.childNodes[range.startOffset]; if (child && child.nodeType == 1 && (dtd.$empty[child.tagName] || dtd.$nonChild[child.tagName])) { node = child; } } } return node; }, /** * 在页面上高亮range所表示的选区 * @method select * @return { UE.dom.Range } 返回当前Range对象 */ //这里不区分ie9以上,trace:3824 select:browser.ie ? function (noFillData, textRange) { var nativeRange; if (!this.collapsed) this.shrinkBoundary(); var node = this.getClosedNode(); if (node && !textRange) { try { nativeRange = this.document.body.createControlRange(); nativeRange.addElement(node); nativeRange.select(); } catch (e) {} return this; } var bookmark = this.createBookmark(), start = bookmark.start, end; nativeRange = this.document.body.createTextRange(); nativeRange.moveToElementText(start); nativeRange.moveStart('character', 1); if (!this.collapsed) { var nativeRangeEnd = this.document.body.createTextRange(); end = bookmark.end; nativeRangeEnd.moveToElementText(end); nativeRange.setEndPoint('EndToEnd', nativeRangeEnd); } else { if (!noFillData && this.startContainer.nodeType != 3) { //使用|x固定住光标 var tmpText = this.document.createTextNode(fillChar), tmp = this.document.createElement('span'); tmp.appendChild(this.document.createTextNode(fillChar)); start.parentNode.insertBefore(tmp, start); start.parentNode.insertBefore(tmpText, start); //当点b,i,u时,不能清除i上边的b removeFillData(this.document, tmpText); fillData = tmpText; mergeSibling(tmp, 'previousSibling'); mergeSibling(start, 'nextSibling'); nativeRange.moveStart('character', -1); nativeRange.collapse(true); } } this.moveToBookmark(bookmark); tmp && domUtils.remove(tmp); //IE在隐藏状态下不支持range操作,catch一下 try { nativeRange.select(); } catch (e) { } return this; } : function (notInsertFillData) { function checkOffset(rng){ function check(node,offset,dir){ if(node.nodeType == 3 && node.nodeValue.length < offset){ rng[dir + 'Offset'] = node.nodeValue.length } } check(rng.startContainer,rng.startOffset,'start'); check(rng.endContainer,rng.endOffset,'end'); } var win = domUtils.getWindow(this.document), sel = win.getSelection(), txtNode; //FF下关闭自动长高时滚动条在关闭dialog时会跳 //ff下如果不body.focus将不能定位闭合光标到编辑器内 browser.gecko ? this.document.body.focus() : win.focus(); if (sel) { sel.removeAllRanges(); // trace:870 chrome/safari后边是br对于闭合得range不能定位 所以去掉了判断 // this.startContainer.nodeType != 3 &&! ((child = this.startContainer.childNodes[this.startOffset]) && child.nodeType == 1 && child.tagName == 'BR' if (this.collapsed && !notInsertFillData) { // //opear如果没有节点接着,原生的不能够定位,不能在body的第一级插入空白节点 // if (notInsertFillData && browser.opera && !domUtils.isBody(this.startContainer) && this.startContainer.nodeType == 1) { // var tmp = this.document.createTextNode(''); // this.insertNode(tmp).setStart(tmp, 0).collapse(true); // } // //处理光标落在文本节点的情况 //处理以下的情况 //|xxxx //xxxx|xxxx //xxxx| var start = this.startContainer,child = start; if(start.nodeType == 1){ child = start.childNodes[this.startOffset]; } if( !(start.nodeType == 3 && this.startOffset) && (child ? (!child.previousSibling || child.previousSibling.nodeType != 3) : (!start.lastChild || start.lastChild.nodeType != 3) ) ){ txtNode = this.document.createTextNode(fillChar); //跟着前边走 this.insertNode(txtNode); removeFillData(this.document, txtNode); mergeSibling(txtNode, 'previousSibling'); mergeSibling(txtNode, 'nextSibling'); fillData = txtNode; this.setStart(txtNode, browser.webkit ? 1 : 0).collapse(true); } } var nativeRange = this.document.createRange(); if(this.collapsed && browser.opera && this.startContainer.nodeType == 1){ var child = this.startContainer.childNodes[this.startOffset]; if(!child){ //往前靠拢 child = this.startContainer.lastChild; if( child && domUtils.isBr(child)){ this.setStartBefore(child).collapse(true); } }else{ //向后靠拢 while(child && domUtils.isBlockElm(child)){ if(child.nodeType == 1 && child.childNodes[0]){ child = child.childNodes[0] }else{ break; } } child && this.setStartBefore(child).collapse(true) } } //是createAddress最后一位算的不准,现在这里进行微调 checkOffset(this); nativeRange.setStart(this.startContainer, this.startOffset); nativeRange.setEnd(this.endContainer, this.endOffset); sel.addRange(nativeRange); } return this; }, /** * 滚动到当前range开始的位置 * @method scrollToView * @param { Window } win 当前range对象所属的window对象 * @return { UE.dom.Range } 当前Range对象 */ /** * 滚动到距离当前range开始位置 offset 的位置处 * @method scrollToView * @param { Window } win 当前range对象所属的window对象 * @param { Number } offset 距离range开始位置处的偏移量, 如果为正数, 则向下偏移, 反之, 则向上偏移 * @return { UE.dom.Range } 当前Range对象 */ scrollToView:function (win, offset) { win = win ? window : domUtils.getWindow(this.document); var me = this, span = me.document.createElement('span'); //trace:717 span.innerHTML = ' '; me.cloneRange().insertNode(span); domUtils.scrollToView(span, win, offset); domUtils.remove(span); return me; }, /** * 判断当前选区内容是否占位符 * @private * @method inFillChar * @return { Boolean } 如果是占位符返回true,否则返回false */ inFillChar : function(){ var start = this.startContainer; if(this.collapsed && start.nodeType == 3 && start.nodeValue.replace(new RegExp('^' + domUtils.fillChar),'').length + 1 == start.nodeValue.length ){ return true; } return false; }, /** * 保存 * @method createAddress * @private * @return { Boolean } 返回开始和结束的位置 * @example * ```html * *

    * aaaa * * * bbbb * * *

    * * * * ``` */ createAddress : function(ignoreEnd,ignoreTxt){ var addr = {},me = this; function getAddress(isStart){ var node = isStart ? me.startContainer : me.endContainer; var parents = domUtils.findParents(node,true,function(node){return !domUtils.isBody(node)}), addrs = []; for(var i = 0,ci;ci = parents[i++];){ addrs.push(domUtils.getNodeIndex(ci,ignoreTxt)); } var firstIndex = 0; if(ignoreTxt){ if(node.nodeType == 3){ var tmpNode = node.previousSibling; while(tmpNode && tmpNode.nodeType == 3){ firstIndex += tmpNode.nodeValue.replace(fillCharReg,'').length; tmpNode = tmpNode.previousSibling; } firstIndex += (isStart ? me.startOffset : me.endOffset)// - (fillCharReg.test(node.nodeValue) ? 1 : 0 ) }else{ node = node.childNodes[ isStart ? me.startOffset : me.endOffset]; if(node){ firstIndex = domUtils.getNodeIndex(node,ignoreTxt); }else{ node = isStart ? me.startContainer : me.endContainer; var first = node.firstChild; while(first){ if(domUtils.isFillChar(first)){ first = first.nextSibling; continue; } firstIndex++; if(first.nodeType == 3){ while( first && first.nodeType == 3){ first = first.nextSibling; } }else{ first = first.nextSibling; } } } } }else{ firstIndex = isStart ? domUtils.isFillChar(node) ? 0 : me.startOffset : me.endOffset } if(firstIndex < 0){ firstIndex = 0; } addrs.push(firstIndex); return addrs; } addr.startAddress = getAddress(true); if(!ignoreEnd){ addr.endAddress = me.collapsed ? [].concat(addr.startAddress) : getAddress(); } return addr; }, /** * 保存 * @method createAddress * @private * @return { Boolean } 返回开始和结束的位置 * @example * ```html * *

    * aaaa * * * bbbb * * *

    * * * * ``` */ moveToAddress : function(addr,ignoreEnd){ var me = this; function getNode(address,isStart){ var tmpNode = me.document.body, parentNode,offset; for(var i= 0,ci,l=address.length;i * * * * * * * * * ``` */ /** * 遍历range内的节点。 * 每当遍历一个节点时, 都会执行参数项 doFn 指定的函数, 该函数的接受当前遍历的节点 * 作为其参数。 * 可以通过参数项 filterFn 来指定一个过滤器, 只有符合该过滤器过滤规则的节点才会触 * 发doFn函数的执行 * @method traversal * @param { Function } doFn 对每个遍历的节点要执行的方法, 该方法接受当前遍历的节点作为其参数 * @param { Function } filterFn 过滤器, 该函数接受当前遍历的节点作为参数, 如果该节点满足过滤 * 规则, 请返回true, 该节点会触发doFn, 否则, 请返回false, 则该节点不 * 会触发doFn。 * @return { UE.dom.Range } 当前range对象 * @see UE.dom.Range:traversal(Function) * @example * ```html * * * * * * * * * * * ``` */ traversal:function(doFn,filterFn){ if (this.collapsed) return this; var bookmark = this.createBookmark(), end = bookmark.end, current = domUtils.getNextDomNode(bookmark.start, false, filterFn); while (current && current !== end && (domUtils.getPosition(current, end) & domUtils.POSITION_PRECEDING)) { var tmpNode = domUtils.getNextDomNode(current,false,filterFn); doFn(current); current = tmpNode; } return this.moveToBookmark(bookmark); } }; })(); // core/Selection.js /** * 选集 * @file * @module UE.dom * @class Selection * @since 1.2.6.1 */ /** * 选区集合 * @unfile * @module UE.dom * @class Selection */ (function () { function getBoundaryInformation( range, start ) { var getIndex = domUtils.getNodeIndex; range = range.duplicate(); range.collapse( start ); var parent = range.parentElement(); //如果节点里没有子节点,直接退出 if ( !parent.hasChildNodes() ) { return {container:parent, offset:0}; } var siblings = parent.children, child, testRange = range.duplicate(), startIndex = 0, endIndex = siblings.length - 1, index = -1, distance; while ( startIndex <= endIndex ) { index = Math.floor( (startIndex + endIndex) / 2 ); child = siblings[index]; testRange.moveToElementText( child ); var position = testRange.compareEndPoints( 'StartToStart', range ); if ( position > 0 ) { endIndex = index - 1; } else if ( position < 0 ) { startIndex = index + 1; } else { //trace:1043 return {container:parent, offset:getIndex( child )}; } } if ( index == -1 ) { testRange.moveToElementText( parent ); testRange.setEndPoint( 'StartToStart', range ); distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length; siblings = parent.childNodes; if ( !distance ) { child = siblings[siblings.length - 1]; return {container:child, offset:child.nodeValue.length}; } var i = siblings.length; while ( distance > 0 ){ distance -= siblings[ --i ].nodeValue.length; } return {container:siblings[i], offset:-distance}; } testRange.collapse( position > 0 ); testRange.setEndPoint( position > 0 ? 'StartToStart' : 'EndToStart', range ); distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length; if ( !distance ) { return dtd.$empty[child.tagName] || dtd.$nonChild[child.tagName] ? {container:parent, offset:getIndex( child ) + (position > 0 ? 0 : 1)} : {container:child, offset:position > 0 ? 0 : child.childNodes.length} } while ( distance > 0 ) { try { var pre = child; child = child[position > 0 ? 'previousSibling' : 'nextSibling']; distance -= child.nodeValue.length; } catch ( e ) { return {container:parent, offset:getIndex( pre )}; } } return {container:child, offset:position > 0 ? -distance : child.nodeValue.length + distance} } /** * 将ieRange转换为Range对象 * @param {Range} ieRange ieRange对象 * @param {Range} range Range对象 * @return {Range} range 返回转换后的Range对象 */ function transformIERangeToRange( ieRange, range ) { if ( ieRange.item ) { range.selectNode( ieRange.item( 0 ) ); } else { var bi = getBoundaryInformation( ieRange, true ); range.setStart( bi.container, bi.offset ); if ( ieRange.compareEndPoints( 'StartToEnd', ieRange ) != 0 ) { bi = getBoundaryInformation( ieRange, false ); range.setEnd( bi.container, bi.offset ); } } return range; } /** * 获得ieRange * @param {Selection} sel Selection对象 * @return {ieRange} 得到ieRange */ function _getIERange( sel ) { var ieRange; //ie下有可能报错 try { ieRange = sel.getNative().createRange(); } catch ( e ) { return null; } var el = ieRange.item ? ieRange.item( 0 ) : ieRange.parentElement(); if ( ( el.ownerDocument || el ) === sel.document ) { return ieRange; } return null; } var Selection = dom.Selection = function ( doc ) { var me = this, iframe; me.document = doc; if ( browser.ie9below ) { iframe = domUtils.getWindow( doc ).frameElement; domUtils.on( iframe, 'beforedeactivate', function () { me._bakIERange = me.getIERange(); } ); domUtils.on( iframe, 'activate', function () { try { if ( !_getIERange( me ) && me._bakIERange ) { me._bakIERange.select(); } } catch ( ex ) { } me._bakIERange = null; } ); } iframe = doc = null; }; Selection.prototype = { rangeInBody : function(rng,txtRange){ var node = browser.ie9below || txtRange ? rng.item ? rng.item() : rng.parentElement() : rng.startContainer; return node === this.document.body || domUtils.inDoc(node,this.document); }, /** * 获取原生seleciton对象 * @method getNative * @return { Object } 获得selection对象 * @example * ```javascript * editor.selection.getNative(); * ``` */ getNative:function () { var doc = this.document; try { return !doc ? null : browser.ie9below ? doc.selection : domUtils.getWindow( doc ).getSelection(); } catch ( e ) { return null; } }, /** * 获得ieRange * @method getIERange * @return { Object } 返回ie原生的Range * @example * ```javascript * editor.selection.getIERange(); * ``` */ getIERange:function () { var ieRange = _getIERange( this ); if ( !ieRange ) { if ( this._bakIERange ) { return this._bakIERange; } } return ieRange; }, /** * 缓存当前选区的range和选区的开始节点 * @method cache */ cache:function () { this.clear(); this._cachedRange = this.getRange(); this._cachedStartElement = this.getStart(); this._cachedStartElementPath = this.getStartElementPath(); }, /** * 获取选区开始位置的父节点到body * @method getStartElementPath * @return { Array } 返回父节点集合 * @example * ```javascript * editor.selection.getStartElementPath(); * ``` */ getStartElementPath:function () { if ( this._cachedStartElementPath ) { return this._cachedStartElementPath; } var start = this.getStart(); if ( start ) { return domUtils.findParents( start, true, null, true ) } return []; }, /** * 清空缓存 * @method clear */ clear:function () { this._cachedStartElementPath = this._cachedRange = this._cachedStartElement = null; }, /** * 编辑器是否得到了选区 * @method isFocus */ isFocus:function () { try { if(browser.ie9below){ var nativeRange = _getIERange(this); return !!(nativeRange && this.rangeInBody(nativeRange)); }else{ return !!this.getNative().rangeCount; } } catch ( e ) { return false; } }, /** * 获取选区对应的Range * @method getRange * @return { Object } 得到Range对象 * @example * ```javascript * editor.selection.getRange(); * ``` */ getRange:function () { var me = this; function optimze( range ) { var child = me.document.body.firstChild, collapsed = range.collapsed; while ( child && child.firstChild ) { range.setStart( child, 0 ); child = child.firstChild; } if ( !range.startContainer ) { range.setStart( me.document.body, 0 ) } if ( collapsed ) { range.collapse( true ); } } if ( me._cachedRange != null ) { return this._cachedRange; } var range = new baidu.editor.dom.Range( me.document ); if ( browser.ie9below ) { var nativeRange = me.getIERange(); if ( nativeRange ) { //备份的_bakIERange可能已经实效了,dom树发生了变化比如从源码模式切回来,所以try一下,实效就放到body开始位置 try{ transformIERangeToRange( nativeRange, range ); }catch(e){ optimze( range ); } } else { optimze( range ); } } else { var sel = me.getNative(); if ( sel && sel.rangeCount ) { var firstRange = sel.getRangeAt( 0 ); var lastRange = sel.getRangeAt( sel.rangeCount - 1 ); range.setStart( firstRange.startContainer, firstRange.startOffset ).setEnd( lastRange.endContainer, lastRange.endOffset ); if ( range.collapsed && domUtils.isBody( range.startContainer ) && !range.startOffset ) { optimze( range ); } } else { //trace:1734 有可能已经不在dom树上了,标识的节点 if ( this._bakRange && domUtils.inDoc( this._bakRange.startContainer, this.document ) ){ return this._bakRange; } optimze( range ); } } return this._bakRange = range; }, /** * 获取开始元素,用于状态反射 * @method getStart * @return { Element } 获得开始元素 * @example * ```javascript * editor.selection.getStart(); * ``` */ getStart:function () { if ( this._cachedStartElement ) { return this._cachedStartElement; } var range = browser.ie9below ? this.getIERange() : this.getRange(), tmpRange, start, tmp, parent; if ( browser.ie9below ) { if ( !range ) { //todo 给第一个值可能会有问题 return this.document.body.firstChild; } //control元素 if ( range.item ){ return range.item( 0 ); } tmpRange = range.duplicate(); //修正ie下x[xx] 闭合后 x|xx tmpRange.text.length > 0 && tmpRange.moveStart( 'character', 1 ); tmpRange.collapse( 1 ); start = tmpRange.parentElement(); parent = tmp = range.parentElement(); while ( tmp = tmp.parentNode ) { if ( tmp == start ) { start = parent; break; } } } else { range.shrinkBoundary(); start = range.startContainer; if ( start.nodeType == 1 && start.hasChildNodes() ){ start = start.childNodes[Math.min( start.childNodes.length - 1, range.startOffset )]; } if ( start.nodeType == 3 ){ return start.parentNode; } } return start; }, /** * 得到选区中的文本 * @method getText * @return { String } 选区中包含的文本 * @example * ```javascript * editor.selection.getText(); * ``` */ getText:function () { var nativeSel, nativeRange; if ( this.isFocus() && (nativeSel = this.getNative()) ) { nativeRange = browser.ie9below ? nativeSel.createRange() : nativeSel.getRangeAt( 0 ); return browser.ie9below ? nativeRange.text : nativeRange.toString(); } return ''; }, /** * 清除选区 * @method clearRange * @example * ```javascript * editor.selection.clearRange(); * ``` */ clearRange : function(){ this.getNative()[browser.ie9below ? 'empty' : 'removeAllRanges'](); } }; })(); // core/Editor.js /** * 编辑器主类,包含编辑器提供的大部分公用接口 * @file * @module UE * @class Editor * @since 1.2.6.1 */ /** * UEditor公用空间,UEditor所有的功能都挂载在该空间下 * @unfile * @module UE */ /** * UEditor的核心类,为用户提供与编辑器交互的接口。 * @unfile * @module UE * @class Editor */ (function () { var uid = 0, _selectionChangeTimer; /** * 获取编辑器的html内容,赋值到编辑器所在表单的textarea文本域里面 * @private * @method setValue * @param { UE.Editor } editor 编辑器事例 */ function setValue(form, editor) { var textarea; if (editor.textarea) { if (utils.isString(editor.textarea)) { for (var i = 0, ti, tis = domUtils.getElementsByTagName(form, 'textarea'); ti = tis[i++];) { if (ti.id == 'ueditor_textarea_' + editor.options.textarea) { textarea = ti; break; } } } else { textarea = editor.textarea; } } if (!textarea) { form.appendChild(textarea = domUtils.createElement(document, 'textarea', { 'name': editor.options.textarea, 'id': 'ueditor_textarea_' + editor.options.textarea, 'style': "display:none" })); //不要产生多个textarea editor.textarea = textarea; } !textarea.getAttribute('name') && textarea.setAttribute('name', editor.options.textarea ); textarea.value = editor.hasContents() ? (editor.options.allHtmlEnabled ? editor.getAllHtml() : editor.getContent(null, null, true)) : '' } function loadPlugins(me){ //初始化插件 for (var pi in UE.plugins) { UE.plugins[pi].call(me); } } function checkCurLang(I18N){ for(var lang in I18N){ return lang } } function langReadied(me){ me.langIsReady = true; me.fireEvent("langReady"); } /** * 编辑器准备就绪后会触发该事件 * @module UE * @class Editor * @event ready * @remind render方法执行完成之后,会触发该事件 * @remind * @example * ```javascript * editor.addListener( 'ready', function( editor ) { * editor.execCommand( 'focus' ); //编辑器家在完成后,让编辑器拿到焦点 * } ); * ``` */ /** * 执行destroy方法,会触发该事件 * @module UE * @class Editor * @event destroy * @see UE.Editor:destroy() */ /** * 执行reset方法,会触发该事件 * @module UE * @class Editor * @event reset * @see UE.Editor:reset() */ /** * 执行focus方法,会触发该事件 * @module UE * @class Editor * @event focus * @see UE.Editor:focus(Boolean) */ /** * 语言加载完成会触发该事件 * @module UE * @class Editor * @event langReady */ /** * 运行命令之后会触发该命令 * @module UE * @class Editor * @event beforeExecCommand */ /** * 运行命令之后会触发该命令 * @module UE * @class Editor * @event afterExecCommand */ /** * 运行命令之前会触发该命令 * @module UE * @class Editor * @event firstBeforeExecCommand */ /** * 在getContent方法执行之前会触发该事件 * @module UE * @class Editor * @event beforeGetContent * @see UE.Editor:getContent() */ /** * 在getContent方法执行之后会触发该事件 * @module UE * @class Editor * @event afterGetContent * @see UE.Editor:getContent() */ /** * 在getAllHtml方法执行时会触发该事件 * @module UE * @class Editor * @event getAllHtml * @see UE.Editor:getAllHtml() */ /** * 在setContent方法执行之前会触发该事件 * @module UE * @class Editor * @event beforeSetContent * @see UE.Editor:setContent(String) */ /** * 在setContent方法执行之后会触发该事件 * @module UE * @class Editor * @event afterSetContent * @see UE.Editor:setContent(String) */ /** * 每当编辑器内部选区发生改变时,将触发该事件 * @event selectionchange * @warning 该事件的触发非常频繁,不建议在该事件的处理过程中做重量级的处理 * @example * ```javascript * editor.addListener( 'selectionchange', function( editor ) { * console.log('选区发生改变'); * } */ /** * 在所有selectionchange的监听函数执行之前,会触发该事件 * @module UE * @class Editor * @event beforeSelectionChange * @see UE.Editor:selectionchange */ /** * 在所有selectionchange的监听函数执行完之后,会触发该事件 * @module UE * @class Editor * @event afterSelectionChange * @see UE.Editor:selectionchange */ /** * 编辑器内容发生改变时会触发该事件 * @module UE * @class Editor * @event contentChange */ /** * 以默认参数构建一个编辑器实例 * @constructor * @remind 通过 改构造方法实例化的编辑器,不带ui层.需要render到一个容器,编辑器实例才能正常渲染到页面 * @example * ```javascript * var editor = new UE.Editor(); * editor.execCommand('blod'); * ``` * @see UE.Config */ /** * 以给定的参数集合创建一个编辑器实例,对于未指定的参数,将应用默认参数。 * @constructor * @remind 通过 改构造方法实例化的编辑器,不带ui层.需要render到一个容器,编辑器实例才能正常渲染到页面 * @param { Object } setting 创建编辑器的参数 * @example * ```javascript * var editor = new UE.Editor(); * editor.execCommand('blod'); * ``` * @see UE.Config */ var Editor = UE.Editor = function (options) { var me = this; me.uid = uid++; EventBase.call(me); me.commands = {}; me.options = utils.extend(utils.clone(options || {}), UEDITOR_CONFIG, true); me.shortcutkeys = {}; me.inputRules = []; me.outputRules = []; //设置默认的常用属性 me.setOpt(Editor.defaultOptions(me)); /* 尝试异步加载后台配置 */ me.loadServerConfig(); if(!utils.isEmptyObject(UE.I18N)){ //修改默认的语言类型 me.options.lang = checkCurLang(UE.I18N); UE.plugin.load(me); langReadied(me); }else{ utils.loadFile(document, { src: me.options.langPath + me.options.lang + "/" + me.options.lang + ".js", tag: "script", type: "text/javascript", defer: "defer" }, function () { UE.plugin.load(me); langReadied(me); }); } UE.instants['ueditorInstant' + me.uid] = me; }; Editor.prototype = { registerCommand : function(name,obj){ this.commands[name] = obj; }, /** * 编辑器对外提供的监听ready事件的接口, 通过调用该方法,达到的效果与监听ready事件是一致的 * @method ready * @param { Function } fn 编辑器ready之后所执行的回调, 如果在注册事件之前编辑器已经ready,将会 * 立即触发该回调。 * @remind 需要等待编辑器加载完成后才能执行的代码,可以使用该方法传入 * @example * ```javascript * editor.ready( function( editor ) { * editor.setContent('初始化完毕'); * } ); * ``` * @see UE.Editor.event:ready */ ready: function (fn) { var me = this; if (fn) { me.isReady ? fn.apply(me) : me.addListener('ready', fn); } }, /** * 该方法是提供给插件里面使用,设置配置项默认值 * @method setOpt * @warning 三处设置配置项的优先级: 实例化时传入参数 > setOpt()设置 > config文件里设置 * @warning 该方法仅供编辑器插件内部和编辑器初始化时调用,其他地方不能调用。 * @param { String } key 编辑器的可接受的选项名称 * @param { * } val 该选项可接受的值 * @example * ```javascript * editor.setOpt( 'initContent', '欢迎使用编辑器' ); * ``` */ /** * 该方法是提供给插件里面使用,以{key:value}集合的方式设置插件内用到的配置项默认值 * @method setOpt * @warning 三处设置配置项的优先级: 实例化时传入参数 > setOpt()设置 > config文件里设置 * @warning 该方法仅供编辑器插件内部和编辑器初始化时调用,其他地方不能调用。 * @param { Object } options 将要设置的选项的键值对对象 * @example * ```javascript * editor.setOpt( { * 'initContent': '欢迎使用编辑器' * } ); * ``` */ setOpt: function (key, val) { var obj = {}; if (utils.isString(key)) { obj[key] = val } else { obj = key; } utils.extend(this.options, obj, true); }, getOpt:function(key){ return this.options[key] }, /** * 销毁编辑器实例,使用textarea代替 * @method destroy * @example * ```javascript * editor.destroy(); * ``` */ destroy: function () { var me = this; me.fireEvent('destroy'); var container = me.container.parentNode; var textarea = me.textarea; if (!textarea) { textarea = document.createElement('textarea'); container.parentNode.insertBefore(textarea, container); } else { textarea.style.display = '' } textarea.style.width = me.iframe.offsetWidth + 'px'; textarea.style.height = me.iframe.offsetHeight + 'px'; textarea.value = me.getContent(); textarea.id = me.key; container.innerHTML = ''; domUtils.remove(container); var key = me.key; //trace:2004 for (var p in me) { if (me.hasOwnProperty(p)) { delete this[p]; } } UE.delEditor(key); }, /** * 渲染编辑器的DOM到指定容器 * @method render * @param { String } containerId 指定一个容器ID * @remind 执行该方法,会触发ready事件 * @warning 必须且只能调用一次 */ /** * 渲染编辑器的DOM到指定容器 * @method render * @param { Element } containerDom 直接指定容器对象 * @remind 执行该方法,会触发ready事件 * @warning 必须且只能调用一次 */ render: function (container) { var me = this, options = me.options, getStyleValue=function(attr){ return parseInt(domUtils.getComputedStyle(container,attr)); }; if (utils.isString(container)) { container = document.getElementById(container); } if (container) { if(options.initialFrameWidth){ options.minFrameWidth = options.initialFrameWidth }else{ options.minFrameWidth = options.initialFrameWidth = container.offsetWidth; } if(options.initialFrameHeight){ options.minFrameHeight = options.initialFrameHeight }else{ options.initialFrameHeight = options.minFrameHeight = container.offsetHeight; } container.style.width = /%$/.test(options.initialFrameWidth) ? '100%' : options.initialFrameWidth- getStyleValue("padding-left")- getStyleValue("padding-right") +'px'; container.style.height = /%$/.test(options.initialFrameHeight) ? '100%' : options.initialFrameHeight - getStyleValue("padding-top")- getStyleValue("padding-bottom") +'px'; container.style.zIndex = options.zIndex; var html = ( ie && browser.version < 9 ? '' : '') + '' + '' + ( options.iframeCssUrl ? '' : '' ) + (options.initialStyle ? '' : '') + '' + ''; container.appendChild(domUtils.createElement(document, 'iframe', { id: 'ueditor_' + me.uid, width: "100%", height: "100%", frameborder: "0", //先注释掉了,加的原因忘记了,但开启会直接导致全屏模式下内容多时不会出现滚动条 // scrolling :'no', src: 'javascript:void(function(){document.open();' + (options.customDomain && document.domain != location.hostname ? 'document.domain="' + document.domain + '";' : '') + 'document.write("' + html + '");document.close();}())' })); container.style.overflow = 'hidden'; //解决如果是给定的百分比,会导致高度算不对的问题 setTimeout(function(){ if( /%$/.test(options.initialFrameWidth)){ options.minFrameWidth = options.initialFrameWidth = container.offsetWidth; //如果这里给定宽度,会导致ie在拖动窗口大小时,编辑区域不随着变化 // container.style.width = options.initialFrameWidth + 'px'; } if(/%$/.test(options.initialFrameHeight)){ options.minFrameHeight = options.initialFrameHeight = container.offsetHeight; container.style.height = options.initialFrameHeight + 'px'; } }) } }, /** * 编辑器初始化 * @method _setup * @private * @param { Element } doc 编辑器Iframe中的文档对象 */ _setup: function (doc) { var me = this, options = me.options; if (ie) { doc.body.disabled = true; doc.body.contentEditable = true; doc.body.disabled = false; } else { doc.body.contentEditable = true; } doc.body.spellcheck = false; me.document = doc; me.window = doc.defaultView || doc.parentWindow; me.iframe = me.window.frameElement; me.body = doc.body; me.selection = new dom.Selection(doc); //gecko初始化就能得到range,无法判断isFocus了 var geckoSel; if (browser.gecko && (geckoSel = this.selection.getNative())) { geckoSel.removeAllRanges(); } this._initEvents(); //为form提交提供一个隐藏的textarea for (var form = this.iframe.parentNode; !domUtils.isBody(form); form = form.parentNode) { if (form.tagName == 'FORM') { me.form = form; if(me.options.autoSyncData){ domUtils.on(me.window,'blur',function(){ setValue(form,me); }); }else{ domUtils.on(form, 'submit', function () { setValue(this, me); }); } break; } } if (options.initialContent) { if (options.autoClearinitialContent) { var oldExecCommand = me.execCommand; me.execCommand = function () { me.fireEvent('firstBeforeExecCommand'); return oldExecCommand.apply(me, arguments); }; this._setDefaultContent(options.initialContent); } else this.setContent(options.initialContent, false, true); } //编辑器不能为空内容 if (domUtils.isEmptyNode(me.body)) { me.body.innerHTML = '

    ' + (browser.ie ? '' : '
    ') + '

    '; } //如果要求focus, 就把光标定位到内容开始 if (options.focus) { setTimeout(function () { me.focus(me.options.focusInEnd); //如果自动清除开着,就不需要做selectionchange; !me.options.autoClearinitialContent && me._selectionChange(); }, 0); } if (!me.container) { me.container = this.iframe.parentNode; } if (options.fullscreen && me.ui) { me.ui.setFullScreen(true); } try { me.document.execCommand('2D-position', false, false); } catch (e) { } try { me.document.execCommand('enableInlineTableEditing', false, false); } catch (e) { } try { me.document.execCommand('enableObjectResizing', false, false); } catch (e) { } //挂接快捷键 me._bindshortcutKeys(); me.isReady = 1; me.fireEvent('ready'); options.onready && options.onready.call(me); if (!browser.ie9below) { domUtils.on(me.window, ['blur', 'focus'], function (e) { //chrome下会出现alt+tab切换时,导致选区位置不对 if (e.type == 'blur') { me._bakRange = me.selection.getRange(); try { me._bakNativeRange = me.selection.getNative().getRangeAt(0); me.selection.getNative().removeAllRanges(); } catch (e) { me._bakNativeRange = null; } } else { try { me._bakRange && me._bakRange.select(); } catch (e) { } } }); } //trace:1518 ff3.6body不够寛,会导致点击空白处无法获得焦点 if (browser.gecko && browser.version <= 10902) { //修复ff3.6初始化进来,不能点击获得焦点 me.body.contentEditable = false; setTimeout(function () { me.body.contentEditable = true; }, 100); setInterval(function () { me.body.style.height = me.iframe.offsetHeight - 20 + 'px' }, 100) } !options.isShow && me.setHide(); options.readonly && me.setDisabled(); }, /** * 同步数据到编辑器所在的form * 从编辑器的容器节点向上查找form元素,若找到,就同步编辑内容到找到的form里,为提交数据做准备,主要用于是手动提交的情况 * 后台取得数据的键值,使用你容器上的name属性,如果没有就使用参数里的textarea项 * @method sync * @example * ```javascript * editor.sync(); * form.sumbit(); //form变量已经指向了form元素 * ``` */ /** * 根据传入的formId,在页面上查找要同步数据的表单,若找到,就同步编辑内容到找到的form里,为提交数据做准备 * 后台取得数据的键值,该键值默认使用给定的编辑器容器的name属性,如果没有name属性则使用参数项里给定的“textarea”项 * @method sync * @param { String } formID 指定一个要同步数据的form的id,编辑器的数据会同步到你指定form下 */ sync: function (formId) { var me = this, form = formId ? document.getElementById(formId) : domUtils.findParent(me.iframe.parentNode, function (node) { return node.tagName == 'FORM' }, true); form && setValue(form, me); }, /** * 设置编辑器高度 * @method setHeight * @remind 当配置项autoHeightEnabled为真时,该方法无效 * @param { Number } number 设置的高度值,纯数值,不带单位 * @example * ```javascript * editor.setHeight(number); * ``` */ setHeight: function (height,notSetHeight) { if (height !== parseInt(this.iframe.parentNode.style.height)) { this.iframe.parentNode.style.height = height + 'px'; } !notSetHeight && (this.options.minFrameHeight = this.options.initialFrameHeight = height); this.body.style.height = height + 'px'; !notSetHeight && this.trigger('setHeight') }, /** * 为编辑器的编辑命令提供快捷键 * 这个接口是为插件扩展提供的接口,主要是为新添加的插件,如果需要添加快捷键,所提供的接口 * @method addshortcutkey * @param { Object } keyset 命令名和快捷键键值对对象,多个按钮的快捷键用“+”分隔 * @example * ```javascript * editor.addshortcutkey({ * "Bold" : "ctrl+66",//^B * "Italic" : "ctrl+73", //^I * }); * ``` */ /** * 这个接口是为插件扩展提供的接口,主要是为新添加的插件,如果需要添加快捷键,所提供的接口 * @method addshortcutkey * @param { String } cmd 触发快捷键时,响应的命令 * @param { String } keys 快捷键的字符串,多个按钮用“+”分隔 * @example * ```javascript * editor.addshortcutkey("Underline", "ctrl+85"); //^U * ``` */ addshortcutkey: function (cmd, keys) { var obj = {}; if (keys) { obj[cmd] = keys } else { obj = cmd; } utils.extend(this.shortcutkeys, obj) }, /** * 对编辑器设置keydown事件监听,绑定快捷键和命令,当快捷键组合触发成功,会响应对应的命令 * @method _bindshortcutKeys * @private */ _bindshortcutKeys: function () { var me = this, shortcutkeys = this.shortcutkeys; me.addListener('keydown', function (type, e) { var keyCode = e.keyCode || e.which; for (var i in shortcutkeys) { var tmp = shortcutkeys[i].split(','); for (var t = 0, ti; ti = tmp[t++];) { ti = ti.split(':'); var key = ti[0], param = ti[1]; if (/^(ctrl)(\+shift)?\+(\d+)$/.test(key.toLowerCase()) || /^(\d+)$/.test(key)) { if (( (RegExp.$1 == 'ctrl' ? (e.ctrlKey || e.metaKey) : 0) && (RegExp.$2 != "" ? e[RegExp.$2.slice(1) + "Key"] : 1) && keyCode == RegExp.$3 ) || keyCode == RegExp.$1 ) { if (me.queryCommandState(i,param) != -1) me.execCommand(i, param); domUtils.preventDefault(e); } } } } }); }, /** * 获取编辑器的内容 * @method getContent * @warning 该方法获取到的是经过编辑器内置的过滤规则进行过滤后得到的内容 * @return { String } 编辑器的内容字符串, 如果编辑器的内容为空,或者是空的标签内容(如:”<p><br/></p>“), 则返回空字符串 * @example * ```javascript * //编辑器html内容:

    123456

    * var content = editor.getContent(); //返回值:

    123456

    * ``` */ /** * 获取编辑器的内容。 可以通过参数定义编辑器内置的判空规则 * @method getContent * @param { Function } fn 自定的判空规则, 要求该方法返回一个boolean类型的值, * 代表当前编辑器的内容是否空, * 如果返回true, 则该方法将直接返回空字符串;如果返回false,则编辑器将返回 * 经过内置过滤规则处理后的内容。 * @remind 该方法在处理包含有初始化内容的时候能起到很好的作用。 * @warning 该方法获取到的是经过编辑器内置的过滤规则进行过滤后得到的内容 * @return { String } 编辑器的内容字符串 * @example * ```javascript * // editor 是一个编辑器的实例 * var content = editor.getContent( function ( editor ) { * return editor.body.innerHTML === '欢迎使用UEditor'; //返回空字符串 * } ); * ``` */ getContent: function (cmd, fn,notSetCursor,ignoreBlank,formatter) { var me = this; if (cmd && utils.isFunction(cmd)) { fn = cmd; cmd = ''; } if (fn ? !fn() : !this.hasContents()) { return ''; } me.fireEvent('beforegetcontent'); var root = UE.htmlparser(me.body.innerHTML,ignoreBlank); me.filterOutputRule(root); me.fireEvent('aftergetcontent', cmd,root); return root.toHtml(formatter); }, /** * 取得完整的html代码,可以直接显示成完整的html文档 * @method getAllHtml * @return { String } 编辑器的内容html文档字符串 * @eaxmple * ```javascript * editor.getAllHtml(); //返回格式大致是: ...... * ``` */ getAllHtml: function () { var me = this, headHtml = [], html = ''; me.fireEvent('getAllHtml', headHtml); if (browser.ie && browser.version > 8) { var headHtmlForIE9 = ''; utils.each(me.document.styleSheets, function (si) { headHtmlForIE9 += ( si.href ? '' : ''); }); utils.each(me.document.getElementsByTagName('script'), function (si) { headHtmlForIE9 += si.outerHTML; }); } return '' + (me.options.charset ? '' : '') + (headHtmlForIE9 || me.document.getElementsByTagName('head')[0].innerHTML) + headHtml.join('\n') + '' + '' + me.getContent(null, null, true) + ''; }, /** * 得到编辑器的纯文本内容,但会保留段落格式 * @method getPlainTxt * @return { String } 编辑器带段落格式的纯文本内容字符串 * @example * ```javascript * //编辑器html内容:

    1

    2

    * console.log(editor.getPlainTxt()); //输出:"1\n2\n * ``` */ getPlainTxt: function () { var reg = new RegExp(domUtils.fillChar, 'g'), html = this.body.innerHTML.replace(/[\n\r]/g, '');//ie要先去了\n在处理 html = html.replace(/<(p|div)[^>]*>(| )<\/\1>/gi, '\n') .replace(//gi, '\n') .replace(/<[^>/]+>/g, '') .replace(/(\n)?<\/([^>]+)>/g, function (a, b, c) { return dtd.$block[c] ? '\n' : b ? b : ''; }); //取出来的空格会有c2a0会变成乱码,处理这种情况\u00a0 return html.replace(reg, '').replace(/\u00a0/g, ' ').replace(/ /g, ' '); }, /** * 获取编辑器中的纯文本内容,没有段落格式 * @method getContentTxt * @return { String } 编辑器不带段落格式的纯文本内容字符串 * @example * ```javascript * //编辑器html内容:

    1

    2

    * console.log(editor.getPlainTxt()); //输出:"12 * ``` */ getContentTxt: function () { var reg = new RegExp(domUtils.fillChar, 'g'); //取出来的空格会有c2a0会变成乱码,处理这种情况\u00a0 return this.body[browser.ie ? 'innerText' : 'textContent'].replace(reg, '').replace(/\u00a0/g, ' '); }, /** * 设置编辑器的内容,可修改编辑器当前的html内容 * @method setContent * @warning 通过该方法插入的内容,是经过编辑器内置的过滤规则进行过滤后得到的内容 * @warning 该方法会触发selectionchange事件 * @param { String } html 要插入的html内容 * @example * ```javascript * editor.getContent('

    test

    '); * ``` */ /** * 设置编辑器的内容,可修改编辑器当前的html内容 * @method setContent * @warning 通过该方法插入的内容,是经过编辑器内置的过滤规则进行过滤后得到的内容 * @warning 该方法会触发selectionchange事件 * @param { String } html 要插入的html内容 * @param { Boolean } isAppendTo 若传入true,不清空原来的内容,在最后插入内容,否则,清空内容再插入 * @example * ```javascript * //假设设置前的编辑器内容是

    old text

    * editor.setContent('

    new text

    ', true); //插入的结果是

    old text

    new text

    * ``` */ setContent: function (html, isAppendTo, notFireSelectionchange) { var me = this; me.fireEvent('beforesetcontent', html); var root = UE.htmlparser(html); me.filterInputRule(root); html = root.toHtml(); me.body.innerHTML = (isAppendTo ? me.body.innerHTML : '') + html; function isCdataDiv(node){ return node.tagName == 'DIV' && node.getAttribute('cdata_tag'); } //给文本或者inline节点套p标签 if (me.options.enterTag == 'p') { var child = this.body.firstChild, tmpNode; if (!child || child.nodeType == 1 && (dtd.$cdata[child.tagName] || isCdataDiv(child) || domUtils.isCustomeNode(child) ) && child === this.body.lastChild) { this.body.innerHTML = '

    ' + (browser.ie ? ' ' : '
    ') + '

    ' + this.body.innerHTML; } else { var p = me.document.createElement('p'); while (child) { while (child && (child.nodeType == 3 || child.nodeType == 1 && dtd.p[child.tagName] && !dtd.$cdata[child.tagName])) { tmpNode = child.nextSibling; p.appendChild(child); child = tmpNode; } if (p.firstChild) { if (!child) { me.body.appendChild(p); break; } else { child.parentNode.insertBefore(p, child); p = me.document.createElement('p'); } } child = child.nextSibling; } } } me.fireEvent('aftersetcontent'); me.fireEvent('contentchange'); !notFireSelectionchange && me._selectionChange(); //清除保存的选区 me._bakRange = me._bakIERange = me._bakNativeRange = null; //trace:1742 setContent后gecko能得到焦点问题 var geckoSel; if (browser.gecko && (geckoSel = this.selection.getNative())) { geckoSel.removeAllRanges(); } if(me.options.autoSyncData){ me.form && setValue(me.form,me); } }, /** * 让编辑器获得焦点,默认focus到编辑器头部 * @method focus * @example * ```javascript * editor.focus() * ``` */ /** * 让编辑器获得焦点,toEnd确定focus位置 * @method focus * @param { Boolean } toEnd 默认focus到编辑器头部,toEnd为true时focus到内容尾部 * @example * ```javascript * editor.focus(true) * ``` */ focus: function (toEnd) { try { var me = this, rng = me.selection.getRange(); if (toEnd) { var node = me.body.lastChild; if(node && node.nodeType == 1 && !dtd.$empty[node.tagName]){ if(domUtils.isEmptyBlock(node)){ rng.setStartAtFirst(node) }else{ rng.setStartAtLast(node) } rng.collapse(true); } rng.setCursor(true); } else { if(!rng.collapsed && domUtils.isBody(rng.startContainer) && rng.startOffset == 0){ var node = me.body.firstChild; if(node && node.nodeType == 1 && !dtd.$empty[node.tagName]){ rng.setStartAtFirst(node).collapse(true); } } rng.select(true); } this.fireEvent('focus selectionchange'); } catch (e) { } }, isFocus:function(){ return this.selection.isFocus(); }, blur:function(){ var sel = this.selection.getNative(); if(sel.empty && browser.ie){ var nativeRng = document.body.createTextRange(); nativeRng.moveToElementText(document.body); nativeRng.collapse(true); nativeRng.select(); sel.empty() }else{ sel.removeAllRanges() } //this.fireEvent('blur selectionchange'); }, /** * 初始化UE事件及部分事件代理 * @method _initEvents * @private */ _initEvents: function () { var me = this, doc = me.document, win = me.window; me._proxyDomEvent = utils.bind(me._proxyDomEvent, me); domUtils.on(doc, ['click', 'contextmenu', 'mousedown', 'keydown', 'keyup', 'keypress', 'mouseup', 'mouseover', 'mouseout', 'selectstart'], me._proxyDomEvent); domUtils.on(win, ['focus', 'blur'], me._proxyDomEvent); domUtils.on(me.body,'drop',function(e){ //阻止ff下默认的弹出新页面打开图片 if(browser.gecko && e.stopPropagation) { e.stopPropagation(); } me.fireEvent('contentchange') }); domUtils.on(doc, ['mouseup', 'keydown'], function (evt) { //特殊键不触发selectionchange if (evt.type == 'keydown' && (evt.ctrlKey || evt.metaKey || evt.shiftKey || evt.altKey)) { return; } if (evt.button == 2)return; me._selectionChange(250, evt); }); }, /** * 触发事件代理 * @method _proxyDomEvent * @private * @return { * } fireEvent的返回值 * @see UE.EventBase:fireEvent(String) */ _proxyDomEvent: function (evt) { if(this.fireEvent('before' + evt.type.replace(/^on/, '').toLowerCase()) === false){ return false; } if(this.fireEvent(evt.type.replace(/^on/, ''), evt) === false){ return false; } return this.fireEvent('after' + evt.type.replace(/^on/, '').toLowerCase()) }, /** * 变化选区 * @method _selectionChange * @private */ _selectionChange: function (delay, evt) { var me = this; //有光标才做selectionchange 为了解决未focus时点击source不能触发更改工具栏状态的问题(source命令notNeedUndo=1) // if ( !me.selection.isFocus() ){ // return; // } var hackForMouseUp = false; var mouseX, mouseY; if (browser.ie && browser.version < 9 && evt && evt.type == 'mouseup') { var range = this.selection.getRange(); if (!range.collapsed) { hackForMouseUp = true; mouseX = evt.clientX; mouseY = evt.clientY; } } clearTimeout(_selectionChangeTimer); _selectionChangeTimer = setTimeout(function () { if (!me.selection || !me.selection.getNative()) { return; } //修复一个IE下的bug: 鼠标点击一段已选择的文本中间时,可能在mouseup后的一段时间内取到的range是在selection的type为None下的错误值. //IE下如果用户是拖拽一段已选择文本,则不会触发mouseup事件,所以这里的特殊处理不会对其有影响 var ieRange; if (hackForMouseUp && me.selection.getNative().type == 'None') { ieRange = me.document.body.createTextRange(); try { ieRange.moveToPoint(mouseX, mouseY); } catch (ex) { ieRange = null; } } var bakGetIERange; if (ieRange) { bakGetIERange = me.selection.getIERange; me.selection.getIERange = function () { return ieRange; }; } me.selection.cache(); if (bakGetIERange) { me.selection.getIERange = bakGetIERange; } if (me.selection._cachedRange && me.selection._cachedStartElement) { me.fireEvent('beforeselectionchange'); // 第二个参数causeByUi为true代表由用户交互造成的selectionchange. me.fireEvent('selectionchange', !!evt); me.fireEvent('afterselectionchange'); me.selection.clear(); } }, delay || 50); }, /** * 执行编辑命令 * @method _callCmdFn * @private * @param { String } fnName 函数名称 * @param { * } args 传给命令函数的参数 * @return { * } 返回命令函数运行的返回值 */ _callCmdFn: function (fnName, args) { var cmdName = args[0].toLowerCase(), cmd, cmdFn; cmd = this.commands[cmdName] || UE.commands[cmdName]; cmdFn = cmd && cmd[fnName]; //没有querycommandstate或者没有command的都默认返回0 if ((!cmd || !cmdFn) && fnName == 'queryCommandState') { return 0; } else if (cmdFn) { return cmdFn.apply(this, args); } }, /** * 执行编辑命令cmdName,完成富文本编辑效果 * @method execCommand * @param { String } cmdName 需要执行的命令 * @remind 具体命令的使用请参考命令列表 * @return { * } 返回命令函数运行的返回值 * @example * ```javascript * editor.execCommand(cmdName); * ``` */ execCommand: function (cmdName) { cmdName = cmdName.toLowerCase(); var me = this, result, cmd = me.commands[cmdName] || UE.commands[cmdName]; if (!cmd || !cmd.execCommand) { return null; } if (!cmd.notNeedUndo && !me.__hasEnterExecCommand) { me.__hasEnterExecCommand = true; if (me.queryCommandState.apply(me,arguments) != -1) { me.fireEvent('saveScene'); me.fireEvent.apply(me, ['beforeexeccommand', cmdName].concat(arguments)); result = this._callCmdFn('execCommand', arguments); //保存场景时,做了内容对比,再看是否进行contentchange触发,这里多触发了一次,去掉 // (!cmd.ignoreContentChange && !me._ignoreContentChange) && me.fireEvent('contentchange'); me.fireEvent.apply(me, ['afterexeccommand', cmdName].concat(arguments)); me.fireEvent('saveScene'); } me.__hasEnterExecCommand = false; } else { result = this._callCmdFn('execCommand', arguments); (!me.__hasEnterExecCommand && !cmd.ignoreContentChange && !me._ignoreContentChange) && me.fireEvent('contentchange') } (!me.__hasEnterExecCommand && !cmd.ignoreContentChange && !me._ignoreContentChange) && me._selectionChange(); return result; }, /** * 根据传入的command命令,查选编辑器当前的选区,返回命令的状态 * @method queryCommandState * @param { String } cmdName 需要查询的命令名称 * @remind 具体命令的使用请参考命令列表 * @return { Number } number 返回放前命令的状态,返回值三种情况:(-1|0|1) * @example * ```javascript * editor.queryCommandState(cmdName) => (-1|0|1) * ``` * @see COMMAND.LIST */ queryCommandState: function (cmdName) { return this._callCmdFn('queryCommandState', arguments); }, /** * 根据传入的command命令,查选编辑器当前的选区,根据命令返回相关的值 * @method queryCommandValue * @param { String } cmdName 需要查询的命令名称 * @remind 具体命令的使用请参考命令列表 * @remind 只有部分插件有此方法 * @return { * } 返回每个命令特定的当前状态值 * @grammar editor.queryCommandValue(cmdName) => {*} * @see COMMAND.LIST */ queryCommandValue: function (cmdName) { return this._callCmdFn('queryCommandValue', arguments); }, /** * 检查编辑区域中是否有内容 * @method hasContents * @remind 默认有文本内容,或者有以下节点都不认为是空 * table,ul,ol,dl,iframe,area,base,col,hr,img,embed,input,link,meta,param * @return { Boolean } 检查有内容返回true,否则返回false * @example * ```javascript * editor.hasContents() * ``` */ /** * 检查编辑区域中是否有内容,若包含参数tags中的节点类型,直接返回true * @method hasContents * @param { Array } tags 传入数组判断时用到的节点类型 * @return { Boolean } 若文档中包含tags数组里对应的tag,返回true,否则返回false * @example * ```javascript * editor.hasContents(['span']); * ``` */ hasContents: function (tags) { if (tags) { for (var i = 0, ci; ci = tags[i++];) { if (this.document.getElementsByTagName(ci).length > 0) { return true; } } } if (!domUtils.isEmptyBlock(this.body)) { return true } //随时添加,定义的特殊标签如果存在,不能认为是空 tags = ['div']; for (i = 0; ci = tags[i++];) { var nodes = domUtils.getElementsByTagName(this.document, ci); for (var n = 0, cn; cn = nodes[n++];) { if (domUtils.isCustomeNode(cn)) { return true; } } } return false; }, /** * 重置编辑器,可用来做多个tab使用同一个编辑器实例 * @method reset * @remind 此方法会清空编辑器内容,清空回退列表,会触发reset事件 * @example * ```javascript * editor.reset() * ``` */ reset: function () { this.fireEvent('reset'); }, /** * 设置当前编辑区域可以编辑 * @method setEnabled * @example * ```javascript * editor.setEnabled() * ``` */ setEnabled: function () { var me = this, range; if (me.body.contentEditable == 'false') { me.body.contentEditable = true; range = me.selection.getRange(); //有可能内容丢失了 try { range.moveToBookmark(me.lastBk); delete me.lastBk } catch (e) { range.setStartAtFirst(me.body).collapse(true) } range.select(true); if (me.bkqueryCommandState) { me.queryCommandState = me.bkqueryCommandState; delete me.bkqueryCommandState; } if (me.bkqueryCommandValue) { me.queryCommandValue = me.bkqueryCommandValue; delete me.bkqueryCommandValue; } me.fireEvent('selectionchange'); } }, enable: function () { return this.setEnabled(); }, /** 设置当前编辑区域不可编辑 * @method setDisabled */ /** 设置当前编辑区域不可编辑,except中的命令除外 * @method setDisabled * @param { String } except 例外命令的字符串 * @remind 即使设置了disable,此处配置的例外命令仍然可以执行 * @example * ```javascript * editor.setDisabled('bold'); //禁用工具栏中除加粗之外的所有功能 * ``` */ /** 设置当前编辑区域不可编辑,except中的命令除外 * @method setDisabled * @param { Array } except 例外命令的字符串数组,数组中的命令仍然可以执行 * @remind 即使设置了disable,此处配置的例外命令仍然可以执行 * @example * ```javascript * editor.setDisabled(['bold','insertimage']); //禁用工具栏中除加粗和插入图片之外的所有功能 * ``` */ setDisabled: function (except) { var me = this; except = except ? utils.isArray(except) ? except : [except] : []; if (me.body.contentEditable == 'true') { if (!me.lastBk) { me.lastBk = me.selection.getRange().createBookmark(true); } me.body.contentEditable = false; me.bkqueryCommandState = me.queryCommandState; me.bkqueryCommandValue = me.queryCommandValue; me.queryCommandState = function (type) { if (utils.indexOf(except, type) != -1) { return me.bkqueryCommandState.apply(me, arguments); } return -1; }; me.queryCommandValue = function (type) { if (utils.indexOf(except, type) != -1) { return me.bkqueryCommandValue.apply(me, arguments); } return null; }; me.fireEvent('selectionchange'); } }, disable: function (except) { return this.setDisabled(except); }, /** * 设置默认内容 * @method _setDefaultContent * @private * @param { String } cont 要存入的内容 */ _setDefaultContent: function () { function clear() { var me = this; if (me.document.getElementById('initContent')) { me.body.innerHTML = '

    ' + (ie ? '' : '
    ') + '

    '; me.removeListener('firstBeforeExecCommand focus', clear); setTimeout(function () { me.focus(); me._selectionChange(); }, 0) } } return function (cont) { var me = this; me.body.innerHTML = '

    ' + cont + '

    '; me.addListener('firstBeforeExecCommand focus', clear); } }(), /** * 显示编辑器 * @method setShow * @example * ```javascript * editor.setShow() * ``` */ setShow: function () { var me = this, range = me.selection.getRange(); if (me.container.style.display == 'none') { //有可能内容丢失了 try { range.moveToBookmark(me.lastBk); delete me.lastBk } catch (e) { range.setStartAtFirst(me.body).collapse(true) } //ie下focus实效,所以做了个延迟 setTimeout(function () { range.select(true); }, 100); me.container.style.display = ''; } }, show: function () { return this.setShow(); }, /** * 隐藏编辑器 * @method setHide * @example * ```javascript * editor.setHide() * ``` */ setHide: function () { var me = this; if (!me.lastBk) { me.lastBk = me.selection.getRange().createBookmark(true); } me.container.style.display = 'none' }, hide: function () { return this.setHide(); }, /** * 根据指定的路径,获取对应的语言资源 * @method getLang * @param { String } path 路径根据的是lang目录下的语言文件的路径结构 * @return { Object | String } 根据路径返回语言资源的Json格式对象或者语言字符串 * @example * ```javascript * editor.getLang('contextMenu.delete'); //如果当前是中文,那返回是的是'删除' * ``` */ getLang: function (path) { // HaoChuan9421 if(!this.options){ return ''; } var lang = UE.I18N[this.options.lang]; if (!lang) { throw Error("not import language file"); } path = (path || "").split("."); for (var i = 0, ci; ci = path[i++];) { lang = lang[ci]; if (!lang)break; } return lang; }, /** * 计算编辑器html内容字符串的长度 * @method getContentLength * @return { Number } 返回计算的长度 * @example * ```javascript * //编辑器html内容

    132

    * editor.getContentLength() //返回27 * ``` */ /** * 计算编辑器当前纯文本内容的长度 * @method getContentLength * @param { Boolean } ingoneHtml 传入true时,只按照纯文本来计算 * @return { Number } 返回计算的长度,内容中有hr/img/iframe标签,长度加1 * @example * ```javascript * //编辑器html内容

    132

    * editor.getContentLength() //返回3 * ``` */ getContentLength: function (ingoneHtml, tagNames) { var count = this.getContent(false,false,true).length; if (ingoneHtml) { tagNames = (tagNames || []).concat([ 'hr', 'img', 'iframe']); count = this.getContentTxt().replace(/[\t\r\n]+/g, '').length; for (var i = 0, ci; ci = tagNames[i++];) { count += this.document.getElementsByTagName(ci).length; } } return count; }, /** * 注册输入过滤规则 * @method addInputRule * @param { Function } rule 要添加的过滤规则 * @example * ```javascript * editor.addInputRule(function(root){ * $.each(root.getNodesByTagName('div'),function(i,node){ * node.tagName="p"; * }); * }); * ``` */ addInputRule: function (rule) { this.inputRules.push(rule); }, /** * 执行注册的过滤规则 * @method filterInputRule * @param { UE.uNode } root 要过滤的uNode节点 * @remind 执行editor.setContent方法和执行'inserthtml'命令后,会运行该过滤函数 * @example * ```javascript * editor.filterInputRule(editor.body); * ``` * @see UE.Editor:addInputRule */ filterInputRule: function (root) { for (var i = 0, ci; ci = this.inputRules[i++];) { ci.call(this, root) } }, /** * 注册输出过滤规则 * @method addOutputRule * @param { Function } rule 要添加的过滤规则 * @example * ```javascript * editor.addOutputRule(function(root){ * $.each(root.getNodesByTagName('p'),function(i,node){ * node.tagName="div"; * }); * }); * ``` */ addOutputRule: function (rule) { this.outputRules.push(rule) }, /** * 根据输出过滤规则,过滤编辑器内容 * @method filterOutputRule * @remind 执行editor.getContent方法的时候,会先运行该过滤函数 * @param { UE.uNode } root 要过滤的uNode节点 * @example * ```javascript * editor.filterOutputRule(editor.body); * ``` * @see UE.Editor:addOutputRule */ filterOutputRule: function (root) { for (var i = 0, ci; ci = this.outputRules[i++];) { ci.call(this, root) } }, /** * 根据action名称获取请求的路径 * @method getActionUrl * @remind 假如没有设置serverUrl,会根据imageUrl设置默认的controller路径 * @param { String } action action名称 * @example * ```javascript * editor.getActionUrl('config'); //返回 "/ueditor/php/controller.php?action=config" * editor.getActionUrl('image'); //返回 "/ueditor/php/controller.php?action=uplaodimage" * editor.getActionUrl('scrawl'); //返回 "/ueditor/php/controller.php?action=uplaodscrawl" * editor.getActionUrl('imageManager'); //返回 "/ueditor/php/controller.php?action=listimage" * ``` */ getActionUrl: function(action){ var actionName = this.getOpt(action) || action, imageUrl = this.getOpt('imageUrl'), serverUrl = this.getOpt('serverUrl'); if(!serverUrl && imageUrl) { serverUrl = imageUrl.replace(/^(.*[\/]).+([\.].+)$/, '$1controller$2'); } if(serverUrl) { serverUrl = serverUrl + (serverUrl.indexOf('?') == -1 ? '?':'&') + 'action=' + (actionName || ''); return utils.formatUrl(serverUrl); } else { return ''; } } }; utils.inherits(Editor, EventBase); })(); // core/Editor.defaultoptions.js //维护编辑器一下默认的不在插件中的配置项 UE.Editor.defaultOptions = function(editor){ var _url = editor.options.UEDITOR_HOME_URL; return { isShow: true, initialContent: '', initialStyle:'', autoClearinitialContent: false, iframeCssUrl: _url + 'themes/iframe.css', textarea: 'editorValue', focus: false, focusInEnd: true, autoClearEmptyNode: true, fullscreen: false, readonly: false, zIndex: 999, imagePopup: true, enterTag: 'p', customDomain: false, lang: 'zh-cn', langPath: _url + 'lang/', theme: 'default', themePath: _url + 'themes/', allHtmlEnabled: false, scaleEnabled: false, tableNativeEditInFF: false, autoSyncData : true, fileNameFormat: '{time}{rand:6}' } }; // core/loadconfig.js (function(){ UE.Editor.prototype.loadServerConfig = function(){ var me = this; setTimeout(function(){ try{ me.options.imageUrl && me.setOpt('serverUrl', me.options.imageUrl.replace(/^(.*[\/]).+([\.].+)$/, '$1controller$2')); var configUrl = me.getActionUrl('config'), isJsonp = utils.isCrossDomainUrl(configUrl); /* 发出ajax请求 */ me._serverConfigLoaded = false; configUrl && UE.ajax.request(configUrl,{ 'method': 'GET', 'dataType': isJsonp ? 'jsonp':'', 'onsuccess':function(r){ try { var config = isJsonp ? r:eval("("+r.responseText+")"); utils.extend(me.options, config); me.fireEvent('serverConfigLoaded'); me._serverConfigLoaded = true; } catch (e) { showErrorMsg(me.getLang('loadconfigFormatError')); } }, 'onerror':function(){ showErrorMsg(me.getLang('loadconfigHttpError')); } }); } catch(e){ showErrorMsg(me.getLang('loadconfigError')); } }); function showErrorMsg(msg) { console && console.error(msg); //me.fireEvent('showMessage', { // 'title': msg, // 'type': 'error' //}); } }; UE.Editor.prototype.isServerConfigLoaded = function(){ var me = this; return me._serverConfigLoaded || false; }; UE.Editor.prototype.afterConfigReady = function(handler){ if (!handler || !utils.isFunction(handler)) return; var me = this; var readyHandler = function(){ handler.apply(me, arguments); me.removeListener('serverConfigLoaded', readyHandler); }; if (me.isServerConfigLoaded()) { handler.call(me, 'serverConfigLoaded'); } else { me.addListener('serverConfigLoaded', readyHandler); } }; })(); // core/ajax.js /** * @file * @module UE.ajax * @since 1.2.6.1 */ /** * 提供对ajax请求的支持 * @module UE.ajax */ UE.ajax = function() { //创建一个ajaxRequest对象 var fnStr = 'XMLHttpRequest()'; try { new ActiveXObject("Msxml2.XMLHTTP"); fnStr = 'ActiveXObject(\'Msxml2.XMLHTTP\')'; } catch (e) { try { new ActiveXObject("Microsoft.XMLHTTP"); fnStr = 'ActiveXObject(\'Microsoft.XMLHTTP\')' } catch (e) { } } var creatAjaxRequest = new Function('return new ' + fnStr); /** * 将json参数转化成适合ajax提交的参数列表 * @param json */ function json2str(json) { var strArr = []; for (var i in json) { //忽略默认的几个参数 if(i=="method" || i=="timeout" || i=="async" || i=="dataType" || i=="callback") continue; //忽略控制 if(json[i] == undefined || json[i] == null) continue; //传递过来的对象和函数不在提交之列 if (!((typeof json[i]).toLowerCase() == "function" || (typeof json[i]).toLowerCase() == "object")) { strArr.push( encodeURIComponent(i) + "="+encodeURIComponent(json[i]) ); } else if (utils.isArray(json[i])) { //支持传数组内容 for(var j = 0; j < json[i].length; j++) { strArr.push( encodeURIComponent(i) + "[]="+encodeURIComponent(json[i][j]) ); } } } return strArr.join("&"); } function doAjax(url, ajaxOptions) { var xhr = creatAjaxRequest(), //是否超时 timeIsOut = false, //默认参数 defaultAjaxOptions = { method:"POST", timeout:5000, async:true, data:{},//需要传递对象的话只能覆盖 onsuccess:function() { }, onerror:function() { } }; if (typeof url === "object") { ajaxOptions = url; url = ajaxOptions.url; } if (!xhr || !url) return; var ajaxOpts = ajaxOptions ? utils.extend(defaultAjaxOptions,ajaxOptions) : defaultAjaxOptions; var submitStr = json2str(ajaxOpts); // { name:"Jim",city:"Beijing" } --> "name=Jim&city=Beijing" //如果用户直接通过data参数传递json对象过来,则也要将此json对象转化为字符串 if (!utils.isEmptyObject(ajaxOpts.data)){ submitStr += (submitStr? "&":"") + json2str(ajaxOpts.data); } //超时检测 var timerID = setTimeout(function() { if (xhr.readyState != 4) { timeIsOut = true; xhr.abort(); clearTimeout(timerID); } }, ajaxOpts.timeout); var method = ajaxOpts.method.toUpperCase(); var str = url + (url.indexOf("?")==-1?"?":"&") + (method=="POST"?"":submitStr+ "&noCache=" + +new Date); xhr.open(method, str, ajaxOpts.async); xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if (!timeIsOut && xhr.status == 200) { ajaxOpts.onsuccess(xhr); } else { ajaxOpts.onerror(xhr); } } }; if (method == "POST") { xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send(submitStr); } else { xhr.send(null); } } function doJsonp(url, opts) { var successhandler = opts.onsuccess || function(){}, scr = document.createElement('SCRIPT'), options = opts || {}, charset = options['charset'], callbackField = options['jsonp'] || 'callback', callbackFnName, timeOut = options['timeOut'] || 0, timer, reg = new RegExp('(\\?|&)' + callbackField + '=([^&]*)'), matches; if (utils.isFunction(successhandler)) { callbackFnName = 'bd__editor__' + Math.floor(Math.random() * 2147483648).toString(36); window[callbackFnName] = getCallBack(0); } else if(utils.isString(successhandler)){ callbackFnName = successhandler; } else { if (matches = reg.exec(url)) { callbackFnName = matches[2]; } } url = url.replace(reg, '\x241' + callbackField + '=' + callbackFnName); if (url.search(reg) < 0) { url += (url.indexOf('?') < 0 ? '?' : '&') + callbackField + '=' + callbackFnName; } var queryStr = json2str(opts); // { name:"Jim",city:"Beijing" } --> "name=Jim&city=Beijing" //如果用户直接通过data参数传递json对象过来,则也要将此json对象转化为字符串 if (!utils.isEmptyObject(opts.data)){ queryStr += (queryStr? "&":"") + json2str(opts.data); } if (queryStr) { url = url.replace(/\?/, '?' + queryStr + '&'); } scr.onerror = getCallBack(1); if( timeOut ){ timer = setTimeout(getCallBack(1), timeOut); } createScriptTag(scr, url, charset); function createScriptTag(scr, url, charset) { scr.setAttribute('type', 'text/javascript'); scr.setAttribute('defer', 'defer'); charset && scr.setAttribute('charset', charset); scr.setAttribute('src', url); document.getElementsByTagName('head')[0].appendChild(scr); } function getCallBack(onTimeOut){ return function(){ try { if(onTimeOut){ options.onerror && options.onerror(); }else{ try{ clearTimeout(timer); successhandler.apply(window, arguments); } catch (e){} } } catch (exception) { options.onerror && options.onerror.call(window, exception); } finally { options.oncomplete && options.oncomplete.apply(window, arguments); scr.parentNode && scr.parentNode.removeChild(scr); window[callbackFnName] = null; try { delete window[callbackFnName]; }catch(e){} } } } } return { /** * 根据给定的参数项,向指定的url发起一个ajax请求。 ajax请求完成后,会根据请求结果调用相应回调: 如果请求 * 成功, 则调用onsuccess回调, 失败则调用 onerror 回调 * @method request * @param { URLString } url ajax请求的url地址 * @param { Object } ajaxOptions ajax请求选项的键值对,支持的选项如下: * @example * ```javascript * //向sayhello.php发起一个异步的Ajax GET请求, 请求超时时间为10s, 请求完成后执行相应的回调。 * UE.ajax.requeset( 'sayhello.php', { * * //请求方法。可选值: 'GET', 'POST',默认值是'POST' * method: 'GET', * * //超时时间。 默认为5000, 单位是ms * timeout: 10000, * * //是否是异步请求。 true为异步请求, false为同步请求 * async: true, * * //请求携带的数据。如果请求为GET请求, data会经过stringify后附加到请求url之后。 * data: { * name: 'ueditor' * }, * * //请求成功后的回调, 该回调接受当前的XMLHttpRequest对象作为参数。 * onsuccess: function ( xhr ) { * console.log( xhr.responseText ); * }, * * //请求失败或者超时后的回调。 * onerror: function ( xhr ) { * alert( 'Ajax请求失败' ); * } * * } ); * ``` */ /** * 根据给定的参数项发起一个ajax请求, 参数项里必须包含一个url地址。 ajax请求完成后,会根据请求结果调用相应回调: 如果请求 * 成功, 则调用onsuccess回调, 失败则调用 onerror 回调。 * @method request * @warning 如果在参数项里未提供一个key为“url”的地址值,则该请求将直接退出。 * @param { Object } ajaxOptions ajax请求选项的键值对,支持的选项如下: * @example * ```javascript * * //向sayhello.php发起一个异步的Ajax POST请求, 请求超时时间为5s, 请求完成后不执行任何回调。 * UE.ajax.requeset( 'sayhello.php', { * * //请求的地址, 该项是必须的。 * url: 'sayhello.php' * * } ); * ``` */ request:function(url, opts) { if (opts && opts.dataType == 'jsonp') { doJsonp(url, opts); } else { doAjax(url, opts); } }, getJSONP:function(url, data, fn) { var opts = { 'data': data, 'oncomplete': fn }; doJsonp(url, opts); } }; }(); // core/filterword.js /** * UE过滤word的静态方法 * @file */ /** * UEditor公用空间,UEditor所有的功能都挂载在该空间下 * @module UE */ /** * 根据传入html字符串过滤word * @module UE * @since 1.2.6.1 * @method filterWord * @param { String } html html字符串 * @return { String } 已过滤后的结果字符串 * @example * ```javascript * UE.filterWord(html); * ``` */ var filterWord = UE.filterWord = function () { //是否是word过来的内容 function isWordDocument( str ) { return /(class="?Mso|style="[^"]*\bmso\-|w:WordDocument|<(v|o):|lang=)/ig.test( str ); } //去掉小数 function transUnit( v ) { v = v.replace( /[\d.]+\w+/g, function ( m ) { return utils.transUnitToPx(m); } ); return v; } function filterPasteWord( str ) { return str.replace(/[\t\r\n]+/g,' ') .replace( //ig, "" ) //转换图片 .replace(/]*>[\s\S]*?.<\/v:shape>/gi,function(str){ //opera能自己解析出image所这里直接返回空 if(browser.opera){ return ''; } try{ //有可能是bitmap占为图,无用,直接过滤掉,主要体现在粘贴excel表格中 if(/Bitmap/i.test(str)){ return ''; } var width = str.match(/width:([ \d.]*p[tx])/i)[1], height = str.match(/height:([ \d.]*p[tx])/i)[1], src = str.match(/src=\s*"([^"]*)"/i)[1]; return ''; } catch(e){ return ''; } }) //针对wps添加的多余标签处理 .replace(/<\/?div[^>]*>/g,'') //去掉多余的属性 .replace( /v:\w+=(["']?)[^'"]+\1/g, '' ) .replace( /<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|xml|meta|link|style|\w+:\w+)(?=[\s\/>]))[^>]*>/gi, "" ) .replace( /

    ]*class="?MsoHeading"?[^>]*>(.*?)<\/p>/gi, "

    $1

    " ) //去掉多余的属性 .replace( /\s+(class|lang|align)\s*=\s*(['"]?)([\w-]+)\2/ig, function(str,name,marks,val){ //保留list的标示 return name == 'class' && val == 'MsoListParagraph' ? str : '' }) //清除多余的font/span不能匹配 有可能是空格 .replace( /<(font|span)[^>]*>(\s*)<\/\1>/gi, function(a,b,c){ return c.replace(/[\t\r\n ]+/g,' ') }) //处理style的问题 .replace( /(<[a-z][^>]*)\sstyle=(["'])([^\2]*?)\2/gi, function( str, tag, tmp, style ) { var n = [], s = style.replace( /^\s+|\s+$/, '' ) .replace(/'/g,'\'') .replace( /"/gi, "'" ) .replace(/[\d.]+(cm|pt)/g,function(str){ return utils.transUnitToPx(str) }) .split( /;\s*/g ); for ( var i = 0,v; v = s[i];i++ ) { var name, value, parts = v.split( ":" ); if ( parts.length == 2 ) { name = parts[0].toLowerCase(); value = parts[1].toLowerCase(); if(/^(background)\w*/.test(name) && value.replace(/(initial|\s)/g,'').length == 0 || /^(margin)\w*/.test(name) && /^0\w+$/.test(value) ){ continue; } switch ( name ) { case "mso-padding-alt": case "mso-padding-top-alt": case "mso-padding-right-alt": case "mso-padding-bottom-alt": case "mso-padding-left-alt": case "mso-margin-alt": case "mso-margin-top-alt": case "mso-margin-right-alt": case "mso-margin-bottom-alt": case "mso-margin-left-alt": //ie下会出现挤到一起的情况 //case "mso-table-layout-alt": case "mso-height": case "mso-width": case "mso-vertical-align-alt": //trace:1819 ff下会解析出padding在table上 if(!/]/.test(html)) { return UE.htmlparser(html).children[0] } else { return new uNode({ type:'element', children:[], tagName:html }) } }; uNode.createText = function (data,noTrans) { return new UE.uNode({ type:'text', 'data':noTrans ? data : utils.unhtml(data || '') }) }; function nodeToHtml(node, arr, formatter, current) { switch (node.type) { case 'root': for (var i = 0, ci; ci = node.children[i++];) { //插入新行 if (formatter && ci.type == 'element' && !dtd.$inlineWithA[ci.tagName] && i > 1) { insertLine(arr, current, true); insertIndent(arr, current) } nodeToHtml(ci, arr, formatter, current) } break; case 'text': isText(node, arr); break; case 'element': isElement(node, arr, formatter, current); break; case 'comment': isComment(node, arr, formatter); } return arr; } function isText(node, arr) { if(node.parentNode.tagName == 'pre'){ //源码模式下输入html标签,不能做转换处理,直接输出 arr.push(node.data) }else{ arr.push(notTransTagName[node.parentNode.tagName] ? utils.html(node.data) : node.data.replace(/[ ]{2}/g,'  ')) } } function isElement(node, arr, formatter, current) { var attrhtml = ''; if (node.attrs) { attrhtml = []; var attrs = node.attrs; for (var a in attrs) { //这里就针对 //

    '

    //这里边的\"做转换,要不用innerHTML直接被截断了,属性src //有可能做的不够 attrhtml.push(a + (attrs[a] !== undefined ? '="' + (notTransAttrs[a] ? utils.html(attrs[a]).replace(/["]/g, function (a) { return '"' }) : utils.unhtml(attrs[a])) + '"' : '')) } attrhtml = attrhtml.join(' '); } arr.push('<' + node.tagName + (attrhtml ? ' ' + attrhtml : '') + (dtd.$empty[node.tagName] ? '\/' : '' ) + '>' ); //插入新行 if (formatter && !dtd.$inlineWithA[node.tagName] && node.tagName != 'pre') { if(node.children && node.children.length){ current = insertLine(arr, current, true); insertIndent(arr, current) } } if (node.children && node.children.length) { for (var i = 0, ci; ci = node.children[i++];) { if (formatter && ci.type == 'element' && !dtd.$inlineWithA[ci.tagName] && i > 1) { insertLine(arr, current); insertIndent(arr, current) } nodeToHtml(ci, arr, formatter, current) } } if (!dtd.$empty[node.tagName]) { if (formatter && !dtd.$inlineWithA[node.tagName] && node.tagName != 'pre') { if(node.children && node.children.length){ current = insertLine(arr, current); insertIndent(arr, current) } } arr.push('<\/' + node.tagName + '>'); } } function isComment(node, arr) { arr.push(''); } function getNodeById(root, id) { var node; if (root.type == 'element' && root.getAttr('id') == id) { return root; } if (root.children && root.children.length) { for (var i = 0, ci; ci = root.children[i++];) { if (node = getNodeById(ci, id)) { return node; } } } } function getNodesByTagName(node, tagName, arr) { if (node.type == 'element' && node.tagName == tagName) { arr.push(node); } if (node.children && node.children.length) { for (var i = 0, ci; ci = node.children[i++];) { getNodesByTagName(ci, tagName, arr) } } } function nodeTraversal(root,fn){ if(root.children && root.children.length){ for(var i= 0,ci;ci=root.children[i];){ nodeTraversal(ci,fn); //ci被替换的情况,这里就不再走 fn了 if(ci.parentNode ){ if(ci.children && ci.children.length){ fn(ci) } if(ci.parentNode) i++ } } }else{ fn(root) } } uNode.prototype = { /** * 当前节点对象,转换成html文本 * @method toHtml * @return { String } 返回转换后的html字符串 * @example * ```javascript * node.toHtml(); * ``` */ /** * 当前节点对象,转换成html文本 * @method toHtml * @param { Boolean } formatter 是否格式化返回值 * @return { String } 返回转换后的html字符串 * @example * ```javascript * node.toHtml( true ); * ``` */ toHtml:function (formatter) { var arr = []; nodeToHtml(this, arr, formatter, 0); return arr.join('') }, /** * 获取节点的html内容 * @method innerHTML * @warning 假如节点的type不是'element',或节点的标签名称不在dtd列表里,直接返回当前节点 * @return { String } 返回节点的html内容 * @example * ```javascript * var htmlstr = node.innerHTML(); * ``` */ /** * 设置节点的html内容 * @method innerHTML * @warning 假如节点的type不是'element',或节点的标签名称不在dtd列表里,直接返回当前节点 * @param { String } htmlstr 传入要设置的html内容 * @return { UE.uNode } 返回节点本身 * @example * ```javascript * node.innerHTML('text'); * ``` */ innerHTML:function (htmlstr) { if (this.type != 'element' || dtd.$empty[this.tagName]) { return this; } if (utils.isString(htmlstr)) { if(this.children){ for (var i = 0, ci; ci = this.children[i++];) { ci.parentNode = null; } } this.children = []; var tmpRoot = UE.htmlparser(htmlstr); for (var i = 0, ci; ci = tmpRoot.children[i++];) { this.children.push(ci); ci.parentNode = this; } return this; } else { var tmpRoot = new UE.uNode({ type:'root', children:this.children }); return tmpRoot.toHtml(); } }, /** * 获取节点的纯文本内容 * @method innerText * @warning 假如节点的type不是'element',或节点的标签名称不在dtd列表里,直接返回当前节点 * @return { String } 返回节点的存文本内容 * @example * ```javascript * var textStr = node.innerText(); * ``` */ /** * 设置节点的纯文本内容 * @method innerText * @warning 假如节点的type不是'element',或节点的标签名称不在dtd列表里,直接返回当前节点 * @param { String } textStr 传入要设置的文本内容 * @return { UE.uNode } 返回节点本身 * @example * ```javascript * node.innerText('text'); * ``` */ innerText:function (textStr,noTrans) { if (this.type != 'element' || dtd.$empty[this.tagName]) { return this; } if (textStr) { if(this.children){ for (var i = 0, ci; ci = this.children[i++];) { ci.parentNode = null; } } this.children = []; this.appendChild(uNode.createText(textStr,noTrans)); return this; } else { return this.toHtml().replace(/<[^>]+>/g, ''); } }, /** * 获取当前对象的data属性 * @method getData * @return { Object } 若节点的type值是elemenet,返回空字符串,否则返回节点的data属性 * @example * ```javascript * node.getData(); * ``` */ getData:function () { if (this.type == 'element') return ''; return this.data }, /** * 获取当前节点下的第一个子节点 * @method firstChild * @return { UE.uNode } 返回第一个子节点 * @example * ```javascript * node.firstChild(); //返回第一个子节点 * ``` */ firstChild:function () { // if (this.type != 'element' || dtd.$empty[this.tagName]) { // return this; // } return this.children ? this.children[0] : null; }, /** * 获取当前节点下的最后一个子节点 * @method lastChild * @return { UE.uNode } 返回最后一个子节点 * @example * ```javascript * node.lastChild(); //返回最后一个子节点 * ``` */ lastChild:function () { // if (this.type != 'element' || dtd.$empty[this.tagName] ) { // return this; // } return this.children ? this.children[this.children.length - 1] : null; }, /** * 获取和当前节点有相同父亲节点的前一个节点 * @method previousSibling * @return { UE.uNode } 返回前一个节点 * @example * ```javascript * node.children[2].previousSibling(); //返回子节点node.children[1] * ``` */ previousSibling : function(){ var parent = this.parentNode; for (var i = 0, ci; ci = parent.children[i]; i++) { if (ci === this) { return i == 0 ? null : parent.children[i-1]; } } }, /** * 获取和当前节点有相同父亲节点的后一个节点 * @method nextSibling * @return { UE.uNode } 返回后一个节点,找不到返回null * @example * ```javascript * node.children[2].nextSibling(); //如果有,返回子节点node.children[3] * ``` */ nextSibling : function(){ var parent = this.parentNode; for (var i = 0, ci; ci = parent.children[i++];) { if (ci === this) { return parent.children[i]; } } }, /** * 用新的节点替换当前节点 * @method replaceChild * @param { UE.uNode } target 要替换成该节点参数 * @param { UE.uNode } source 要被替换掉的节点 * @return { UE.uNode } 返回替换之后的节点对象 * @example * ```javascript * node.replaceChild(newNode, childNode); //用newNode替换childNode,childNode是node的子节点 * ``` */ replaceChild:function (target, source) { if (this.children) { if(target.parentNode){ target.parentNode.removeChild(target); } for (var i = 0, ci; ci = this.children[i]; i++) { if (ci === source) { this.children.splice(i, 1, target); source.parentNode = null; target.parentNode = this; return target; } } } }, /** * 在节点的子节点列表最后位置插入一个节点 * @method appendChild * @param { UE.uNode } node 要插入的节点 * @return { UE.uNode } 返回刚插入的子节点 * @example * ```javascript * node.appendChild( newNode ); //在node内插入子节点newNode * ``` */ appendChild:function (node) { if (this.type == 'root' || (this.type == 'element' && !dtd.$empty[this.tagName])) { if (!this.children) { this.children = [] } if(node.parentNode){ node.parentNode.removeChild(node); } for (var i = 0, ci; ci = this.children[i]; i++) { if (ci === node) { this.children.splice(i, 1); break; } } this.children.push(node); node.parentNode = this; return node; } }, /** * 在传入节点的前面插入一个节点 * @method insertBefore * @param { UE.uNode } target 要插入的节点 * @param { UE.uNode } source 在该参数节点前面插入 * @return { UE.uNode } 返回刚插入的子节点 * @example * ```javascript * node.parentNode.insertBefore(newNode, node); //在node节点后面插入newNode * ``` */ insertBefore:function (target, source) { if (this.children) { if(target.parentNode){ target.parentNode.removeChild(target); } for (var i = 0, ci; ci = this.children[i]; i++) { if (ci === source) { this.children.splice(i, 0, target); target.parentNode = this; return target; } } } }, /** * 在传入节点的后面插入一个节点 * @method insertAfter * @param { UE.uNode } target 要插入的节点 * @param { UE.uNode } source 在该参数节点后面插入 * @return { UE.uNode } 返回刚插入的子节点 * @example * ```javascript * node.parentNode.insertAfter(newNode, node); //在node节点后面插入newNode * ``` */ insertAfter:function (target, source) { if (this.children) { if(target.parentNode){ target.parentNode.removeChild(target); } for (var i = 0, ci; ci = this.children[i]; i++) { if (ci === source) { this.children.splice(i + 1, 0, target); target.parentNode = this; return target; } } } }, /** * 从当前节点的子节点列表中,移除节点 * @method removeChild * @param { UE.uNode } node 要移除的节点引用 * @param { Boolean } keepChildren 是否保留移除节点的子节点,若传入true,自动把移除节点的子节点插入到移除的位置 * @return { * } 返回刚移除的子节点 * @example * ```javascript * node.removeChild(childNode,true); //在node的子节点列表中移除child节点,并且吧child的子节点插入到移除的位置 * ``` */ removeChild:function (node,keepChildren) { if (this.children) { for (var i = 0, ci; ci = this.children[i]; i++) { if (ci === node) { this.children.splice(i, 1); ci.parentNode = null; if(keepChildren && ci.children && ci.children.length){ for(var j= 0,cj;cj=ci.children[j];j++){ this.children.splice(i+j,0,cj); cj.parentNode = this; } } return ci; } } } }, /** * 获取当前节点所代表的元素属性,即获取attrs对象下的属性值 * @method getAttr * @param { String } attrName 要获取的属性名称 * @return { * } 返回attrs对象下的属性值 * @example * ```javascript * node.getAttr('title'); * ``` */ getAttr:function (attrName) { return this.attrs && this.attrs[attrName.toLowerCase()] }, /** * 设置当前节点所代表的元素属性,即设置attrs对象下的属性值 * @method setAttr * @param { String } attrName 要设置的属性名称 * @param { * } attrVal 要设置的属性值,类型视设置的属性而定 * @return { * } 返回attrs对象下的属性值 * @example * ```javascript * node.setAttr('title','标题'); * ``` */ setAttr:function (attrName, attrVal) { if (!attrName) { delete this.attrs; return; } if(!this.attrs){ this.attrs = {}; } if (utils.isObject(attrName)) { for (var a in attrName) { if (!attrName[a]) { delete this.attrs[a] } else { this.attrs[a.toLowerCase()] = attrName[a]; } } } else { if (!attrVal) { delete this.attrs[attrName] } else { this.attrs[attrName.toLowerCase()] = attrVal; } } }, /** * 获取当前节点在父节点下的位置索引 * @method getIndex * @return { Number } 返回索引数值,如果没有父节点,返回-1 * @example * ```javascript * node.getIndex(); * ``` */ getIndex:function(){ var parent = this.parentNode; for(var i= 0,ci;ci=parent.children[i];i++){ if(ci === this){ return i; } } return -1; }, /** * 在当前节点下,根据id查找节点 * @method getNodeById * @param { String } id 要查找的id * @return { UE.uNode } 返回找到的节点 * @example * ```javascript * node.getNodeById('textId'); * ``` */ getNodeById:function (id) { var node; if (this.children && this.children.length) { for (var i = 0, ci; ci = this.children[i++];) { if (node = getNodeById(ci, id)) { return node; } } } }, /** * 在当前节点下,根据元素名称查找节点列表 * @method getNodesByTagName * @param { String } tagNames 要查找的元素名称 * @return { Array } 返回找到的节点列表 * @example * ```javascript * node.getNodesByTagName('span'); * ``` */ getNodesByTagName:function (tagNames) { tagNames = utils.trim(tagNames).replace(/[ ]{2,}/g, ' ').split(' '); var arr = [], me = this; utils.each(tagNames, function (tagName) { if (me.children && me.children.length) { for (var i = 0, ci; ci = me.children[i++];) { getNodesByTagName(ci, tagName, arr) } } }); return arr; }, /** * 根据样式名称,获取节点的样式值 * @method getStyle * @param { String } name 要获取的样式名称 * @return { String } 返回样式值 * @example * ```javascript * node.getStyle('font-size'); * ``` */ getStyle:function (name) { var cssStyle = this.getAttr('style'); if (!cssStyle) { return '' } var reg = new RegExp('(^|;)\\s*' + name + ':([^;]+)','i'); var match = cssStyle.match(reg); if (match && match[0]) { return match[2] } return ''; }, /** * 给节点设置样式 * @method setStyle * @param { String } name 要设置的的样式名称 * @param { String } val 要设置的的样值 * @example * ```javascript * node.setStyle('font-size', '12px'); * ``` */ setStyle:function (name, val) { function exec(name, val) { var reg = new RegExp('(^|;)\\s*' + name + ':([^;]+;?)', 'gi'); cssStyle = cssStyle.replace(reg, '$1'); if (val) { cssStyle = name + ':' + utils.unhtml(val) + ';' + cssStyle } } var cssStyle = this.getAttr('style'); if (!cssStyle) { cssStyle = ''; } if (utils.isObject(name)) { for (var a in name) { exec(a, name[a]) } } else { exec(name, val) } this.setAttr('style', utils.trim(cssStyle)) }, /** * 传入一个函数,递归遍历当前节点下的所有节点 * @method traversal * @param { Function } fn 遍历到节点的时,传入节点作为参数,运行此函数 * @example * ```javascript * traversal(node, function(){ * console.log(node.type); * }); * ``` */ traversal:function(fn){ if(this.children && this.children.length){ nodeTraversal(this,fn); } return this; } } })(); // core/htmlparser.js /** * html字符串转换成uNode节点 * @file * @module UE * @since 1.2.6.1 */ /** * UEditor公用空间,UEditor所有的功能都挂载在该空间下 * @unfile * @module UE */ /** * html字符串转换成uNode节点的静态方法 * @method htmlparser * @param { String } htmlstr 要转换的html代码 * @param { Boolean } ignoreBlank 若设置为true,转换的时候忽略\n\r\t等空白字符 * @return { uNode } 给定的html片段转换形成的uNode对象 * @example * ```javascript * var root = UE.htmlparser('

    htmlparser

    ', true); * ``` */ var htmlparser = UE.htmlparser = function (htmlstr,ignoreBlank) { //todo 原来的方式 [^"'<>\/] 有\/就不能配对上 ') } html.push('') } //禁止指定table-width return '
    这样的标签了 //先去掉了,加上的原因忘了,这里先记录 var re_tag = /<(?:(?:\/([^>]+)>)|(?:!--([\S|\s]*?)-->)|(?:([^\s\/<>]+)\s*((?:(?:"[^"]*")|(?:'[^']*')|[^"'<>])*)\/?>))/g, re_attr = /([\w\-:.]+)(?:(?:\s*=\s*(?:(?:"([^"]*)")|(?:'([^']*)')|([^\s>]+)))|(?=\s|$))/g; //ie下取得的html可能会有\n存在,要去掉,在处理replace(/[\t\r\n]*/g,'');代码高量的\n不能去除 var allowEmptyTags = { b:1,code:1,i:1,u:1,strike:1,s:1,tt:1,strong:1,q:1,samp:1,em:1,span:1, sub:1,img:1,sup:1,font:1,big:1,small:1,iframe:1,a:1,br:1,pre:1 }; htmlstr = htmlstr.replace(new RegExp(domUtils.fillChar, 'g'), ''); if(!ignoreBlank){ htmlstr = htmlstr.replace(new RegExp('[\\r\\t\\n'+(ignoreBlank?'':' ')+']*<\/?(\\w+)\\s*(?:[^>]*)>[\\r\\t\\n'+(ignoreBlank?'':' ')+']*','g'), function(a,b){ //br暂时单独处理 if(b && allowEmptyTags[b.toLowerCase()]){ return a.replace(/(^[\n\r]+)|([\n\r]+$)/g,''); } return a.replace(new RegExp('^[\\r\\n'+(ignoreBlank?'':' ')+']+'),'').replace(new RegExp('[\\r\\n'+(ignoreBlank?'':' ')+']+$'),''); }); } var notTransAttrs = { 'href':1, 'src':1 }; var uNode = UE.uNode, needParentNode = { 'td':'tr', 'tr':['tbody','thead','tfoot'], 'tbody':'table', 'th':'tr', 'thead':'table', 'tfoot':'table', 'caption':'table', 'li':['ul', 'ol'], 'dt':'dl', 'dd':'dl', 'option':'select' }, needChild = { 'ol':'li', 'ul':'li' }; function text(parent, data) { if(needChild[parent.tagName]){ var tmpNode = uNode.createElement(needChild[parent.tagName]); parent.appendChild(tmpNode); tmpNode.appendChild(uNode.createText(data)); parent = tmpNode; }else{ parent.appendChild(uNode.createText(data)); } } function element(parent, tagName, htmlattr) { var needParentTag; if (needParentTag = needParentNode[tagName]) { var tmpParent = parent,hasParent; while(tmpParent.type != 'root'){ if(utils.isArray(needParentTag) ? utils.indexOf(needParentTag, tmpParent.tagName) != -1 : needParentTag == tmpParent.tagName){ parent = tmpParent; hasParent = true; break; } tmpParent = tmpParent.parentNode; } if(!hasParent){ parent = element(parent, utils.isArray(needParentTag) ? needParentTag[0] : needParentTag) } } //按dtd处理嵌套 // if(parent.type != 'root' && !dtd[parent.tagName][tagName]) // parent = parent.parentNode; var elm = new uNode({ parentNode:parent, type:'element', tagName:tagName.toLowerCase(), //是自闭合的处理一下 children:dtd.$empty[tagName] ? null : [] }); //如果属性存在,处理属性 if (htmlattr) { var attrs = {}, match; while (match = re_attr.exec(htmlattr)) { attrs[match[1].toLowerCase()] = notTransAttrs[match[1].toLowerCase()] ? (match[2] || match[3] || match[4]) : utils.unhtml(match[2] || match[3] || match[4]) } elm.attrs = attrs; } //trace:3970 // //如果parent下不能放elm // if(dtd.$inline[parent.tagName] && dtd.$block[elm.tagName] && !dtd[parent.tagName][elm.tagName]){ // parent = parent.parentNode; // elm.parentNode = parent; // } parent.children.push(elm); //如果是自闭合节点返回父亲节点 return dtd.$empty[tagName] ? parent : elm } function comment(parent, data) { parent.children.push(new uNode({ type:'comment', data:data, parentNode:parent })); } var match, currentIndex = 0, nextIndex = 0; //设置根节点 var root = new uNode({ type:'root', children:[] }); var currentParent = root; while (match = re_tag.exec(htmlstr)) { currentIndex = match.index; try{ if (currentIndex > nextIndex) { //text node text(currentParent, htmlstr.slice(nextIndex, currentIndex)); } if (match[3]) { if(dtd.$cdata[currentParent.tagName]){ text(currentParent, match[0]); }else{ //start tag currentParent = element(currentParent, match[3].toLowerCase(), match[4]); } } else if (match[1]) { if(currentParent.type != 'root'){ if(dtd.$cdata[currentParent.tagName] && !dtd.$cdata[match[1]]){ text(currentParent, match[0]); }else{ var tmpParent = currentParent; while(currentParent.type == 'element' && currentParent.tagName != match[1].toLowerCase()){ currentParent = currentParent.parentNode; if(currentParent.type == 'root'){ currentParent = tmpParent; throw 'break' } } //end tag currentParent = currentParent.parentNode; } } } else if (match[2]) { //comment comment(currentParent, match[2]) } }catch(e){} nextIndex = re_tag.lastIndex; } //如果结束是文本,就有可能丢掉,所以这里手动判断一下 //例如
  • sdfsdfsdf
  • sdfsdfsdfsdf if (nextIndex < htmlstr.length) { text(currentParent, htmlstr.slice(nextIndex)); } return root; }; // core/filternode.js /** * UE过滤节点的静态方法 * @file */ /** * UEditor公用空间,UEditor所有的功能都挂载在该空间下 * @module UE */ /** * 根据传入节点和过滤规则过滤相应节点 * @module UE * @since 1.2.6.1 * @method filterNode * @param { Object } root 指定root节点 * @param { Object } rules 过滤规则json对象 * @example * ```javascript * UE.filterNode(root,editor.options.filterRules); * ``` */ var filterNode = UE.filterNode = function () { function filterNode(node,rules){ switch (node.type) { case 'text': break; case 'element': var val; if(val = rules[node.tagName]){ if(val === '-'){ node.parentNode.removeChild(node) }else if(utils.isFunction(val)){ var parentNode = node.parentNode, index = node.getIndex(); val(node); if(node.parentNode){ if(node.children){ for(var i = 0,ci;ci=node.children[i];){ filterNode(ci,rules); if(ci.parentNode){ i++; } } } }else{ for(var i = index,ci;ci=parentNode.children[i];){ filterNode(ci,rules); if(ci.parentNode){ i++; } } } }else{ var attrs = val['$']; if(attrs && node.attrs){ var tmpAttrs = {},tmpVal; for(var a in attrs){ tmpVal = node.getAttr(a); //todo 只先对style单独处理 if(a == 'style' && utils.isArray(attrs[a])){ var tmpCssStyle = []; utils.each(attrs[a],function(v){ var tmp; if(tmp = node.getStyle(v)){ tmpCssStyle.push(v + ':' + tmp); } }); tmpVal = tmpCssStyle.join(';') } if(tmpVal){ tmpAttrs[a] = tmpVal; } } node.attrs = tmpAttrs; } if(node.children){ for(var i = 0,ci;ci=node.children[i];){ filterNode(ci,rules); if(ci.parentNode){ i++; } } } } }else{ //如果不在名单里扣出子节点并删除该节点,cdata除外 if(dtd.$cdata[node.tagName]){ node.parentNode.removeChild(node) }else{ var parentNode = node.parentNode, index = node.getIndex(); node.parentNode.removeChild(node,true); for(var i = index,ci;ci=parentNode.children[i];){ filterNode(ci,rules); if(ci.parentNode){ i++; } } } } break; case 'comment': node.parentNode.removeChild(node) } } return function(root,rules){ if(utils.isEmptyObject(rules)){ return root; } var val; if(val = rules['-']){ utils.each(val.split(' '),function(k){ rules[k] = '-' }) } for(var i= 0,ci;ci=root.children[i];){ filterNode(ci,rules); if(ci.parentNode){ i++; } } return root; } }(); // core/plugin.js /** * Created with JetBrains PhpStorm. * User: campaign * Date: 10/8/13 * Time: 6:15 PM * To change this template use File | Settings | File Templates. */ UE.plugin = function(){ var _plugins = {}; return { register : function(pluginName,fn,oldOptionName,afterDisabled){ if(oldOptionName && utils.isFunction(oldOptionName)){ afterDisabled = oldOptionName; oldOptionName = null } _plugins[pluginName] = { optionName : oldOptionName || pluginName, execFn : fn, //当插件被禁用时执行 afterDisabled : afterDisabled } }, load : function(editor){ utils.each(_plugins,function(plugin){ var _export = plugin.execFn.call(editor); if(editor.options[plugin.optionName] !== false){ if(_export){ //后边需要再做扩展 utils.each(_export,function(v,k){ switch(k.toLowerCase()){ case 'shortcutkey': editor.addshortcutkey(v); break; case 'bindevents': utils.each(v,function(fn,eventName){ editor.addListener(eventName,fn); }); break; case 'bindmultievents': utils.each(utils.isArray(v) ? v:[v],function(event){ var types = utils.trim(event.type).split(/\s+/); utils.each(types,function(eventName){ editor.addListener(eventName, event.handler); }); }); break; case 'commands': utils.each(v,function(execFn,execName){ editor.commands[execName] = execFn }); break; case 'outputrule': editor.addOutputRule(v); break; case 'inputrule': editor.addInputRule(v); break; case 'defaultoptions': editor.setOpt(v) } }) } }else if(plugin.afterDisabled){ plugin.afterDisabled.call(editor) } }); //向下兼容 utils.each(UE.plugins,function(plugin){ plugin.call(editor); }); }, run : function(pluginName,editor){ var plugin = _plugins[pluginName]; if(plugin){ plugin.exeFn.call(editor) } } } }(); // core/keymap.js var keymap = UE.keymap = { 'Backspace' : 8, 'Tab' : 9, 'Enter' : 13, 'Shift':16, 'Control':17, 'Alt':18, 'CapsLock':20, 'Esc':27, 'Spacebar':32, 'PageUp':33, 'PageDown':34, 'End':35, 'Home':36, 'Left':37, 'Up':38, 'Right':39, 'Down':40, 'Insert':45, 'Del':46, 'NumLock':144, 'Cmd':91, '=':187, '-':189, "b":66, 'i':73, //回退 'z':90, 'y':89, //粘贴 'v' : 86, 'x' : 88, 's' : 83, 'n' : 78 }; // core/localstorage.js //存储媒介封装 var LocalStorage = UE.LocalStorage = (function () { var storage = window.localStorage || getUserData() || null, LOCAL_FILE = 'localStorage'; return { saveLocalData: function (key, data) { if (storage && data) { storage.setItem(key, data); return true; } return false; }, getLocalData: function (key) { if (storage) { return storage.getItem(key); } return null; }, removeItem: function (key) { storage && storage.removeItem(key); } }; function getUserData() { var container = document.createElement("div"); container.style.display = "none"; if (!container.addBehavior) { return null; } container.addBehavior("#default#userdata"); return { getItem: function (key) { var result = null; try { document.body.appendChild(container); container.load(LOCAL_FILE); result = container.getAttribute(key); document.body.removeChild(container); } catch (e) { } return result; }, setItem: function (key, value) { document.body.appendChild(container); container.setAttribute(key, value); container.save(LOCAL_FILE); document.body.removeChild(container); }, //// 暂时没有用到 //clear: function () { // // var expiresTime = new Date(); // expiresTime.setFullYear(expiresTime.getFullYear() - 1); // document.body.appendChild(container); // container.expires = expiresTime.toUTCString(); // container.save(LOCAL_FILE); // document.body.removeChild(container); // //}, removeItem: function (key) { document.body.appendChild(container); container.removeAttribute(key); container.save(LOCAL_FILE); document.body.removeChild(container); } }; } })(); (function () { var ROOTKEY = 'ueditor_preference'; UE.Editor.prototype.setPreferences = function(key,value){ var obj = {}; if (utils.isString(key)) { obj[ key ] = value; } else { obj = key; } var data = LocalStorage.getLocalData(ROOTKEY); if (data && (data = utils.str2json(data))) { utils.extend(data, obj); } else { data = obj; } data && LocalStorage.saveLocalData(ROOTKEY, utils.json2str(data)); }; UE.Editor.prototype.getPreferences = function(key){ var data = LocalStorage.getLocalData(ROOTKEY); if (data && (data = utils.str2json(data))) { return key ? data[key] : data } return null; }; UE.Editor.prototype.removePreferences = function (key) { var data = LocalStorage.getLocalData(ROOTKEY); if (data && (data = utils.str2json(data))) { data[key] = undefined; delete data[key] } data && LocalStorage.saveLocalData(ROOTKEY, utils.json2str(data)); }; })(); // plugins/defaultfilter.js ///import core ///plugin 编辑器默认的过滤转换机制 UE.plugins['defaultfilter'] = function () { var me = this; me.setOpt({ 'allowDivTransToP':true, 'disabledTableInTable':true }); //默认的过滤处理 //进入编辑器的内容处理 me.addInputRule(function (root) { var allowDivTransToP = this.options.allowDivTransToP; var val; function tdParent(node){ while(node && node.type == 'element'){ if(node.tagName == 'td'){ return true; } node = node.parentNode; } return false; } //进行默认的处理 root.traversal(function (node) { if (node.type == 'element') { if (!dtd.$cdata[node.tagName] && me.options.autoClearEmptyNode && dtd.$inline[node.tagName] && !dtd.$empty[node.tagName] && (!node.attrs || utils.isEmptyObject(node.attrs))) { if (!node.firstChild()) node.parentNode.removeChild(node); else if (node.tagName == 'span' && (!node.attrs || utils.isEmptyObject(node.attrs))) { node.parentNode.removeChild(node, true) } return; } switch (node.tagName) { case 'style': case 'script': node.setAttr({ cdata_tag: node.tagName, cdata_data: (node.innerHTML() || ''), '_ue_custom_node_':'true' }); node.tagName = 'div'; node.innerHTML(''); break; case 'a': if (val = node.getAttr('href')) { node.setAttr('_href', val) } break; case 'img': //todo base64暂时去掉,后边做远程图片上传后,干掉这个 if (val = node.getAttr('src')) { if (/^data:/.test(val)) { node.parentNode.removeChild(node); break; } } node.setAttr('_src', node.getAttr('src')); break; case 'span': if (browser.webkit && (val = node.getStyle('white-space'))) { if (/nowrap|normal/.test(val)) { node.setStyle('white-space', ''); if (me.options.autoClearEmptyNode && utils.isEmptyObject(node.attrs)) { node.parentNode.removeChild(node, true) } } } val = node.getAttr('id'); if(val && /^_baidu_bookmark_/i.test(val)){ node.parentNode.removeChild(node) } break; case 'p': if (val = node.getAttr('align')) { node.setAttr('align'); node.setStyle('text-align', val) } //trace:3431 // var cssStyle = node.getAttr('style'); // if (cssStyle) { // cssStyle = cssStyle.replace(/(margin|padding)[^;]+/g, ''); // node.setAttr('style', cssStyle) // // } //p标签不允许嵌套 utils.each(node.children,function(n){ if(n.type == 'element' && n.tagName == 'p'){ var next = n.nextSibling(); node.parentNode.insertAfter(n,node); var last = n; while(next){ var tmp = next.nextSibling(); node.parentNode.insertAfter(next,last); last = next; next = tmp; } return false; } }); if (!node.firstChild()) { node.innerHTML(browser.ie ? ' ' : '
    ') } break; case 'div': if(node.getAttr('cdata_tag')){ break; } //针对代码这里不处理插入代码的div val = node.getAttr('class'); if(val && /^line number\d+/.test(val)){ break; } if(!allowDivTransToP){ break; } var tmpNode, p = UE.uNode.createElement('p'); while (tmpNode = node.firstChild()) { if (tmpNode.type == 'text' || !UE.dom.dtd.$block[tmpNode.tagName]) { p.appendChild(tmpNode); } else { if (p.firstChild()) { node.parentNode.insertBefore(p, node); p = UE.uNode.createElement('p'); } else { node.parentNode.insertBefore(tmpNode, node); } } } if (p.firstChild()) { node.parentNode.insertBefore(p, node); } node.parentNode.removeChild(node); break; case 'dl': node.tagName = 'ul'; break; case 'dt': case 'dd': node.tagName = 'li'; break; case 'li': var className = node.getAttr('class'); if (!className || !/list\-/.test(className)) { node.setAttr() } var tmpNodes = node.getNodesByTagName('ol ul'); UE.utils.each(tmpNodes, function (n) { node.parentNode.insertAfter(n, node); }); break; case 'td': case 'th': case 'caption': if(!node.children || !node.children.length){ node.appendChild(browser.ie11below ? UE.uNode.createText(' ') : UE.uNode.createElement('br')) } break; case 'table': if(me.options.disabledTableInTable && tdParent(node)){ node.parentNode.insertBefore(UE.uNode.createText(node.innerText()),node); node.parentNode.removeChild(node) } } } // if(node.type == 'comment'){ // node.parentNode.removeChild(node); // } }) }); //从编辑器出去的内容处理 me.addOutputRule(function (root) { var val; root.traversal(function (node) { if (node.type == 'element') { if (me.options.autoClearEmptyNode && dtd.$inline[node.tagName] && !dtd.$empty[node.tagName] && (!node.attrs || utils.isEmptyObject(node.attrs))) { if (!node.firstChild()) node.parentNode.removeChild(node); else if (node.tagName == 'span' && (!node.attrs || utils.isEmptyObject(node.attrs))) { node.parentNode.removeChild(node, true) } return; } switch (node.tagName) { case 'div': if (val = node.getAttr('cdata_tag')) { node.tagName = val; node.appendChild(UE.uNode.createText(node.getAttr('cdata_data'))); node.setAttr({cdata_tag: '', cdata_data: '','_ue_custom_node_':''}); } break; case 'a': if (val = node.getAttr('_href')) { node.setAttr({ 'href': utils.html(val), '_href': '' }) } break; break; case 'span': val = node.getAttr('id'); if(val && /^_baidu_bookmark_/i.test(val)){ node.parentNode.removeChild(node) } break; case 'img': if (val = node.getAttr('_src')) { node.setAttr({ 'src': node.getAttr('_src'), '_src': '' }) } } } }) }); }; // plugins/inserthtml.js /** * 插入html字符串插件 * @file * @since 1.2.6.1 */ /** * 插入html代码 * @command inserthtml * @method execCommand * @param { String } cmd 命令字符串 * @param { String } html 插入的html字符串 * @remaind 插入的标签内容是在当前的选区位置上插入,如果当前是闭合状态,那直接插入内容, 如果当前是选中状态,将先清除当前选中内容后,再做插入 * @warning 注意:该命令会对当前选区的位置,对插入的内容进行过滤转换处理。 过滤的规则遵循html语意化的原则。 * @example * ```javascript * //xxx[BB]xxx 当前选区为非闭合选区,选中BB这两个文本 * //执行命令,插入CC * //插入后的效果 xxxCCxxx * //

    xx|xxx

    当前选区为闭合状态 * //插入

    CC

    * //结果

    xx

    CC

    xxx

    * //

    xxxx

    |

    xxx

    当前选区在两个p标签之间 * //插入 xxxx * //结果

    xxxx

    xxxx

    xxx

    * ``` */ UE.commands['inserthtml'] = { execCommand: function (command,html,notNeedFilter){ var me = this, range, div; if(!html){ return; } if(me.fireEvent('beforeinserthtml',html) === true){ return; } range = me.selection.getRange(); div = range.document.createElement( 'div' ); div.style.display = 'inline'; if (!notNeedFilter) { var root = UE.htmlparser(html); //如果给了过滤规则就先进行过滤 if(me.options.filterRules){ UE.filterNode(root,me.options.filterRules); } //执行默认的处理 me.filterInputRule(root); html = root.toHtml() } div.innerHTML = utils.trim( html ); if ( !range.collapsed ) { var tmpNode = range.startContainer; if(domUtils.isFillChar(tmpNode)){ range.setStartBefore(tmpNode) } tmpNode = range.endContainer; if(domUtils.isFillChar(tmpNode)){ range.setEndAfter(tmpNode) } range.txtToElmBoundary(); //结束边界可能放到了br的前边,要把br包含进来 // x[xxx]
    if(range.endContainer && range.endContainer.nodeType == 1){ tmpNode = range.endContainer.childNodes[range.endOffset]; if(tmpNode && domUtils.isBr(tmpNode)){ range.setEndAfter(tmpNode); } } if(range.startOffset == 0){ tmpNode = range.startContainer; if(domUtils.isBoundaryNode(tmpNode,'firstChild') ){ tmpNode = range.endContainer; if(range.endOffset == (tmpNode.nodeType == 3 ? tmpNode.nodeValue.length : tmpNode.childNodes.length) && domUtils.isBoundaryNode(tmpNode,'lastChild')){ me.body.innerHTML = '

    '+(browser.ie ? '' : '
    ')+'

    '; range.setStart(me.body.firstChild,0).collapse(true) } } } !range.collapsed && range.deleteContents(); if(range.startContainer.nodeType == 1){ var child = range.startContainer.childNodes[range.startOffset],pre; if(child && domUtils.isBlockElm(child) && (pre = child.previousSibling) && domUtils.isBlockElm(pre)){ range.setEnd(pre,pre.childNodes.length).collapse(); while(child.firstChild){ pre.appendChild(child.firstChild); } domUtils.remove(child); } } } var child,parent,pre,tmp,hadBreak = 0, nextNode; //如果当前位置选中了fillchar要干掉,要不会产生空行 if(range.inFillChar()){ child = range.startContainer; if(domUtils.isFillChar(child)){ range.setStartBefore(child).collapse(true); domUtils.remove(child); }else if(domUtils.isFillChar(child,true)){ child.nodeValue = child.nodeValue.replace(fillCharReg,''); range.startOffset--; range.collapsed && range.collapse(true) } } //列表单独处理 var li = domUtils.findParentByTagName(range.startContainer,'li',true); if(li){ var next,last; while(child = div.firstChild){ //针对hr单独处理一下先 while(child && (child.nodeType == 3 || !domUtils.isBlockElm(child) || child.tagName=='HR' )){ next = child.nextSibling; range.insertNode( child).collapse(); last = child; child = next; } if(child){ if(/^(ol|ul)$/i.test(child.tagName)){ while(child.firstChild){ last = child.firstChild; domUtils.insertAfter(li,child.firstChild); li = li.nextSibling; } domUtils.remove(child) }else{ var tmpLi; next = child.nextSibling; tmpLi = me.document.createElement('li'); domUtils.insertAfter(li,tmpLi); tmpLi.appendChild(child); last = child; child = next; li = tmpLi; } } } li = domUtils.findParentByTagName(range.startContainer,'li',true); if(domUtils.isEmptyBlock(li)){ domUtils.remove(li) } if(last){ range.setStartAfter(last).collapse(true).select(true) } }else{ while ( child = div.firstChild ) { if(hadBreak){ var p = me.document.createElement('p'); while(child && (child.nodeType == 3 || !dtd.$block[child.tagName])){ nextNode = child.nextSibling; p.appendChild(child); child = nextNode; } if(p.firstChild){ child = p } } range.insertNode( child ); nextNode = child.nextSibling; if ( !hadBreak && child.nodeType == domUtils.NODE_ELEMENT && domUtils.isBlockElm( child ) ){ parent = domUtils.findParent( child,function ( node ){ return domUtils.isBlockElm( node ); } ); if ( parent && parent.tagName.toLowerCase() != 'body' && !(dtd[parent.tagName][child.nodeName] && child.parentNode === parent)){ if(!dtd[parent.tagName][child.nodeName]){ pre = parent; }else{ tmp = child.parentNode; while (tmp !== parent){ pre = tmp; tmp = tmp.parentNode; } } domUtils.breakParent( child, pre || tmp ); //去掉break后前一个多余的节点

    |<[p> ==>

    |

    var pre = child.previousSibling; domUtils.trimWhiteTextNode(pre); if(!pre.childNodes.length){ domUtils.remove(pre); } //trace:2012,在非ie的情况,切开后剩下的节点有可能不能点入光标添加br占位 if(!browser.ie && (next = child.nextSibling) && domUtils.isBlockElm(next) && next.lastChild && !domUtils.isBr(next.lastChild)){ next.appendChild(me.document.createElement('br')); } hadBreak = 1; } } var next = child.nextSibling; if(!div.firstChild && next && domUtils.isBlockElm(next)){ range.setStart(next,0).collapse(true); break; } range.setEndAfter( child ).collapse(); } child = range.startContainer; if(nextNode && domUtils.isBr(nextNode)){ domUtils.remove(nextNode) } //用chrome可能有空白展位符 if(domUtils.isBlockElm(child) && domUtils.isEmptyNode(child)){ if(nextNode = child.nextSibling){ domUtils.remove(child); if(nextNode.nodeType == 1 && dtd.$block[nextNode.tagName]){ range.setStart(nextNode,0).collapse(true).shrinkBoundary() } }else{ try{ child.innerHTML = browser.ie ? domUtils.fillChar : '
    '; }catch(e){ range.setStartBefore(child); domUtils.remove(child) } } } //加上true因为在删除表情等时会删两次,第一次是删的fillData try{ range.select(true); }catch(e){} } setTimeout(function(){ range = me.selection.getRange(); range.scrollToView(me.autoHeightEnabled,me.autoHeightEnabled ? domUtils.getXY(me.iframe).y:0); me.fireEvent('afterinserthtml', html); },200); } }; // plugins/autotypeset.js /** * 自动排版 * @file * @since 1.2.6.1 */ /** * 对当前编辑器的内容执行自动排版, 排版的行为根据config配置文件里的“autotypeset”选项进行控制。 * @command autotypeset * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'autotypeset' ); * ``` */ UE.plugins['autotypeset'] = function(){ this.setOpt({'autotypeset': { mergeEmptyline: true, //合并空行 removeClass: true, //去掉冗余的class removeEmptyline: false, //去掉空行 textAlign:"left", //段落的排版方式,可以是 left,right,center,justify 去掉这个属性表示不执行排版 imageBlockLine: 'center', //图片的浮动方式,独占一行剧中,左右浮动,默认: center,left,right,none 去掉这个属性表示不执行排版 pasteFilter: false, //根据规则过滤没事粘贴进来的内容 clearFontSize: false, //去掉所有的内嵌字号,使用编辑器默认的字号 clearFontFamily: false, //去掉所有的内嵌字体,使用编辑器默认的字体 removeEmptyNode: false, // 去掉空节点 //可以去掉的标签 removeTagNames: utils.extend({div:1},dtd.$removeEmpty), indent: false, // 行首缩进 indentValue : '2em', //行首缩进的大小 bdc2sb: false, tobdc: false }}); var me = this, opt = me.options.autotypeset, remainClass = { 'selectTdClass':1, 'pagebreak':1, 'anchorclass':1 }, remainTag = { 'li':1 }, tags = { div:1, p:1, //trace:2183 这些也认为是行 blockquote:1,center:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1, span:1 }, highlightCont; //升级了版本,但配置项目里没有autotypeset if(!opt){ return; } readLocalOpts(); function isLine(node,notEmpty){ if(!node || node.nodeType == 3) return 0; if(domUtils.isBr(node)) return 1; if(node && node.parentNode && tags[node.tagName.toLowerCase()]){ if(highlightCont && highlightCont.contains(node) || node.getAttribute('pagebreak') ){ return 0; } return notEmpty ? !domUtils.isEmptyBlock(node) : domUtils.isEmptyBlock(node,new RegExp('[\\s'+domUtils.fillChar +']','g')); } } function removeNotAttributeSpan(node){ if(!node.style.cssText){ domUtils.removeAttributes(node,['style']); if(node.tagName.toLowerCase() == 'span' && domUtils.hasNoAttributes(node)){ domUtils.remove(node,true); } } } function autotype(type,html){ var me = this,cont; if(html){ if(!opt.pasteFilter){ return; } cont = me.document.createElement('div'); cont.innerHTML = html.html; }else{ cont = me.document.body; } var nodes = domUtils.getElementsByTagName(cont,'*'); // 行首缩进,段落方向,段间距,段内间距 for(var i=0,ci;ci=nodes[i++];){ if(me.fireEvent('excludeNodeinautotype',ci) === true){ continue; } //font-size if(opt.clearFontSize && ci.style.fontSize){ domUtils.removeStyle(ci,'font-size'); removeNotAttributeSpan(ci); } //font-family if(opt.clearFontFamily && ci.style.fontFamily){ domUtils.removeStyle(ci,'font-family'); removeNotAttributeSpan(ci); } if(isLine(ci)){ //合并空行 if(opt.mergeEmptyline ){ var next = ci.nextSibling,tmpNode,isBr = domUtils.isBr(ci); while(isLine(next)){ tmpNode = next; next = tmpNode.nextSibling; if(isBr && (!next || next && !domUtils.isBr(next))){ break; } domUtils.remove(tmpNode); } } //去掉空行,保留占位的空行 if(opt.removeEmptyline && domUtils.inDoc(ci,cont) && !remainTag[ci.parentNode.tagName.toLowerCase()] ){ if(domUtils.isBr(ci)){ next = ci.nextSibling; if(next && !domUtils.isBr(next)){ continue; } } domUtils.remove(ci); continue; } } if(isLine(ci,true) && ci.tagName != 'SPAN'){ if(opt.indent){ ci.style.textIndent = opt.indentValue; } if(opt.textAlign){ ci.style.textAlign = opt.textAlign; } // if(opt.lineHeight) // ci.style.lineHeight = opt.lineHeight + 'cm'; } //去掉class,保留的class不去掉 if(opt.removeClass && ci.className && !remainClass[ci.className.toLowerCase()]){ if(highlightCont && highlightCont.contains(ci)){ continue; } domUtils.removeAttributes(ci,['class']); } //表情不处理 if(opt.imageBlockLine && ci.tagName.toLowerCase() == 'img' && !ci.getAttribute('emotion')){ if(html){ var img = ci; switch (opt.imageBlockLine){ case 'left': case 'right': case 'none': var pN = img.parentNode,tmpNode,pre,next; while(dtd.$inline[pN.tagName] || pN.tagName == 'A'){ pN = pN.parentNode; } tmpNode = pN; if(tmpNode.tagName == 'P' && domUtils.getStyle(tmpNode,'text-align') == 'center'){ if(!domUtils.isBody(tmpNode) && domUtils.getChildCount(tmpNode,function(node){return !domUtils.isBr(node) && !domUtils.isWhitespace(node)}) == 1){ pre = tmpNode.previousSibling; next = tmpNode.nextSibling; if(pre && next && pre.nodeType == 1 && next.nodeType == 1 && pre.tagName == next.tagName && domUtils.isBlockElm(pre)){ pre.appendChild(tmpNode.firstChild); while(next.firstChild){ pre.appendChild(next.firstChild); } domUtils.remove(tmpNode); domUtils.remove(next); }else{ domUtils.setStyle(tmpNode,'text-align',''); } } } domUtils.setStyle(img,'float', opt.imageBlockLine); break; case 'center': if(me.queryCommandValue('imagefloat') != 'center'){ pN = img.parentNode; domUtils.setStyle(img,'float','none'); tmpNode = img; while(pN && domUtils.getChildCount(pN,function(node){return !domUtils.isBr(node) && !domUtils.isWhitespace(node)}) == 1 && (dtd.$inline[pN.tagName] || pN.tagName == 'A')){ tmpNode = pN; pN = pN.parentNode; } var pNode = me.document.createElement('p'); domUtils.setAttributes(pNode,{ style:'text-align:center' }); tmpNode.parentNode.insertBefore(pNode,tmpNode); pNode.appendChild(tmpNode); domUtils.setStyle(tmpNode,'float',''); } } } else { var range = me.selection.getRange(); range.selectNode(ci).select(); me.execCommand('imagefloat', opt.imageBlockLine); } } //去掉冗余的标签 if(opt.removeEmptyNode){ if(opt.removeTagNames[ci.tagName.toLowerCase()] && domUtils.hasNoAttributes(ci) && domUtils.isEmptyBlock(ci)){ domUtils.remove(ci); } } } if(opt.tobdc){ var root = UE.htmlparser(cont.innerHTML); root.traversal(function(node){ if(node.type == 'text'){ node.data = ToDBC(node.data) } }); cont.innerHTML = root.toHtml() } if(opt.bdc2sb){ var root = UE.htmlparser(cont.innerHTML); root.traversal(function(node){ if(node.type == 'text'){ node.data = DBC2SB(node.data) } }); cont.innerHTML = root.toHtml() } if(html){ html.html = cont.innerHTML; } } if(opt.pasteFilter){ me.addListener('beforepaste',autotype); } function DBC2SB(str) { var result = ''; for (var i = 0; i < str.length; i++) { var code = str.charCodeAt(i); //获取当前字符的unicode编码 if (code >= 65281 && code <= 65373)//在这个unicode编码范围中的是所有的英文字母已经各种字符 { result += String.fromCharCode(str.charCodeAt(i) - 65248); //把全角字符的unicode编码转换为对应半角字符的unicode码 } else if (code == 12288)//空格 { result += String.fromCharCode(str.charCodeAt(i) - 12288 + 32); } else { result += str.charAt(i); } } return result; } function ToDBC(txtstring) { txtstring = utils.html(txtstring); var tmp = ""; var mark = "";/*用于判断,如果是html尖括里的标记,则不进行全角的转换*/ for (var i = 0; i < txtstring.length; i++) { if (txtstring.charCodeAt(i) == 32) { tmp = tmp + String.fromCharCode(12288); } else if (txtstring.charCodeAt(i) < 127) { tmp = tmp + String.fromCharCode(txtstring.charCodeAt(i) + 65248); } else { tmp += txtstring.charAt(i); } } return tmp; } function readLocalOpts() { var cookieOpt = me.getPreferences('autotypeset'); utils.extend(me.options.autotypeset, cookieOpt); } me.commands['autotypeset'] = { execCommand:function () { me.removeListener('beforepaste',autotype); if(opt.pasteFilter){ me.addListener('beforepaste',autotype); } autotype.call(me) } }; }; // plugins/autosubmit.js /** * 快捷键提交 * @file * @since 1.2.6.1 */ /** * 提交表单 * @command autosubmit * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'autosubmit' ); * ``` */ UE.plugin.register('autosubmit',function(){ return { shortcutkey:{ "autosubmit":"ctrl+13" //手动提交 }, commands:{ 'autosubmit':{ execCommand:function () { var me=this, form = domUtils.findParentByTagName(me.iframe,"form", false); if (form){ if(me.fireEvent("beforesubmit")===false){ return; } me.sync(); form.submit(); } } } } } }); // plugins/background.js /** * 背景插件,为UEditor提供设置背景功能 * @file * @since 1.2.6.1 */ UE.plugin.register('background', function () { var me = this, cssRuleId = 'editor_background', isSetColored, reg = new RegExp('body[\\s]*\\{(.+)\\}', 'i'); function stringToObj(str) { var obj = {}, styles = str.split(';'); utils.each(styles, function (v) { var index = v.indexOf(':'), key = utils.trim(v.substr(0, index)).toLowerCase(); key && (obj[key] = utils.trim(v.substr(index + 1) || '')); }); return obj; } function setBackground(obj) { if (obj) { var styles = []; for (var name in obj) { if (obj.hasOwnProperty(name)) { styles.push(name + ":" + obj[name] + '; '); } } utils.cssRule(cssRuleId, styles.length ? ('body{' + styles.join("") + '}') : '', me.document); } else { utils.cssRule(cssRuleId, '', me.document) } } //重写editor.hasContent方法 var orgFn = me.hasContents; me.hasContents = function(){ if(me.queryCommandValue('background')){ return true } return orgFn.apply(me,arguments); }; return { bindEvents: { 'getAllHtml': function (type, headHtml) { var body = this.body, su = domUtils.getComputedStyle(body, "background-image"), url = ""; if (su.indexOf(me.options.imagePath) > 0) { url = su.substring(su.indexOf(me.options.imagePath), su.length - 1).replace(/"|\(|\)/ig, ""); } else { url = su != "none" ? su.replace(/url\("?|"?\)/ig, "") : ""; } var html = ' '; headHtml.push(html); }, 'aftersetcontent': function () { if(isSetColored == false) setBackground(); } }, inputRule: function (root) { isSetColored = false; utils.each(root.getNodesByTagName('p'), function (p) { var styles = p.getAttr('data-background'); if (styles) { isSetColored = true; setBackground(stringToObj(styles)); p.parentNode.removeChild(p); } }) }, outputRule: function (root) { var me = this, styles = (utils.cssRule(cssRuleId, me.document) || '').replace(/[\n\r]+/g, '').match(reg); if (styles) { root.appendChild(UE.uNode.createElement('


    ')); } }, commands: { 'background': { execCommand: function (cmd, obj) { setBackground(obj); }, queryCommandValue: function () { var me = this, styles = (utils.cssRule(cssRuleId, me.document) || '').replace(/[\n\r]+/g, '').match(reg); return styles ? stringToObj(styles[1]) : null; }, notNeedUndo: true } } } }); // plugins/image.js /** * 图片插入、排版插件 * @file * @since 1.2.6.1 */ /** * 图片对齐方式 * @command imagefloat * @method execCommand * @remind 值center为独占一行居中 * @param { String } cmd 命令字符串 * @param { String } align 对齐方式,可传left、right、none、center * @remaind center表示图片独占一行 * @example * ```javascript * editor.execCommand( 'imagefloat', 'center' ); * ``` */ /** * 如果选区所在位置是图片区域 * @command imagefloat * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { String } 返回图片对齐方式 * @example * ```javascript * editor.queryCommandValue( 'imagefloat' ); * ``` */ UE.commands['imagefloat'] = { execCommand:function (cmd, align) { var me = this, range = me.selection.getRange(); if (!range.collapsed) { var img = range.getClosedNode(); if (img && img.tagName == 'IMG') { switch (align) { case 'left': case 'right': case 'none': var pN = img.parentNode, tmpNode, pre, next; while (dtd.$inline[pN.tagName] || pN.tagName == 'A') { pN = pN.parentNode; } tmpNode = pN; if (tmpNode.tagName == 'P' && domUtils.getStyle(tmpNode, 'text-align') == 'center') { if (!domUtils.isBody(tmpNode) && domUtils.getChildCount(tmpNode, function (node) { return !domUtils.isBr(node) && !domUtils.isWhitespace(node); }) == 1) { pre = tmpNode.previousSibling; next = tmpNode.nextSibling; if (pre && next && pre.nodeType == 1 && next.nodeType == 1 && pre.tagName == next.tagName && domUtils.isBlockElm(pre)) { pre.appendChild(tmpNode.firstChild); while (next.firstChild) { pre.appendChild(next.firstChild); } domUtils.remove(tmpNode); domUtils.remove(next); } else { domUtils.setStyle(tmpNode, 'text-align', ''); } } range.selectNode(img).select(); } domUtils.setStyle(img, 'float', align == 'none' ? '' : align); if(align == 'none'){ domUtils.removeAttributes(img,'align'); } break; case 'center': if (me.queryCommandValue('imagefloat') != 'center') { pN = img.parentNode; domUtils.setStyle(img, 'float', ''); domUtils.removeAttributes(img,'align'); tmpNode = img; while (pN && domUtils.getChildCount(pN, function (node) { return !domUtils.isBr(node) && !domUtils.isWhitespace(node); }) == 1 && (dtd.$inline[pN.tagName] || pN.tagName == 'A')) { tmpNode = pN; pN = pN.parentNode; } range.setStartBefore(tmpNode).setCursor(false); pN = me.document.createElement('div'); pN.appendChild(tmpNode); domUtils.setStyle(tmpNode, 'float', ''); me.execCommand('insertHtml', '

    ' + pN.innerHTML + '

    '); tmpNode = me.document.getElementById('_img_parent_tmp'); tmpNode.removeAttribute('id'); tmpNode = tmpNode.firstChild; range.selectNode(tmpNode).select(); //去掉后边多余的元素 next = tmpNode.parentNode.nextSibling; if (next && domUtils.isEmptyNode(next)) { domUtils.remove(next); } } break; } } } }, queryCommandValue:function () { var range = this.selection.getRange(), startNode, floatStyle; if (range.collapsed) { return 'none'; } startNode = range.getClosedNode(); if (startNode && startNode.nodeType == 1 && startNode.tagName == 'IMG') { floatStyle = domUtils.getComputedStyle(startNode, 'float') || startNode.getAttribute('align'); if (floatStyle == 'none') { floatStyle = domUtils.getComputedStyle(startNode.parentNode, 'text-align') == 'center' ? 'center' : floatStyle; } return { left:1, right:1, center:1 }[floatStyle] ? floatStyle : 'none'; } return 'none'; }, queryCommandState:function () { var range = this.selection.getRange(), startNode; if (range.collapsed) return -1; startNode = range.getClosedNode(); if (startNode && startNode.nodeType == 1 && startNode.tagName == 'IMG') { return 0; } return -1; } }; /** * 插入图片 * @command insertimage * @method execCommand * @param { String } cmd 命令字符串 * @param { Object } opt 属性键值对,这些属性都将被复制到当前插入图片 * @remind 该命令第二个参数可接受一个图片配置项对象的数组,可以插入多张图片, * 此时数组的每一个元素都是一个Object类型的图片属性集合。 * @example * ```javascript * editor.execCommand( 'insertimage', { * src:'a/b/c.jpg', * width:'100', * height:'100' * } ); * ``` * @example * ```javascript * editor.execCommand( 'insertimage', [{ * src:'a/b/c.jpg', * width:'100', * height:'100' * },{ * src:'a/b/d.jpg', * width:'100', * height:'100' * }] ); * ``` */ UE.commands['insertimage'] = { execCommand:function (cmd, opt) { opt = utils.isArray(opt) ? opt : [opt]; if (!opt.length) { return; } var me = this, range = me.selection.getRange(), img = range.getClosedNode(); if(me.fireEvent('beforeinsertimage', opt) === true){ return; } function unhtmlData(imgCi) { utils.each('width,height,border,hspace,vspace'.split(','), function (item) { if (imgCi[item]) { imgCi[item] = parseInt(imgCi[item], 10) || 0; } }); utils.each('src,_src'.split(','), function (item) { if (imgCi[item]) { imgCi[item] = utils.unhtmlForUrl(imgCi[item]); } }); utils.each('title,alt'.split(','), function (item) { if (imgCi[item]) { imgCi[item] = utils.unhtml(imgCi[item]); } }); } if (img && /img/i.test(img.tagName) && (img.className != "edui-faked-video" || img.className.indexOf("edui-upload-video")!=-1) && !img.getAttribute("word_img")) { var first = opt.shift(); var floatStyle = first['floatStyle']; delete first['floatStyle']; //// img.style.border = (first.border||0) +"px solid #000"; //// img.style.margin = (first.margin||0) +"px"; // img.style.cssText += ';margin:' + (first.margin||0) +"px;" + 'border:' + (first.border||0) +"px solid #000"; domUtils.setAttributes(img, first); me.execCommand('imagefloat', floatStyle); if (opt.length > 0) { range.setStartAfter(img).setCursor(false, true); me.execCommand('insertimage', opt); } } else { var html = [], str = '', ci; ci = opt[0]; if (opt.length == 1) { unhtmlData(ci); str = '' + ci.alt + ''; if (ci['floatStyle'] == 'center') { str = '

    ' + str + '

    '; } html.push(str); } else { for (var i = 0; ci = opt[i++];) { unhtmlData(ci); str = '

    '; html.push(str); } } me.execCommand('insertHtml', html.join('')); } me.fireEvent('afterinsertimage', opt) } }; // plugins/justify.js /** * 段落格式 * @file * @since 1.2.6.1 */ /** * 段落对齐方式 * @command justify * @method execCommand * @param { String } cmd 命令字符串 * @param { String } align 对齐方式:left => 居左,right => 居右,center => 居中,justify => 两端对齐 * @example * ```javascript * editor.execCommand( 'justify', 'center' ); * ``` */ /** * 如果选区所在位置是段落区域,返回当前段落对齐方式 * @command justify * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { String } 返回段落对齐方式 * @example * ```javascript * editor.queryCommandValue( 'justify' ); * ``` */ UE.plugins['justify']=function(){ var me=this, block = domUtils.isBlockElm, defaultValue = { left:1, right:1, center:1, justify:1 }, doJustify = function (range, style) { var bookmark = range.createBookmark(), filterFn = function (node) { return node.nodeType == 1 ? node.tagName.toLowerCase() != 'br' && !domUtils.isBookmarkNode(node) : !domUtils.isWhitespace(node); }; range.enlarge(true); var bookmark2 = range.createBookmark(), current = domUtils.getNextDomNode(bookmark2.start, false, filterFn), tmpRange = range.cloneRange(), tmpNode; while (current && !(domUtils.getPosition(current, bookmark2.end) & domUtils.POSITION_FOLLOWING)) { if (current.nodeType == 3 || !block(current)) { tmpRange.setStartBefore(current); while (current && current !== bookmark2.end && !block(current)) { tmpNode = current; current = domUtils.getNextDomNode(current, false, null, function (node) { return !block(node); }); } tmpRange.setEndAfter(tmpNode); var common = tmpRange.getCommonAncestor(); if (!domUtils.isBody(common) && block(common)) { domUtils.setStyles(common, utils.isString(style) ? {'text-align':style} : style); current = common; } else { var p = range.document.createElement('p'); domUtils.setStyles(p, utils.isString(style) ? {'text-align':style} : style); var frag = tmpRange.extractContents(); p.appendChild(frag); tmpRange.insertNode(p); current = p; } current = domUtils.getNextDomNode(current, false, filterFn); } else { current = domUtils.getNextDomNode(current, true, filterFn); } } return range.moveToBookmark(bookmark2).moveToBookmark(bookmark); }; UE.commands['justify'] = { execCommand:function (cmdName, align) { var range = this.selection.getRange(), txt; //闭合时单独处理 if (range.collapsed) { txt = this.document.createTextNode('p'); range.insertNode(txt); } doJustify(range, align); if (txt) { range.setStartBefore(txt).collapse(true); domUtils.remove(txt); } range.select(); return true; }, queryCommandValue:function () { var startNode = this.selection.getStart(), value = domUtils.getComputedStyle(startNode, 'text-align'); return defaultValue[value] ? value : 'left'; }, queryCommandState:function () { var start = this.selection.getStart(), cell = start && domUtils.findParentByTagName(start, ["td", "th","caption"], true); return cell? -1:0; } }; }; // plugins/font.js /** * 字体颜色,背景色,字号,字体,下划线,删除线 * @file * @since 1.2.6.1 */ /** * 字体颜色 * @command forecolor * @method execCommand * @param { String } cmd 命令字符串 * @param { String } value 色值(必须十六进制) * @example * ```javascript * editor.execCommand( 'forecolor', '#000' ); * ``` */ /** * 返回选区字体颜色 * @command forecolor * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { String } 返回字体颜色 * @example * ```javascript * editor.queryCommandValue( 'forecolor' ); * ``` */ /** * 字体背景颜色 * @command backcolor * @method execCommand * @param { String } cmd 命令字符串 * @param { String } value 色值(必须十六进制) * @example * ```javascript * editor.execCommand( 'backcolor', '#000' ); * ``` */ /** * 返回选区字体颜色 * @command backcolor * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { String } 返回字体背景颜色 * @example * ```javascript * editor.queryCommandValue( 'backcolor' ); * ``` */ /** * 字体大小 * @command fontsize * @method execCommand * @param { String } cmd 命令字符串 * @param { String } value 字体大小 * @example * ```javascript * editor.execCommand( 'fontsize', '14px' ); * ``` */ /** * 返回选区字体大小 * @command fontsize * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { String } 返回字体大小 * @example * ```javascript * editor.queryCommandValue( 'fontsize' ); * ``` */ /** * 字体样式 * @command fontfamily * @method execCommand * @param { String } cmd 命令字符串 * @param { String } value 字体样式 * @example * ```javascript * editor.execCommand( 'fontfamily', '微软雅黑' ); * ``` */ /** * 返回选区字体样式 * @command fontfamily * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { String } 返回字体样式 * @example * ```javascript * editor.queryCommandValue( 'fontfamily' ); * ``` */ /** * 字体下划线,与删除线互斥 * @command underline * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'underline' ); * ``` */ /** * 字体删除线,与下划线互斥 * @command strikethrough * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'strikethrough' ); * ``` */ /** * 字体边框 * @command fontborder * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'fontborder' ); * ``` */ UE.plugins['font'] = function () { var me = this, fonts = { 'forecolor': 'color', 'backcolor': 'background-color', 'fontsize': 'font-size', 'fontfamily': 'font-family', 'underline': 'text-decoration', 'strikethrough': 'text-decoration', 'fontborder': 'border' }, needCmd = {'underline': 1, 'strikethrough': 1, 'fontborder': 1}, needSetChild = { 'forecolor': 'color', 'backcolor': 'background-color', 'fontsize': 'font-size', 'fontfamily': 'font-family' }; me.setOpt({ 'fontfamily': [ { name: 'songti', val: '宋体,SimSun'}, { name: 'yahei', val: '微软雅黑,Microsoft YaHei'}, { name: 'kaiti', val: '楷体,楷体_GB2312, SimKai'}, { name: 'heiti', val: '黑体, SimHei'}, { name: 'lishu', val: '隶书, SimLi'}, { name: 'andaleMono', val: 'andale mono'}, { name: 'arial', val: 'arial, helvetica,sans-serif'}, { name: 'arialBlack', val: 'arial black,avant garde'}, { name: 'comicSansMs', val: 'comic sans ms'}, { name: 'impact', val: 'impact,chicago'}, { name: 'timesNewRoman', val: 'times new roman'} ], 'fontsize': [10, 11, 12, 14, 16, 18, 20, 24, 36] }); function mergeWithParent(node){ var parent; while(parent = node.parentNode){ if(parent.tagName == 'SPAN' && domUtils.getChildCount(parent,function(child){ return !domUtils.isBookmarkNode(child) && !domUtils.isBr(child) }) == 1) { parent.style.cssText += node.style.cssText; domUtils.remove(node,true); node = parent; }else{ break; } } } function mergeChild(rng,cmdName,value){ if(needSetChild[cmdName]){ rng.adjustmentBoundary(); if(!rng.collapsed && rng.startContainer.nodeType == 1){ var start = rng.startContainer.childNodes[rng.startOffset]; if(start && domUtils.isTagNode(start,'span')){ var bk = rng.createBookmark(); utils.each(domUtils.getElementsByTagName(start, 'span'), function (span) { if (!span.parentNode || domUtils.isBookmarkNode(span))return; if(cmdName == 'backcolor' && domUtils.getComputedStyle(span,'background-color').toLowerCase() === value){ return; } domUtils.removeStyle(span,needSetChild[cmdName]); if(span.style.cssText.replace(/^\s+$/,'').length == 0){ domUtils.remove(span,true) } }); rng.moveToBookmark(bk) } } } } function mergesibling(rng,cmdName,value) { var collapsed = rng.collapsed, bk = rng.createBookmark(), common; if (collapsed) { common = bk.start.parentNode; while (dtd.$inline[common.tagName]) { common = common.parentNode; } } else { common = domUtils.getCommonAncestor(bk.start, bk.end); } utils.each(domUtils.getElementsByTagName(common, 'span'), function (span) { if (!span.parentNode || domUtils.isBookmarkNode(span))return; if (/\s*border\s*:\s*none;?\s*/i.test(span.style.cssText)) { if(/^\s*border\s*:\s*none;?\s*$/.test(span.style.cssText)){ domUtils.remove(span, true); }else{ domUtils.removeStyle(span,'border'); } return } if (/border/i.test(span.style.cssText) && span.parentNode.tagName == 'SPAN' && /border/i.test(span.parentNode.style.cssText)) { span.style.cssText = span.style.cssText.replace(/border[^:]*:[^;]+;?/gi, ''); } if(!(cmdName=='fontborder' && value=='none')){ var next = span.nextSibling; while (next && next.nodeType == 1 && next.tagName == 'SPAN' ) { if(domUtils.isBookmarkNode(next) && cmdName == 'fontborder') { span.appendChild(next); next = span.nextSibling; continue; } if (next.style.cssText == span.style.cssText) { domUtils.moveChild(next, span); domUtils.remove(next); } if (span.nextSibling === next) break; next = span.nextSibling; } } mergeWithParent(span); if(browser.ie && browser.version > 8 ){ //拷贝父亲们的特别的属性,这里只做背景颜色的处理 var parent = domUtils.findParent(span,function(n){return n.tagName == 'SPAN' && /background-color/.test(n.style.cssText)}); if(parent && !/background-color/.test(span.style.cssText)){ span.style.backgroundColor = parent.style.backgroundColor; } } }); rng.moveToBookmark(bk); mergeChild(rng,cmdName,value) } me.addInputRule(function (root) { utils.each(root.getNodesByTagName('u s del font strike'), function (node) { if (node.tagName == 'font') { var cssStyle = []; for (var p in node.attrs) { switch (p) { case 'size': cssStyle.push('font-size:' + ({ '1':'10', '2':'12', '3':'16', '4':'18', '5':'24', '6':'32', '7':'48' }[node.attrs[p]] || node.attrs[p]) + 'px'); break; case 'color': cssStyle.push('color:' + node.attrs[p]); break; case 'face': cssStyle.push('font-family:' + node.attrs[p]); break; case 'style': cssStyle.push(node.attrs[p]); } } node.attrs = { 'style': cssStyle.join(';') }; } else { var val = node.tagName == 'u' ? 'underline' : 'line-through'; node.attrs = { 'style': (node.getAttr('style') || '') + 'text-decoration:' + val + ';' } } node.tagName = 'span'; }); // utils.each(root.getNodesByTagName('span'), function (node) { // var val; // if(val = node.getAttr('class')){ // if(/fontstrikethrough/.test(val)){ // node.setStyle('text-decoration','line-through'); // if(node.attrs['class']){ // node.attrs['class'] = node.attrs['class'].replace(/fontstrikethrough/,''); // }else{ // node.setAttr('class') // } // } // if(/fontborder/.test(val)){ // node.setStyle('border','1px solid #000'); // if(node.attrs['class']){ // node.attrs['class'] = node.attrs['class'].replace(/fontborder/,''); // }else{ // node.setAttr('class') // } // } // } // }); }); // me.addOutputRule(function(root){ // utils.each(root.getNodesByTagName('span'), function (node) { // var val; // if(val = node.getStyle('text-decoration')){ // if(/line-through/.test(val)){ // if(node.attrs['class']){ // node.attrs['class'] += ' fontstrikethrough'; // }else{ // node.setAttr('class','fontstrikethrough') // } // } // // node.setStyle('text-decoration') // } // if(val = node.getStyle('border')){ // if(/1px/.test(val) && /solid/.test(val)){ // if(node.attrs['class']){ // node.attrs['class'] += ' fontborder'; // // }else{ // node.setAttr('class','fontborder') // } // } // node.setStyle('border') // // } // }); // }); for (var p in fonts) { (function (cmd, style) { UE.commands[cmd] = { execCommand: function (cmdName, value) { value = value || (this.queryCommandState(cmdName) ? 'none' : cmdName == 'underline' ? 'underline' : cmdName == 'fontborder' ? '1px solid #000' : 'line-through'); var me = this, range = this.selection.getRange(), text; if (value == 'default') { if (range.collapsed) { text = me.document.createTextNode('font'); range.insertNode(text).select(); } me.execCommand('removeFormat', 'span,a', style); if (text) { range.setStartBefore(text).collapse(true); domUtils.remove(text); } mergesibling(range,cmdName,value); range.select() } else { if (!range.collapsed) { if (needCmd[cmd] && me.queryCommandValue(cmd)) { me.execCommand('removeFormat', 'span,a', style); } range = me.selection.getRange(); range.applyInlineStyle('span', {'style': style + ':' + value}); mergesibling(range, cmdName,value); range.select(); } else { var span = domUtils.findParentByTagName(range.startContainer, 'span', true); text = me.document.createTextNode('font'); if (span && !span.children.length && !span[browser.ie ? 'innerText' : 'textContent'].replace(fillCharReg, '').length) { //for ie hack when enter range.insertNode(text); if (needCmd[cmd]) { range.selectNode(text).select(); me.execCommand('removeFormat', 'span,a', style, null); span = domUtils.findParentByTagName(text, 'span', true); range.setStartBefore(text); } span && (span.style.cssText += ';' + style + ':' + value); range.collapse(true).select(); } else { range.insertNode(text); range.selectNode(text).select(); span = range.document.createElement('span'); if (needCmd[cmd]) { //a标签内的不处理跳过 if (domUtils.findParentByTagName(text, 'a', true)) { range.setStartBefore(text).setCursor(); domUtils.remove(text); return; } me.execCommand('removeFormat', 'span,a', style); } span.style.cssText = style + ':' + value; text.parentNode.insertBefore(span, text); //修复,span套span 但样式不继承的问题 if (!browser.ie || browser.ie && browser.version == 9) { var spanParent = span.parentNode; while (!domUtils.isBlockElm(spanParent)) { if (spanParent.tagName == 'SPAN') { //opera合并style不会加入";" span.style.cssText = spanParent.style.cssText + ";" + span.style.cssText; } spanParent = spanParent.parentNode; } } if (opera) { setTimeout(function () { range.setStart(span, 0).collapse(true); mergesibling(range, cmdName,value); range.select(); }); } else { range.setStart(span, 0).collapse(true); mergesibling(range,cmdName,value); range.select(); } //trace:981 //domUtils.mergeToParent(span) } domUtils.remove(text); } } return true; }, queryCommandValue: function (cmdName) { var startNode = this.selection.getStart(); //trace:946 if (cmdName == 'underline' || cmdName == 'strikethrough') { var tmpNode = startNode, value; while (tmpNode && !domUtils.isBlockElm(tmpNode) && !domUtils.isBody(tmpNode)) { if (tmpNode.nodeType == 1) { value = domUtils.getComputedStyle(tmpNode, style); if (value != 'none') { return value; } } tmpNode = tmpNode.parentNode; } return 'none'; } if (cmdName == 'fontborder') { var tmp = startNode, val; while (tmp && dtd.$inline[tmp.tagName]) { if (val = domUtils.getComputedStyle(tmp, 'border')) { if (/1px/.test(val) && /solid/.test(val)) { return val; } } tmp = tmp.parentNode; } return '' } if( cmdName == 'FontSize' ) { var styleVal = domUtils.getComputedStyle(startNode, style), tmp = /^([\d\.]+)(\w+)$/.exec( styleVal ); if( tmp ) { return Math.floor( tmp[1] ) + tmp[2]; } return styleVal; } return domUtils.getComputedStyle(startNode, style); }, queryCommandState: function (cmdName) { if (!needCmd[cmdName]) return 0; var val = this.queryCommandValue(cmdName); if (cmdName == 'fontborder') { return /1px/.test(val) && /solid/.test(val) } else { return cmdName == 'underline' ? /underline/.test(val) : /line\-through/.test(val); } } }; })(p, fonts[p]); } }; // plugins/link.js /** * 超链接 * @file * @since 1.2.6.1 */ /** * 插入超链接 * @command link * @method execCommand * @param { String } cmd 命令字符串 * @param { Object } options 设置自定义属性,例如:url、title、target * @example * ```javascript * editor.execCommand( 'link', '{ * url:'ueditor.baidu.com', * title:'ueditor', * target:'_blank' * }' ); * ``` */ /** * 返回当前选中的第一个超链接节点 * @command link * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { Element } 超链接节点 * @example * ```javascript * editor.queryCommandValue( 'link' ); * ``` */ /** * 取消超链接 * @command unlink * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'unlink'); * ``` */ UE.plugins['link'] = function(){ function optimize( range ) { var start = range.startContainer,end = range.endContainer; if ( start = domUtils.findParentByTagName( start, 'a', true ) ) { range.setStartBefore( start ); } if ( end = domUtils.findParentByTagName( end, 'a', true ) ) { range.setEndAfter( end ); } } UE.commands['unlink'] = { execCommand : function() { var range = this.selection.getRange(), bookmark; if(range.collapsed && !domUtils.findParentByTagName( range.startContainer, 'a', true )){ return; } bookmark = range.createBookmark(); optimize( range ); range.removeInlineStyle( 'a' ).moveToBookmark( bookmark ).select(); }, queryCommandState : function(){ return !this.highlight && this.queryCommandValue('link') ? 0 : -1; } }; function doLink(range,opt,me){ var rngClone = range.cloneRange(), link = me.queryCommandValue('link'); optimize( range = range.adjustmentBoundary() ); var start = range.startContainer; if(start.nodeType == 1 && link){ start = start.childNodes[range.startOffset]; if(start && start.nodeType == 1 && start.tagName == 'A' && /^(?:https?|ftp|file)\s*:\s*\/\//.test(start[browser.ie?'innerText':'textContent'])){ start[browser.ie ? 'innerText' : 'textContent'] = utils.html(opt.textValue||opt.href); } } if( !rngClone.collapsed || link){ range.removeInlineStyle( 'a' ); rngClone = range.cloneRange(); } if ( rngClone.collapsed ) { var a = range.document.createElement( 'a'), text = ''; if(opt.textValue){ text = utils.html(opt.textValue); delete opt.textValue; }else{ text = utils.html(opt.href); } domUtils.setAttributes( a, opt ); start = domUtils.findParentByTagName( rngClone.startContainer, 'a', true ); if(start && domUtils.isInNodeEndBoundary(rngClone,start)){ range.setStartAfter(start).collapse(true); } a[browser.ie ? 'innerText' : 'textContent'] = text; range.insertNode(a).selectNode( a ); } else { range.applyInlineStyle( 'a', opt ); } } UE.commands['link'] = { execCommand : function( cmdName, opt ) { var range; opt._href && (opt._href = utils.unhtml(opt._href,/[<">]/g)); opt.href && (opt.href = utils.unhtml(opt.href,/[<">]/g)); opt.textValue && (opt.textValue = utils.unhtml(opt.textValue,/[<">]/g)); doLink(range=this.selection.getRange(),opt,this); //闭合都不加占位符,如果加了会在a后边多个占位符节点,导致a是图片背景组成的列表,出现空白问题 range.collapse().select(true); }, queryCommandValue : function() { var range = this.selection.getRange(), node; if ( range.collapsed ) { // node = this.selection.getStart(); //在ie下getstart()取值偏上了 node = range.startContainer; node = node.nodeType == 1 ? node : node.parentNode; if ( node && (node = domUtils.findParentByTagName( node, 'a', true )) && ! domUtils.isInNodeEndBoundary(range,node)) { return node; } } else { //trace:1111 如果是

    xx

    startContainer是p就会找不到a range.shrinkBoundary(); var start = range.startContainer.nodeType == 3 || !range.startContainer.childNodes[range.startOffset] ? range.startContainer : range.startContainer.childNodes[range.startOffset], end = range.endContainer.nodeType == 3 || range.endOffset == 0 ? range.endContainer : range.endContainer.childNodes[range.endOffset-1], common = range.getCommonAncestor(); node = domUtils.findParentByTagName( common, 'a', true ); if ( !node && common.nodeType == 1){ var as = common.getElementsByTagName( 'a' ), ps,pe; for ( var i = 0,ci; ci = as[i++]; ) { ps = domUtils.getPosition( ci, start ),pe = domUtils.getPosition( ci,end); if ( (ps & domUtils.POSITION_FOLLOWING || ps & domUtils.POSITION_CONTAINS) && (pe & domUtils.POSITION_PRECEDING || pe & domUtils.POSITION_CONTAINS) ) { node = ci; break; } } } return node; } }, queryCommandState : function() { //判断如果是视频的话连接不可用 //fix 853 var img = this.selection.getRange().getClosedNode(), flag = img && (img.className == "edui-faked-video" || img.className.indexOf("edui-upload-video")!=-1); return flag ? -1 : 0; } }; }; // plugins/iframe.js ///import core ///import plugins\inserthtml.js ///commands 插入框架 ///commandsName InsertFrame ///commandsTitle 插入Iframe ///commandsDialog dialogs\insertframe UE.plugins['insertframe'] = function() { var me =this; function deleteIframe(){ me._iframe && delete me._iframe; } me.addListener("selectionchange",function(){ deleteIframe(); }); }; // plugins/scrawl.js ///import core ///commands 涂鸦 ///commandsName Scrawl ///commandsTitle 涂鸦 ///commandsDialog dialogs\scrawl UE.commands['scrawl'] = { queryCommandState : function(){ return ( browser.ie && browser.version <= 8 ) ? -1 :0; } }; // plugins/removeformat.js /** * 清除格式 * @file * @since 1.2.6.1 */ /** * 清除文字样式 * @command removeformat * @method execCommand * @param { String } cmd 命令字符串 * @param {String} tags 以逗号隔开的标签。如:strong * @param {String} style 样式如:color * @param {String} attrs 属性如:width * @example * ```javascript * editor.execCommand( 'removeformat', 'strong','color','width' ); * ``` */ UE.plugins['removeformat'] = function(){ var me = this; me.setOpt({ 'removeFormatTags': 'b,big,code,del,dfn,em,font,i,ins,kbd,q,samp,small,span,strike,strong,sub,sup,tt,u,var', 'removeFormatAttributes':'class,style,lang,width,height,align,hspace,valign' }); me.commands['removeformat'] = { execCommand : function( cmdName, tags, style, attrs,notIncludeA ) { var tagReg = new RegExp( '^(?:' + (tags || this.options.removeFormatTags).replace( /,/g, '|' ) + ')$', 'i' ) , removeFormatAttributes = style ? [] : (attrs || this.options.removeFormatAttributes).split( ',' ), range = new dom.Range( this.document ), bookmark,node,parent, filter = function( node ) { return node.nodeType == 1; }; function isRedundantSpan (node) { if (node.nodeType == 3 || node.tagName.toLowerCase() != 'span'){ return 0; } if (browser.ie) { //ie 下判断实效,所以只能简单用style来判断 //return node.style.cssText == '' ? 1 : 0; var attrs = node.attributes; if ( attrs.length ) { for ( var i = 0,l = attrs.length; i var node = range.startContainer, tmp, collapsed = range.collapsed; while(node.nodeType == 1 && domUtils.isEmptyNode(node) && dtd.$removeEmpty[node.tagName]){ tmp = node.parentNode; range.setStartBefore(node); //trace:937 //更新结束边界 if(range.startContainer === range.endContainer){ range.endOffset--; } domUtils.remove(node); node = tmp; } if(!collapsed){ node = range.endContainer; while(node.nodeType == 1 && domUtils.isEmptyNode(node) && dtd.$removeEmpty[node.tagName]){ tmp = node.parentNode; range.setEndBefore(node); domUtils.remove(node); node = tmp; } } } range = this.selection.getRange(); doRemove( range ); range.select(); } }; }; // plugins/blockquote.js /** * 添加引用 * @file * @since 1.2.6.1 */ /** * 添加引用 * @command blockquote * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'blockquote' ); * ``` */ /** * 添加引用 * @command blockquote * @method execCommand * @param { String } cmd 命令字符串 * @param { Object } attrs 节点属性 * @example * ```javascript * editor.execCommand( 'blockquote',{ * style: "color: red;" * } ); * ``` */ UE.plugins['blockquote'] = function(){ var me = this; function getObj(editor){ return domUtils.filterNodeList(editor.selection.getStartElementPath(),'blockquote'); } me.commands['blockquote'] = { execCommand : function( cmdName, attrs ) { var range = this.selection.getRange(), obj = getObj(this), blockquote = dtd.blockquote, bookmark = range.createBookmark(); if ( obj ) { var start = range.startContainer, startBlock = domUtils.isBlockElm(start) ? start : domUtils.findParent(start,function(node){return domUtils.isBlockElm(node)}), end = range.endContainer, endBlock = domUtils.isBlockElm(end) ? end : domUtils.findParent(end,function(node){return domUtils.isBlockElm(node)}); //处理一下li startBlock = domUtils.findParentByTagName(startBlock,'li',true) || startBlock; endBlock = domUtils.findParentByTagName(endBlock,'li',true) || endBlock; if(startBlock.tagName == 'LI' || startBlock.tagName == 'TD' || startBlock === obj || domUtils.isBody(startBlock)){ domUtils.remove(obj,true); }else{ domUtils.breakParent(startBlock,obj); } if(startBlock !== endBlock){ obj = domUtils.findParentByTagName(endBlock,'blockquote'); if(obj){ if(endBlock.tagName == 'LI' || endBlock.tagName == 'TD'|| domUtils.isBody(endBlock)){ obj.parentNode && domUtils.remove(obj,true); }else{ domUtils.breakParent(endBlock,obj); } } } var blockquotes = domUtils.getElementsByTagName(this.document,'blockquote'); for(var i=0,bi;bi=blockquotes[i++];){ if(!bi.childNodes.length){ domUtils.remove(bi); }else if(domUtils.getPosition(bi,startBlock)&domUtils.POSITION_FOLLOWING && domUtils.getPosition(bi,endBlock)&domUtils.POSITION_PRECEDING){ domUtils.remove(bi,true); } } } else { var tmpRange = range.cloneRange(), node = tmpRange.startContainer.nodeType == 1 ? tmpRange.startContainer : tmpRange.startContainer.parentNode, preNode = node, doEnd = 1; //调整开始 while ( 1 ) { if ( domUtils.isBody(node) ) { if ( preNode !== node ) { if ( range.collapsed ) { tmpRange.selectNode( preNode ); doEnd = 0; } else { tmpRange.setStartBefore( preNode ); } }else{ tmpRange.setStart(node,0); } break; } if ( !blockquote[node.tagName] ) { if ( range.collapsed ) { tmpRange.selectNode( preNode ); } else{ tmpRange.setStartBefore( preNode); } break; } preNode = node; node = node.parentNode; } //调整结束 if ( doEnd ) { preNode = node = node = tmpRange.endContainer.nodeType == 1 ? tmpRange.endContainer : tmpRange.endContainer.parentNode; while ( 1 ) { if ( domUtils.isBody( node ) ) { if ( preNode !== node ) { tmpRange.setEndAfter( preNode ); } else { tmpRange.setEnd( node, node.childNodes.length ); } break; } if ( !blockquote[node.tagName] ) { tmpRange.setEndAfter( preNode ); break; } preNode = node; node = node.parentNode; } } node = range.document.createElement( 'blockquote' ); domUtils.setAttributes( node, attrs ); node.appendChild( tmpRange.extractContents() ); tmpRange.insertNode( node ); //去除重复的 var childs = domUtils.getElementsByTagName(node,'blockquote'); for(var i=0,ci;ci=childs[i++];){ if(ci.parentNode){ domUtils.remove(ci,true); } } } range.moveToBookmark( bookmark ).select(); }, queryCommandState : function() { return getObj(this) ? 1 : 0; } }; }; // plugins/convertcase.js /** * 大小写转换 * @file * @since 1.2.6.1 */ /** * 把选区内文本变大写,与“tolowercase”命令互斥 * @command touppercase * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'touppercase' ); * ``` */ /** * 把选区内文本变小写,与“touppercase”命令互斥 * @command tolowercase * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'tolowercase' ); * ``` */ UE.commands['touppercase'] = UE.commands['tolowercase'] = { execCommand:function (cmd) { var me = this; var rng = me.selection.getRange(); if(rng.collapsed){ return rng; } var bk = rng.createBookmark(), bkEnd = bk.end, filterFn = function( node ) { return !domUtils.isBr(node) && !domUtils.isWhitespace( node ); }, curNode = domUtils.getNextDomNode( bk.start, false, filterFn ); while ( curNode && (domUtils.getPosition( curNode, bkEnd ) & domUtils.POSITION_PRECEDING) ) { if ( curNode.nodeType == 3 ) { curNode.nodeValue = curNode.nodeValue[cmd == 'touppercase' ? 'toUpperCase' : 'toLowerCase'](); } curNode = domUtils.getNextDomNode( curNode, true, filterFn ); if(curNode === bkEnd){ break; } } rng.moveToBookmark(bk).select(); } }; // plugins/indent.js /** * 首行缩进 * @file * @since 1.2.6.1 */ /** * 缩进 * @command indent * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'indent' ); * ``` */ UE.commands['indent'] = { execCommand : function() { var me = this,value = me.queryCommandState("indent") ? "0em" : (me.options.indentValue || '2em'); me.execCommand('Paragraph','p',{style:'text-indent:'+ value}); }, queryCommandState : function() { var pN = domUtils.filterNodeList(this.selection.getStartElementPath(),'p h1 h2 h3 h4 h5 h6'); return pN && pN.style.textIndent && parseInt(pN.style.textIndent) ? 1 : 0; } }; // plugins/print.js /** * 打印 * @file * @since 1.2.6.1 */ /** * 打印 * @command print * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'print' ); * ``` */ UE.commands['print'] = { execCommand : function(){ this.window.print(); }, notNeedUndo : 1 }; // plugins/preview.js /** * 预览 * @file * @since 1.2.6.1 */ /** * 预览 * @command preview * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'preview' ); * ``` */ UE.commands['preview'] = { execCommand : function(){ var w = window.open('', '_blank', ''), d = w.document; d.open(); d.write('
    '+this.getContent(null,null,true)+'
    '); d.close(); }, notNeedUndo : 1 }; // plugins/selectall.js /** * 全选 * @file * @since 1.2.6.1 */ /** * 选中所有内容 * @command selectall * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'selectall' ); * ``` */ UE.plugins['selectall'] = function(){ var me = this; me.commands['selectall'] = { execCommand : function(){ //去掉了原生的selectAll,因为会出现报错和当内容为空时,不能出现闭合状态的光标 var me = this,body = me.body, range = me.selection.getRange(); range.selectNodeContents(body); if(domUtils.isEmptyBlock(body)){ //opera不能自动合并到元素的里边,要手动处理一下 if(browser.opera && body.firstChild && body.firstChild.nodeType == 1){ range.setStartAtFirst(body.firstChild); } range.collapse(true); } range.select(true); }, notNeedUndo : 1 }; //快捷键 me.addshortcutkey({ "selectAll" : "ctrl+65" }); }; // plugins/paragraph.js /** * 段落样式 * @file * @since 1.2.6.1 */ /** * 段落格式 * @command paragraph * @method execCommand * @param { String } cmd 命令字符串 * @param {String} style 标签值为:'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' * @param {Object} attrs 标签的属性 * @example * ```javascript * editor.execCommand( 'Paragraph','h1','{ * class:'test' * }' ); * ``` */ /** * 返回选区内节点标签名 * @command paragraph * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { String } 节点标签名 * @example * ```javascript * editor.queryCommandValue( 'Paragraph' ); * ``` */ UE.plugins['paragraph'] = function() { var me = this, block = domUtils.isBlockElm, notExchange = ['TD','LI','PRE'], doParagraph = function(range,style,attrs,sourceCmdName){ var bookmark = range.createBookmark(), filterFn = function( node ) { return node.nodeType == 1 ? node.tagName.toLowerCase() != 'br' && !domUtils.isBookmarkNode(node) : !domUtils.isWhitespace( node ); }, para; range.enlarge( true ); var bookmark2 = range.createBookmark(), current = domUtils.getNextDomNode( bookmark2.start, false, filterFn ), tmpRange = range.cloneRange(), tmpNode; while ( current && !(domUtils.getPosition( current, bookmark2.end ) & domUtils.POSITION_FOLLOWING) ) { if ( current.nodeType == 3 || !block( current ) ) { tmpRange.setStartBefore( current ); while ( current && current !== bookmark2.end && !block( current ) ) { tmpNode = current; current = domUtils.getNextDomNode( current, false, null, function( node ) { return !block( node ); } ); } tmpRange.setEndAfter( tmpNode ); para = range.document.createElement( style ); if(attrs){ domUtils.setAttributes(para,attrs); if(sourceCmdName && sourceCmdName == 'customstyle' && attrs.style){ para.style.cssText = attrs.style; } } para.appendChild( tmpRange.extractContents() ); //需要内容占位 if(domUtils.isEmptyNode(para)){ domUtils.fillChar(range.document,para); } tmpRange.insertNode( para ); var parent = para.parentNode; //如果para上一级是一个block元素且不是body,td就删除它 if ( block( parent ) && !domUtils.isBody( para.parentNode ) && utils.indexOf(notExchange,parent.tagName)==-1) { //存储dir,style if(!(sourceCmdName && sourceCmdName == 'customstyle')){ parent.getAttribute('dir') && para.setAttribute('dir',parent.getAttribute('dir')); //trace:1070 parent.style.cssText && (para.style.cssText = parent.style.cssText + ';' + para.style.cssText); //trace:1030 parent.style.textAlign && !para.style.textAlign && (para.style.textAlign = parent.style.textAlign); parent.style.textIndent && !para.style.textIndent && (para.style.textIndent = parent.style.textIndent); parent.style.padding && !para.style.padding && (para.style.padding = parent.style.padding); } //trace:1706 选择的就是h1-6要删除 if(attrs && /h\d/i.test(parent.tagName) && !/h\d/i.test(para.tagName) ){ domUtils.setAttributes(parent,attrs); if(sourceCmdName && sourceCmdName == 'customstyle' && attrs.style){ parent.style.cssText = attrs.style; } domUtils.remove(para,true); para = parent; }else{ domUtils.remove( para.parentNode, true ); } } if( utils.indexOf(notExchange,parent.tagName)!=-1){ current = parent; }else{ current = para; } current = domUtils.getNextDomNode( current, false, filterFn ); } else { current = domUtils.getNextDomNode( current, true, filterFn ); } } return range.moveToBookmark( bookmark2 ).moveToBookmark( bookmark ); }; me.setOpt('paragraph',{'p':'', 'h1':'', 'h2':'', 'h3':'', 'h4':'', 'h5':'', 'h6':''}); me.commands['paragraph'] = { execCommand : function( cmdName, style,attrs,sourceCmdName ) { var range = this.selection.getRange(); //闭合时单独处理 if(range.collapsed){ var txt = this.document.createTextNode('p'); range.insertNode(txt); //去掉冗余的fillchar if(browser.ie){ var node = txt.previousSibling; if(node && domUtils.isWhitespace(node)){ domUtils.remove(node); } node = txt.nextSibling; if(node && domUtils.isWhitespace(node)){ domUtils.remove(node); } } } range = doParagraph(range,style,attrs,sourceCmdName); if(txt){ range.setStartBefore(txt).collapse(true); pN = txt.parentNode; domUtils.remove(txt); if(domUtils.isBlockElm(pN)&&domUtils.isEmptyNode(pN)){ domUtils.fillNode(this.document,pN); } } if(browser.gecko && range.collapsed && range.startContainer.nodeType == 1){ var child = range.startContainer.childNodes[range.startOffset]; if(child && child.nodeType == 1 && child.tagName.toLowerCase() == style){ range.setStart(child,0).collapse(true); } } //trace:1097 原来有true,原因忘了,但去了就不能清除多余的占位符了 range.select(); return true; }, queryCommandValue : function() { var node = domUtils.filterNodeList(this.selection.getStartElementPath(),'p h1 h2 h3 h4 h5 h6'); return node ? node.tagName.toLowerCase() : ''; } }; }; // plugins/directionality.js /** * 设置文字输入的方向的插件 * @file * @since 1.2.6.1 */ (function() { var block = domUtils.isBlockElm , getObj = function(editor){ // var startNode = editor.selection.getStart(), // parents; // if ( startNode ) { // //查找所有的是block的父亲节点 // parents = domUtils.findParents( startNode, true, block, true ); // for ( var i = 0,ci; ci = parents[i++]; ) { // if ( ci.getAttribute( 'dir' ) ) { // return ci; // } // } // } return domUtils.filterNodeList(editor.selection.getStartElementPath(),function(n){return n && n.nodeType == 1 && n.getAttribute('dir')}); }, doDirectionality = function(range,editor,forward){ var bookmark, filterFn = function( node ) { return node.nodeType == 1 ? !domUtils.isBookmarkNode(node) : !domUtils.isWhitespace(node); }, obj = getObj( editor ); if ( obj && range.collapsed ) { obj.setAttribute( 'dir', forward ); return range; } bookmark = range.createBookmark(); range.enlarge( true ); var bookmark2 = range.createBookmark(), current = domUtils.getNextDomNode( bookmark2.start, false, filterFn ), tmpRange = range.cloneRange(), tmpNode; while ( current && !(domUtils.getPosition( current, bookmark2.end ) & domUtils.POSITION_FOLLOWING) ) { if ( current.nodeType == 3 || !block( current ) ) { tmpRange.setStartBefore( current ); while ( current && current !== bookmark2.end && !block( current ) ) { tmpNode = current; current = domUtils.getNextDomNode( current, false, null, function( node ) { return !block( node ); } ); } tmpRange.setEndAfter( tmpNode ); var common = tmpRange.getCommonAncestor(); if ( !domUtils.isBody( common ) && block( common ) ) { //遍历到了block节点 common.setAttribute( 'dir', forward ); current = common; } else { //没有遍历到,添加一个block节点 var p = range.document.createElement( 'p' ); p.setAttribute( 'dir', forward ); var frag = tmpRange.extractContents(); p.appendChild( frag ); tmpRange.insertNode( p ); current = p; } current = domUtils.getNextDomNode( current, false, filterFn ); } else { current = domUtils.getNextDomNode( current, true, filterFn ); } } return range.moveToBookmark( bookmark2 ).moveToBookmark( bookmark ); }; /** * 文字输入方向 * @command directionality * @method execCommand * @param { String } cmdName 命令字符串 * @param { String } forward 传入'ltr'表示从左向右输入,传入'rtl'表示从右向左输入 * @example * ```javascript * editor.execCommand( 'directionality', 'ltr'); * ``` */ /** * 查询当前选区的文字输入方向 * @command directionality * @method queryCommandValue * @param { String } cmdName 命令字符串 * @return { String } 返回'ltr'表示从左向右输入,返回'rtl'表示从右向左输入 * @example * ```javascript * editor.queryCommandValue( 'directionality'); * ``` */ UE.commands['directionality'] = { execCommand : function( cmdName,forward ) { var range = this.selection.getRange(); //闭合时单独处理 if(range.collapsed){ var txt = this.document.createTextNode('d'); range.insertNode(txt); } doDirectionality(range,this,forward); if(txt){ range.setStartBefore(txt).collapse(true); domUtils.remove(txt); } range.select(); return true; }, queryCommandValue : function() { var node = getObj(this); return node ? node.getAttribute('dir') : 'ltr'; } }; })(); // plugins/horizontal.js /** * 插入分割线插件 * @file * @since 1.2.6.1 */ /** * 插入分割线 * @command horizontal * @method execCommand * @param { String } cmdName 命令字符串 * @example * ```javascript * editor.execCommand( 'horizontal' ); * ``` */ UE.plugins['horizontal'] = function(){ var me = this; me.commands['horizontal'] = { execCommand : function( cmdName ) { var me = this; if(me.queryCommandState(cmdName)!==-1){ me.execCommand('insertHtml','
    '); var range = me.selection.getRange(), start = range.startContainer; if(start.nodeType == 1 && !start.childNodes[range.startOffset] ){ var tmp; if(tmp = start.childNodes[range.startOffset - 1]){ if(tmp.nodeType == 1 && tmp.tagName == 'HR'){ if(me.options.enterTag == 'p'){ tmp = me.document.createElement('p'); range.insertNode(tmp); range.setStart(tmp,0).setCursor(); }else{ tmp = me.document.createElement('br'); range.insertNode(tmp); range.setStartBefore(tmp).setCursor(); } } } } return true; } }, //边界在table里不能加分隔线 queryCommandState : function() { return domUtils.filterNodeList(this.selection.getStartElementPath(),'table') ? -1 : 0; } }; // me.addListener('delkeyup',function(){ // var rng = this.selection.getRange(); // if(browser.ie && browser.version > 8){ // rng.txtToElmBoundary(true); // if(domUtils.isStartInblock(rng)){ // var tmpNode = rng.startContainer; // var pre = tmpNode.previousSibling; // if(pre && domUtils.isTagNode(pre,'hr')){ // domUtils.remove(pre); // rng.select(); // return; // } // } // } // if(domUtils.isBody(rng.startContainer)){ // var hr = rng.startContainer.childNodes[rng.startOffset -1]; // if(hr && hr.nodeName == 'HR'){ // var next = hr.nextSibling; // if(next){ // rng.setStart(next,0) // }else if(hr.previousSibling){ // rng.setStartAtLast(hr.previousSibling) // }else{ // var p = this.document.createElement('p'); // hr.parentNode.insertBefore(p,hr); // domUtils.fillNode(this.document,p); // rng.setStart(p,0); // } // domUtils.remove(hr); // rng.setCursor(false,true); // } // } // }) me.addListener('delkeydown',function(name,evt){ var rng = this.selection.getRange(); rng.txtToElmBoundary(true); if(domUtils.isStartInblock(rng)){ var tmpNode = rng.startContainer; var pre = tmpNode.previousSibling; if(pre && domUtils.isTagNode(pre,'hr')){ domUtils.remove(pre); rng.select(); domUtils.preventDefault(evt); return true; } } }) }; // plugins/time.js /** * 插入时间和日期 * @file * @since 1.2.6.1 */ /** * 插入时间,默认格式:12:59:59 * @command time * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'time'); * ``` */ /** * 插入日期,默认格式:2013-08-30 * @command date * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'date'); * ``` */ UE.commands['time'] = UE.commands["date"] = { execCommand : function(cmd, format){ var date = new Date; function formatTime(date, format) { var hh = ('0' + date.getHours()).slice(-2), ii = ('0' + date.getMinutes()).slice(-2), ss = ('0' + date.getSeconds()).slice(-2); format = format || 'hh:ii:ss'; return format.replace(/hh/ig, hh).replace(/ii/ig, ii).replace(/ss/ig, ss); } function formatDate(date, format) { var yyyy = ('000' + date.getFullYear()).slice(-4), yy = yyyy.slice(-2), mm = ('0' + (date.getMonth()+1)).slice(-2), dd = ('0' + date.getDate()).slice(-2); format = format || 'yyyy-mm-dd'; return format.replace(/yyyy/ig, yyyy).replace(/yy/ig, yy).replace(/mm/ig, mm).replace(/dd/ig, dd); } this.execCommand('insertHtml',cmd == "time" ? formatTime(date, format):formatDate(date, format) ); } }; // plugins/rowspacing.js /** * 段前段后间距插件 * @file * @since 1.2.6.1 */ /** * 设置段间距 * @command rowspacing * @method execCommand * @param { String } cmd 命令字符串 * @param { String } value 段间距的值,以px为单位 * @param { String } dir 间距位置,top或bottom,分别表示段前和段后 * @example * ```javascript * editor.execCommand( 'rowspacing', '10', 'top' ); * ``` */ UE.plugins['rowspacing'] = function(){ var me = this; me.setOpt({ 'rowspacingtop':['5', '10', '15', '20', '25'], 'rowspacingbottom':['5', '10', '15', '20', '25'] }); me.commands['rowspacing'] = { execCommand : function( cmdName,value,dir ) { this.execCommand('paragraph','p',{style:'margin-'+dir+':'+value + 'px'}); return true; }, queryCommandValue : function(cmdName,dir) { var pN = domUtils.filterNodeList(this.selection.getStartElementPath(),function(node){return domUtils.isBlockElm(node) }), value; //trace:1026 if(pN){ value = domUtils.getComputedStyle(pN,'margin-'+dir).replace(/[^\d]/g,''); return !value ? 0 : value; } return 0; } }; }; // plugins/lineheight.js /** * 设置行内间距 * @file * @since 1.2.6.1 */ UE.plugins['lineheight'] = function(){ var me = this; me.setOpt({'lineheight':['1', '1.5','1.75','2', '3', '4', '5']}); /** * 行距 * @command lineheight * @method execCommand * @param { String } cmdName 命令字符串 * @param { String } value 传入的行高值, 该值是当前字体的倍数, 例如: 1.5, 1.75 * @example * ```javascript * editor.execCommand( 'lineheight', 1.5); * ``` */ /** * 查询当前选区内容的行高大小 * @command lineheight * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { String } 返回当前行高大小 * @example * ```javascript * editor.queryCommandValue( 'lineheight' ); * ``` */ me.commands['lineheight'] = { execCommand : function( cmdName,value ) { this.execCommand('paragraph','p',{style:'line-height:'+ (value == "1" ? "normal" : value + 'em') }); return true; }, queryCommandValue : function() { var pN = domUtils.filterNodeList(this.selection.getStartElementPath(),function(node){return domUtils.isBlockElm(node)}); if(pN){ var value = domUtils.getComputedStyle(pN,'line-height'); return value == 'normal' ? 1 : value.replace(/[^\d.]*/ig,""); } } }; }; // plugins/insertcode.js /** * 插入代码插件 * @file * @since 1.2.6.1 */ UE.plugins['insertcode'] = function() { var me = this; me.ready(function(){ utils.cssRule('pre','pre{margin:.5em 0;padding:.4em .6em;border-radius:8px;background:#f8f8f8;}', me.document) }); me.setOpt('insertcode',{ 'as3':'ActionScript3', 'bash':'Bash/Shell', 'cpp':'C/C++', 'css':'Css', 'cf':'CodeFunction', 'c#':'C#', 'delphi':'Delphi', 'diff':'Diff', 'erlang':'Erlang', 'groovy':'Groovy', 'html':'Html', 'java':'Java', 'jfx':'JavaFx', 'js':'Javascript', 'pl':'Perl', 'php':'Php', 'plain':'Plain Text', 'ps':'PowerShell', 'python':'Python', 'ruby':'Ruby', 'scala':'Scala', 'sql':'Sql', 'vb':'Vb', 'xml':'Xml' }); /** * 插入代码 * @command insertcode * @method execCommand * @param { String } cmd 命令字符串 * @param { String } lang 插入代码的语言 * @example * ```javascript * editor.execCommand( 'insertcode', 'javascript' ); * ``` */ /** * 如果选区所在位置是插入插入代码区域,返回代码的语言 * @command insertcode * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { String } 返回代码的语言 * @example * ```javascript * editor.queryCommandValue( 'insertcode' ); * ``` */ me.commands['insertcode'] = { execCommand : function(cmd,lang){ var me = this, rng = me.selection.getRange(), pre = domUtils.findParentByTagName(rng.startContainer,'pre',true); if(pre){ pre.className = 'brush:'+lang+';toolbar:false;'; }else{ var code = ''; if(rng.collapsed){ code = browser.ie && browser.ie11below ? (browser.version <= 8 ? ' ':''):'
    '; }else{ var frag = rng.extractContents(); var div = me.document.createElement('div'); div.appendChild(frag); utils.each(UE.filterNode(UE.htmlparser(div.innerHTML.replace(/[\r\t]/g,'')),me.options.filterTxtRules).children,function(node){ if(browser.ie && browser.ie11below && browser.version > 8){ if(node.type =='element'){ if(node.tagName == 'br'){ code += '\n' }else if(!dtd.$empty[node.tagName]){ utils.each(node.children,function(cn){ if(cn.type =='element'){ if(cn.tagName == 'br'){ code += '\n' }else if(!dtd.$empty[node.tagName]){ code += cn.innerText(); } }else{ code += cn.data } }) if(!/\n$/.test(code)){ code += '\n'; } } }else{ code += node.data + '\n' } if(!node.nextSibling() && /\n$/.test(code)){ code = code.replace(/\n$/,''); } }else{ if(browser.ie && browser.ie11below){ if(node.type =='element'){ if(node.tagName == 'br'){ code += '
    ' }else if(!dtd.$empty[node.tagName]){ utils.each(node.children,function(cn){ if(cn.type =='element'){ if(cn.tagName == 'br'){ code += '
    ' }else if(!dtd.$empty[node.tagName]){ code += cn.innerText(); } }else{ code += cn.data } }); if(!/br>$/.test(code)){ code += '
    '; } } }else{ code += node.data + '
    ' } if(!node.nextSibling() && /
    $/.test(code)){ code = code.replace(/
    $/,''); } }else{ code += (node.type == 'element' ? (dtd.$empty[node.tagName] ? '' : node.innerText()) : node.data); if(!/br\/?\s*>$/.test(code)){ if(!node.nextSibling()) return; code += '
    ' } } } }); } me.execCommand('inserthtml','
    '+code+'
    ',true); pre = me.document.getElementById('coder'); domUtils.removeAttributes(pre,'id'); var tmpNode = pre.previousSibling; if(tmpNode && (tmpNode.nodeType == 3 && tmpNode.nodeValue.length == 1 && browser.ie && browser.version == 6 || domUtils.isEmptyBlock(tmpNode))){ domUtils.remove(tmpNode) } var rng = me.selection.getRange(); if(domUtils.isEmptyBlock(pre)){ rng.setStart(pre,0).setCursor(false,true) }else{ rng.selectNodeContents(pre).select() } } }, queryCommandValue : function(){ var path = this.selection.getStartElementPath(); var lang = ''; utils.each(path,function(node){ if(node.nodeName =='PRE'){ var match = node.className.match(/brush:([^;]+)/); lang = match && match[1] ? match[1] : ''; return false; } }); return lang; } }; me.addInputRule(function(root){ utils.each(root.getNodesByTagName('pre'),function(pre){ var brs = pre.getNodesByTagName('br'); if(brs.length){ browser.ie && browser.ie11below && browser.version > 8 && utils.each(brs,function(br){ var txt = UE.uNode.createText('\n'); br.parentNode.insertBefore(txt,br); br.parentNode.removeChild(br); }); return; } if(browser.ie && browser.ie11below && browser.version > 8) return; var code = pre.innerText().split(/\n/); pre.innerHTML(''); utils.each(code,function(c){ if(c.length){ pre.appendChild(UE.uNode.createText(c)); } pre.appendChild(UE.uNode.createElement('br')) }) }) }); me.addOutputRule(function(root){ utils.each(root.getNodesByTagName('pre'),function(pre){ var code = ''; utils.each(pre.children,function(n){ if(n.type == 'text'){ //在ie下文本内容有可能末尾带有\n要去掉 //trace:3396 code += n.data.replace(/[ ]/g,' ').replace(/\n$/,''); }else{ if(n.tagName == 'br'){ code += '\n' }else{ code += (!dtd.$empty[n.tagName] ? '' : n.innerText()); } } }); pre.innerText(code.replace(/( |\n)+$/,'')) }) }); //不需要判断highlight的command列表 me.notNeedCodeQuery ={ help:1, undo:1, redo:1, source:1, print:1, searchreplace:1, fullscreen:1, preview:1, insertparagraph:1, elementpath:1, insertcode:1, inserthtml:1, selectall:1 }; //将queyCommamndState重置 var orgQuery = me.queryCommandState; me.queryCommandState = function(cmd){ var me = this; if(!me.notNeedCodeQuery[cmd.toLowerCase()] && me.selection && me.queryCommandValue('insertcode')){ return -1; } return UE.Editor.prototype.queryCommandState.apply(this,arguments) }; me.addListener('beforeenterkeydown',function(){ var rng = me.selection.getRange(); var pre = domUtils.findParentByTagName(rng.startContainer,'pre',true); if(pre){ me.fireEvent('saveScene'); if(!rng.collapsed){ rng.deleteContents(); } if(!browser.ie || browser.ie9above){ var tmpNode = me.document.createElement('br'),pre; rng.insertNode(tmpNode).setStartAfter(tmpNode).collapse(true); var next = tmpNode.nextSibling; if(!next && (!browser.ie || browser.version > 10)){ rng.insertNode(tmpNode.cloneNode(false)); }else{ rng.setStartAfter(tmpNode); } pre = tmpNode.previousSibling; var tmp; while(pre ){ tmp = pre; pre = pre.previousSibling; if(!pre || pre.nodeName == 'BR'){ pre = tmp; break; } } if(pre){ var str = ''; while(pre && pre.nodeName != 'BR' && new RegExp('^[\\s'+domUtils.fillChar+']*$').test(pre.nodeValue)){ str += pre.nodeValue; pre = pre.nextSibling; } if(pre.nodeName != 'BR'){ var match = pre.nodeValue.match(new RegExp('^([\\s'+domUtils.fillChar+']+)')); if(match && match[1]){ str += match[1] } } if(str){ str = me.document.createTextNode(str); rng.insertNode(str).setStartAfter(str); } } rng.collapse(true).select(true); }else{ if(browser.version > 8){ var txt = me.document.createTextNode('\n'); var start = rng.startContainer; if(rng.startOffset == 0){ var preNode = start.previousSibling; if(preNode){ rng.insertNode(txt); var fillchar = me.document.createTextNode(' '); rng.setStartAfter(txt).insertNode(fillchar).setStart(fillchar,0).collapse(true).select(true) } }else{ rng.insertNode(txt).setStartAfter(txt); var fillchar = me.document.createTextNode(' '); start = rng.startContainer.childNodes[rng.startOffset]; if(start && !/^\n/.test(start.nodeValue)){ rng.setStartBefore(txt) } rng.insertNode(fillchar).setStart(fillchar,0).collapse(true).select(true) } }else{ var tmpNode = me.document.createElement('br'); rng.insertNode(tmpNode); rng.insertNode(me.document.createTextNode(domUtils.fillChar)); rng.setStartAfter(tmpNode); pre = tmpNode.previousSibling; var tmp; while(pre ){ tmp = pre; pre = pre.previousSibling; if(!pre || pre.nodeName == 'BR'){ pre = tmp; break; } } if(pre){ var str = ''; while(pre && pre.nodeName != 'BR' && new RegExp('^[ '+domUtils.fillChar+']*$').test(pre.nodeValue)){ str += pre.nodeValue; pre = pre.nextSibling; } if(pre.nodeName != 'BR'){ var match = pre.nodeValue.match(new RegExp('^([ '+domUtils.fillChar+']+)')); if(match && match[1]){ str += match[1] } } str = me.document.createTextNode(str); rng.insertNode(str).setStartAfter(str); } rng.collapse(true).select(); } } me.fireEvent('saveScene'); return true; } }); me.addListener('tabkeydown',function(cmd,evt){ var rng = me.selection.getRange(); var pre = domUtils.findParentByTagName(rng.startContainer,'pre',true); if(pre){ me.fireEvent('saveScene'); if(evt.shiftKey){ }else{ if(!rng.collapsed){ var bk = rng.createBookmark(); var start = bk.start.previousSibling; while(start){ if(pre.firstChild === start && !domUtils.isBr(start)){ pre.insertBefore(me.document.createTextNode(' '),start); break; } if(domUtils.isBr(start)){ pre.insertBefore(me.document.createTextNode(' '),start.nextSibling); break; } start = start.previousSibling; } var end = bk.end; start = bk.start.nextSibling; if(pre.firstChild === bk.start){ pre.insertBefore(me.document.createTextNode(' '),start.nextSibling) } while(start && start !== end){ if(domUtils.isBr(start) && start.nextSibling){ if(start.nextSibling === end){ break; } pre.insertBefore(me.document.createTextNode(' '),start.nextSibling) } start = start.nextSibling; } rng.moveToBookmark(bk).select(); }else{ var tmpNode = me.document.createTextNode(' '); rng.insertNode(tmpNode).setStartAfter(tmpNode).collapse(true).select(true); } } me.fireEvent('saveScene'); return true; } }); me.addListener('beforeinserthtml',function(evtName,html){ var me = this, rng = me.selection.getRange(), pre = domUtils.findParentByTagName(rng.startContainer,'pre',true); if(pre){ if(!rng.collapsed){ rng.deleteContents() } var htmlstr = ''; if(browser.ie && browser.version > 8){ utils.each(UE.filterNode(UE.htmlparser(html),me.options.filterTxtRules).children,function(node){ if(node.type =='element'){ if(node.tagName == 'br'){ htmlstr += '\n' }else if(!dtd.$empty[node.tagName]){ utils.each(node.children,function(cn){ if(cn.type =='element'){ if(cn.tagName == 'br'){ htmlstr += '\n' }else if(!dtd.$empty[node.tagName]){ htmlstr += cn.innerText(); } }else{ htmlstr += cn.data } }) if(!/\n$/.test(htmlstr)){ htmlstr += '\n'; } } }else{ htmlstr += node.data + '\n' } if(!node.nextSibling() && /\n$/.test(htmlstr)){ htmlstr = htmlstr.replace(/\n$/,''); } }); var tmpNode = me.document.createTextNode(utils.html(htmlstr.replace(/ /g,' '))); rng.insertNode(tmpNode).selectNode(tmpNode).select(); }else{ var frag = me.document.createDocumentFragment(); utils.each(UE.filterNode(UE.htmlparser(html),me.options.filterTxtRules).children,function(node){ if(node.type =='element'){ if(node.tagName == 'br'){ frag.appendChild(me.document.createElement('br')) }else if(!dtd.$empty[node.tagName]){ utils.each(node.children,function(cn){ if(cn.type =='element'){ if(cn.tagName == 'br'){ frag.appendChild(me.document.createElement('br')) }else if(!dtd.$empty[node.tagName]){ frag.appendChild(me.document.createTextNode(utils.html(cn.innerText().replace(/ /g,' ')))); } }else{ frag.appendChild(me.document.createTextNode(utils.html( cn.data.replace(/ /g,' ')))); } }) if(frag.lastChild.nodeName != 'BR'){ frag.appendChild(me.document.createElement('br')) } } }else{ frag.appendChild(me.document.createTextNode(utils.html( node.data.replace(/ /g,' ')))); } if(!node.nextSibling() && frag.lastChild.nodeName == 'BR'){ frag.removeChild(frag.lastChild) } }); rng.insertNode(frag).select(); } return true; } }); //方向键的处理 me.addListener('keydown',function(cmd,evt){ var me = this,keyCode = evt.keyCode || evt.which; if(keyCode == 40){ var rng = me.selection.getRange(),pre,start = rng.startContainer; if(rng.collapsed && (pre = domUtils.findParentByTagName(rng.startContainer,'pre',true)) && !pre.nextSibling){ var last = pre.lastChild while(last && last.nodeName == 'BR'){ last = last.previousSibling; } if(last === start || rng.startContainer === pre && rng.startOffset == pre.childNodes.length){ me.execCommand('insertparagraph'); domUtils.preventDefault(evt) } } } }); //trace:3395 me.addListener('delkeydown',function(type,evt){ var rng = this.selection.getRange(); rng.txtToElmBoundary(true); var start = rng.startContainer; if(domUtils.isTagNode(start,'pre') && rng.collapsed && domUtils.isStartInblock(rng)){ var p = me.document.createElement('p'); domUtils.fillNode(me.document,p); start.parentNode.insertBefore(p,start); domUtils.remove(start); rng.setStart(p,0).setCursor(false,true); domUtils.preventDefault(evt); return true; } }) }; // plugins/cleardoc.js /** * 清空文档插件 * @file * @since 1.2.6.1 */ /** * 清空文档 * @command cleardoc * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * //editor 是编辑器实例 * editor.execCommand('cleardoc'); * ``` */ UE.commands['cleardoc'] = { execCommand : function( cmdName) { var me = this, enterTag = me.options.enterTag, range = me.selection.getRange(); if(enterTag == "br"){ me.body.innerHTML = "
    "; range.setStart(me.body,0).setCursor(); }else{ me.body.innerHTML = "

    "+(ie ? "" : "
    ")+"

    "; range.setStart(me.body.firstChild,0).setCursor(false,true); } setTimeout(function(){ me.fireEvent("clearDoc"); },0); } }; // plugins/anchor.js /** * 锚点插件,为UEditor提供插入锚点支持 * @file * @since 1.2.6.1 */ UE.plugin.register('anchor', function (){ return { bindEvents:{ 'ready':function(){ utils.cssRule('anchor', '.anchorclass{background: url(\'' + this.options.themePath + this.options.theme +'/images/anchor.gif\') no-repeat scroll left center transparent;cursor: auto;display: inline-block;height: 16px;width: 15px;}', this.document); } }, outputRule: function(root){ utils.each(root.getNodesByTagName('img'),function(a){ var val; if(val = a.getAttr('anchorname')){ a.tagName = 'a'; a.setAttr({ anchorname : '', name : val, 'class' : '' }) } }) }, inputRule:function(root){ utils.each(root.getNodesByTagName('a'),function(a){ var val; if((val = a.getAttr('name')) && !a.getAttr('href')){ a.tagName = 'img'; a.setAttr({ anchorname :a.getAttr('name'), 'class' : 'anchorclass' }); a.setAttr('name') } }) }, commands:{ /** * 插入锚点 * @command anchor * @method execCommand * @param { String } cmd 命令字符串 * @param { String } name 锚点名称字符串 * @example * ```javascript * //editor 是编辑器实例 * editor.execCommand('anchor', 'anchor1'); * ``` */ 'anchor':{ execCommand:function (cmd, name) { var range = this.selection.getRange(),img = range.getClosedNode(); if (img && img.getAttribute('anchorname')) { if (name) { img.setAttribute('anchorname', name); } else { range.setStartBefore(img).setCursor(); domUtils.remove(img); } } else { if (name) { //只在选区的开始插入 var anchor = this.document.createElement('img'); range.collapse(true); domUtils.setAttributes(anchor,{ 'anchorname':name, 'class':'anchorclass' }); range.insertNode(anchor).setStartAfter(anchor).setCursor(false,true); } } } } } } }); // plugins/wordcount.js ///import core ///commands 字数统计 ///commandsName WordCount,wordCount ///commandsTitle 字数统计 /* * Created by JetBrains WebStorm. * User: taoqili * Date: 11-9-7 * Time: 下午8:18 * To change this template use File | Settings | File Templates. */ UE.plugins['wordcount'] = function(){ var me = this; me.setOpt('wordCount',true); me.addListener('contentchange',function(){ me.fireEvent('wordcount'); }); var timer; me.addListener('ready',function(){ var me = this; domUtils.on(me.body,"keyup",function(evt){ var code = evt.keyCode||evt.which, //忽略的按键,ctr,alt,shift,方向键 ignores = {"16":1,"18":1,"20":1,"37":1,"38":1,"39":1,"40":1}; if(code in ignores) return; clearTimeout(timer); timer = setTimeout(function(){ me.fireEvent('wordcount'); },200) }) }); }; // plugins/pagebreak.js /** * 分页功能插件 * @file * @since 1.2.6.1 */ UE.plugins['pagebreak'] = function () { var me = this, notBreakTags = ['td']; me.setOpt('pageBreakTag','_ueditor_page_break_tag_'); function fillNode(node){ if(domUtils.isEmptyBlock(node)){ var firstChild = node.firstChild,tmpNode; while(firstChild && firstChild.nodeType == 1 && domUtils.isEmptyBlock(firstChild)){ tmpNode = firstChild; firstChild = firstChild.firstChild; } !tmpNode && (tmpNode = node); domUtils.fillNode(me.document,tmpNode); } } //分页符样式添加 me.ready(function(){ utils.cssRule('pagebreak','.pagebreak{display:block;clear:both !important;cursor:default !important;width: 100% !important;margin:0;}',me.document); }); function isHr(node){ return node && node.nodeType == 1 && node.tagName == 'HR' && node.className == 'pagebreak'; } me.addInputRule(function(root){ root.traversal(function(node){ if(node.type == 'text' && node.data == me.options.pageBreakTag){ var hr = UE.uNode.createElement('
    '); node.parentNode.insertBefore(hr,node); node.parentNode.removeChild(node) } }) }); me.addOutputRule(function(node){ utils.each(node.getNodesByTagName('hr'),function(n){ if(n.getAttr('class') == 'pagebreak'){ var txt = UE.uNode.createText(me.options.pageBreakTag); n.parentNode.insertBefore(txt,n); n.parentNode.removeChild(n); } }) }); /** * 插入分页符 * @command pagebreak * @method execCommand * @param { String } cmd 命令字符串 * @remind 在表格中插入分页符会把表格切分成两部分 * @remind 获取编辑器内的数据时, 编辑器会把分页符转换成“_ueditor_page_break_tag_”字符串, * 以便于提交数据到服务器端后处理分页。 * @example * ```javascript * editor.execCommand( 'pagebreak'); //插入一个hr标签,带有样式类名pagebreak * ``` */ me.commands['pagebreak'] = { execCommand:function () { var range = me.selection.getRange(),hr = me.document.createElement('hr'); domUtils.setAttributes(hr,{ 'class' : 'pagebreak', noshade:"noshade", size:"5" }); domUtils.unSelectable(hr); //table单独处理 var node = domUtils.findParentByTagName(range.startContainer, notBreakTags, true), parents = [], pN; if (node) { switch (node.tagName) { case 'TD': pN = node.parentNode; if (!pN.previousSibling) { var table = domUtils.findParentByTagName(pN, 'table'); // var tableWrapDiv = table.parentNode; // if(tableWrapDiv && tableWrapDiv.nodeType == 1 // && tableWrapDiv.tagName == 'DIV' // && tableWrapDiv.getAttribute('dropdrag') // ){ // domUtils.remove(tableWrapDiv,true); // } table.parentNode.insertBefore(hr, table); parents = domUtils.findParents(hr, true); } else { pN.parentNode.insertBefore(hr, pN); parents = domUtils.findParents(hr); } pN = parents[1]; if (hr !== pN) { domUtils.breakParent(hr, pN); } //table要重写绑定一下拖拽 me.fireEvent('afteradjusttable',me.document); } } else { if (!range.collapsed) { range.deleteContents(); var start = range.startContainer; while ( !domUtils.isBody(start) && domUtils.isBlockElm(start) && domUtils.isEmptyNode(start)) { range.setStartBefore(start).collapse(true); domUtils.remove(start); start = range.startContainer; } } range.insertNode(hr); var pN = hr.parentNode, nextNode; while (!domUtils.isBody(pN)) { domUtils.breakParent(hr, pN); nextNode = hr.nextSibling; if (nextNode && domUtils.isEmptyBlock(nextNode)) { domUtils.remove(nextNode); } pN = hr.parentNode; } nextNode = hr.nextSibling; var pre = hr.previousSibling; if(isHr(pre)){ domUtils.remove(pre); }else{ pre && fillNode(pre); } if(!nextNode){ var p = me.document.createElement('p'); hr.parentNode.appendChild(p); domUtils.fillNode(me.document,p); range.setStart(p,0).collapse(true); }else{ if(isHr(nextNode)){ domUtils.remove(nextNode); }else{ fillNode(nextNode); } range.setEndAfter(hr).collapse(false); } range.select(true); } } }; }; // plugins/wordimage.js ///import core ///commands 本地图片引导上传 ///commandsName WordImage ///commandsTitle 本地图片引导上传 ///commandsDialog dialogs\wordimage UE.plugin.register('wordimage',function(){ var me = this, images = []; return { commands : { 'wordimage':{ execCommand:function () { var images = domUtils.getElementsByTagName(me.body, "img"); var urlList = []; for (var i = 0, ci; ci = images[i++];) { var url = ci.getAttribute("word_img"); url && urlList.push(url); } return urlList; }, queryCommandState:function () { images = domUtils.getElementsByTagName(me.body, "img"); for (var i = 0, ci; ci = images[i++];) { if (ci.getAttribute("word_img")) { return 1; } } return -1; }, notNeedUndo:true } }, inputRule : function (root) { utils.each(root.getNodesByTagName('img'), function (img) { var attrs = img.attrs, flag = parseInt(attrs.width) < 128 || parseInt(attrs.height) < 43, opt = me.options, src = opt.UEDITOR_HOME_URL + 'themes/default/images/spacer.gif'; if (attrs['src'] && /^(?:(file:\/+))/.test(attrs['src'])) { img.setAttr({ width:attrs.width, height:attrs.height, alt:attrs.alt, word_img: attrs.src, src:src, 'style':'background:url(' + ( flag ? opt.themePath + opt.theme + '/images/word.gif' : opt.langPath + opt.lang + '/images/localimage.png') + ') no-repeat center center;border:1px solid #ddd' }) } }) } } }); // plugins/dragdrop.js UE.plugins['dragdrop'] = function (){ var me = this; me.ready(function(){ domUtils.on(this.body,'dragend',function(){ var rng = me.selection.getRange(); var node = rng.getClosedNode()||me.selection.getStart(); if(node && node.tagName == 'IMG'){ var pre = node.previousSibling,next; while(next = node.nextSibling){ if(next.nodeType == 1 && next.tagName == 'SPAN' && !next.firstChild){ domUtils.remove(next) }else{ break; } } if((pre && pre.nodeType == 1 && !domUtils.isEmptyBlock(pre) || !pre) && (!next || next && !domUtils.isEmptyBlock(next))){ if(pre && pre.tagName == 'P' && !domUtils.isEmptyBlock(pre)){ pre.appendChild(node); domUtils.moveChild(next,pre); domUtils.remove(next); }else if(next && next.tagName == 'P' && !domUtils.isEmptyBlock(next)){ next.insertBefore(node,next.firstChild); } if(pre && pre.tagName == 'P' && domUtils.isEmptyBlock(pre)){ domUtils.remove(pre) } if(next && next.tagName == 'P' && domUtils.isEmptyBlock(next)){ domUtils.remove(next) } rng.selectNode(node).select(); me.fireEvent('saveScene'); } } }) }); me.addListener('keyup', function(type, evt) { var keyCode = evt.keyCode || evt.which; if (keyCode == 13) { var rng = me.selection.getRange(),node; if(node = domUtils.findParentByTagName(rng.startContainer,'p',true)){ if(domUtils.getComputedStyle(node,'text-align') == 'center'){ domUtils.removeStyle(node,'text-align') } } } }) }; // plugins/undo.js /** * undo redo * @file * @since 1.2.6.1 */ /** * 撤销上一次执行的命令 * @command undo * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'undo' ); * ``` */ /** * 重做上一次执行的命令 * @command redo * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'redo' ); * ``` */ UE.plugins['undo'] = function () { var saveSceneTimer; var me = this, maxUndoCount = me.options.maxUndoCount || 20, maxInputCount = me.options.maxInputCount || 20, fillchar = new RegExp(domUtils.fillChar + '|<\/hr>', 'gi');// ie会产生多余的 var noNeedFillCharTags = { ol:1,ul:1,table:1,tbody:1,tr:1,body:1 }; var orgState = me.options.autoClearEmptyNode; function compareAddr(indexA, indexB) { if (indexA.length != indexB.length) return 0; for (var i = 0, l = indexA.length; i < l; i++) { if (indexA[i] != indexB[i]) return 0 } return 1; } function compareRangeAddress(rngAddrA, rngAddrB) { if (rngAddrA.collapsed != rngAddrB.collapsed) { return 0; } if (!compareAddr(rngAddrA.startAddress, rngAddrB.startAddress) || !compareAddr(rngAddrA.endAddress, rngAddrB.endAddress)) { return 0; } return 1; } function UndoManager() { this.list = []; this.index = 0; this.hasUndo = false; this.hasRedo = false; this.undo = function () { if (this.hasUndo) { if (!this.list[this.index - 1] && this.list.length == 1) { this.reset(); return; } while (this.list[this.index].content == this.list[this.index - 1].content) { this.index--; if (this.index == 0) { return this.restore(0); } } this.restore(--this.index); } }; this.redo = function () { if (this.hasRedo) { while (this.list[this.index].content == this.list[this.index + 1].content) { this.index++; if (this.index == this.list.length - 1) { return this.restore(this.index); } } this.restore(++this.index); } }; this.restore = function () { var me = this.editor; var scene = this.list[this.index]; var root = UE.htmlparser(scene.content.replace(fillchar, '')); me.options.autoClearEmptyNode = false; me.filterInputRule(root); me.options.autoClearEmptyNode = orgState; //trace:873 //去掉展位符 me.document.body.innerHTML = root.toHtml(); me.fireEvent('afterscencerestore'); //处理undo后空格不展位的问题 if (browser.ie) { utils.each(domUtils.getElementsByTagName(me.document,'td th caption p'),function(node){ if(domUtils.isEmptyNode(node)){ domUtils.fillNode(me.document, node); } }) } try{ var rng = new dom.Range(me.document).moveToAddress(scene.address); rng.select(noNeedFillCharTags[rng.startContainer.nodeName.toLowerCase()]); }catch(e){} this.update(); this.clearKey(); //不能把自己reset了 me.fireEvent('reset', true); }; this.getScene = function () { var me = this.editor; var rng = me.selection.getRange(), rngAddress = rng.createAddress(false,true); me.fireEvent('beforegetscene'); var root = UE.htmlparser(me.body.innerHTML); me.options.autoClearEmptyNode = false; me.filterOutputRule(root); me.options.autoClearEmptyNode = orgState; var cont = root.toHtml(); //trace:3461 //这个会引起回退时导致空格丢失的情况 // browser.ie && (cont = cont.replace(/> <').replace(/\s*\s*/g, '>')); me.fireEvent('aftergetscene'); return { address:rngAddress, content:cont } }; this.save = function (notCompareRange,notSetCursor) { clearTimeout(saveSceneTimer); var currentScene = this.getScene(notSetCursor), lastScene = this.list[this.index]; if(lastScene && lastScene.content != currentScene.content){ me.trigger('contentchange') } //内容相同位置相同不存 if (lastScene && lastScene.content == currentScene.content && ( notCompareRange ? 1 : compareRangeAddress(lastScene.address, currentScene.address) ) ) { return; } this.list = this.list.slice(0, this.index + 1); this.list.push(currentScene); //如果大于最大数量了,就把最前的剔除 if (this.list.length > maxUndoCount) { this.list.shift(); } this.index = this.list.length - 1; this.clearKey(); //跟新undo/redo状态 this.update(); }; this.update = function () { this.hasRedo = !!this.list[this.index + 1]; this.hasUndo = !!this.list[this.index - 1]; }; this.reset = function () { this.list = []; this.index = 0; this.hasUndo = false; this.hasRedo = false; this.clearKey(); }; this.clearKey = function () { keycont = 0; lastKeyCode = null; }; } me.undoManger = new UndoManager(); me.undoManger.editor = me; function saveScene() { this.undoManger.save(); } me.addListener('saveScene', function () { var args = Array.prototype.splice.call(arguments,1); this.undoManger.save.apply(this.undoManger,args); }); // me.addListener('beforeexeccommand', saveScene); // me.addListener('afterexeccommand', saveScene); me.addListener('reset', function (type, exclude) { if (!exclude) { this.undoManger.reset(); } }); me.commands['redo'] = me.commands['undo'] = { execCommand:function (cmdName) { this.undoManger[cmdName](); }, queryCommandState:function (cmdName) { return this.undoManger['has' + (cmdName.toLowerCase() == 'undo' ? 'Undo' : 'Redo')] ? 0 : -1; }, notNeedUndo:1 }; var keys = { // /*Backspace*/ 8:1, /*Delete*/ 46:1, /*Shift*/ 16:1, /*Ctrl*/ 17:1, /*Alt*/ 18:1, 37:1, 38:1, 39:1, 40:1 }, keycont = 0, lastKeyCode; //输入法状态下不计算字符数 var inputType = false; me.addListener('ready', function () { domUtils.on(this.body, 'compositionstart', function () { inputType = true; }); domUtils.on(this.body, 'compositionend', function () { inputType = false; }) }); //快捷键 me.addshortcutkey({ "Undo":"ctrl+90", //undo "Redo":"ctrl+89" //redo }); var isCollapsed = true; me.addListener('keydown', function (type, evt) { var me = this; var keyCode = evt.keyCode || evt.which; if (!keys[keyCode] && !evt.ctrlKey && !evt.metaKey && !evt.shiftKey && !evt.altKey) { if (inputType) return; if(!me.selection.getRange().collapsed){ me.undoManger.save(false,true); isCollapsed = false; return; } if (me.undoManger.list.length == 0) { me.undoManger.save(true); } clearTimeout(saveSceneTimer); function save(cont){ cont.undoManger.save(false,true); cont.fireEvent('selectionchange'); } saveSceneTimer = setTimeout(function(){ if(inputType){ var interalTimer = setInterval(function(){ if(!inputType){ save(me); clearInterval(interalTimer) } },300) return; } save(me); },200); lastKeyCode = keyCode; keycont++; if (keycont >= maxInputCount ) { save(me) } } }); me.addListener('keyup', function (type, evt) { var keyCode = evt.keyCode || evt.which; if (!keys[keyCode] && !evt.ctrlKey && !evt.metaKey && !evt.shiftKey && !evt.altKey) { if (inputType) return; if(!isCollapsed){ this.undoManger.save(false,true); isCollapsed = true; } } }); //扩展实例,添加关闭和开启命令undo me.stopCmdUndo = function(){ me.__hasEnterExecCommand = true; }; me.startCmdUndo = function(){ me.__hasEnterExecCommand = false; } }; // plugins/copy.js UE.plugin.register('copy', function () { var me = this; function initZeroClipboard() { ZeroClipboard.config({ debug: false, swfPath: me.options.UEDITOR_HOME_URL + 'third-party/zeroclipboard/ZeroClipboard.swf' }); var client = me.zeroclipboard = new ZeroClipboard(); // 复制内容 client.on('copy', function (e) { var client = e.client, rng = me.selection.getRange(), div = document.createElement('div'); div.appendChild(rng.cloneContents()); client.setText(div.innerText || div.textContent); client.setHtml(div.innerHTML); rng.select(); }); // hover事件传递到target client.on('mouseover mouseout', function (e) { var target = e.target; if (e.type == 'mouseover') { domUtils.addClass(target, 'edui-state-hover'); } else if (e.type == 'mouseout') { domUtils.removeClasses(target, 'edui-state-hover'); } }); // flash加载不成功 client.on('wrongflash noflash', function () { ZeroClipboard.destroy(); }); } return { bindEvents: { 'ready': function () { if (!browser.ie) { if (window.ZeroClipboard) { initZeroClipboard(); } else { utils.loadFile(document, { src: me.options.UEDITOR_HOME_URL + "third-party/zeroclipboard/ZeroClipboard.js", tag: "script", type: "text/javascript", defer: "defer" }, function () { initZeroClipboard(); }); } } } }, commands: { 'copy': { execCommand: function (cmd) { if (!me.document.execCommand('copy')) { alert(me.getLang('copymsg')); } } } } } }); // plugins/paste.js ///import core ///import plugins/inserthtml.js ///import plugins/undo.js ///import plugins/serialize.js ///commands 粘贴 ///commandsName PastePlain ///commandsTitle 纯文本粘贴模式 /** * @description 粘贴 * @author zhanyi */ UE.plugins['paste'] = function () { function getClipboardData(callback) { var doc = this.document; if (doc.getElementById('baidu_pastebin')) { return; } var range = this.selection.getRange(), bk = range.createBookmark(), //创建剪贴的容器div pastebin = doc.createElement('div'); pastebin.id = 'baidu_pastebin'; // Safari 要求div必须有内容,才能粘贴内容进来 browser.webkit && pastebin.appendChild(doc.createTextNode(domUtils.fillChar + domUtils.fillChar)); doc.body.appendChild(pastebin); //trace:717 隐藏的span不能得到top //bk.start.innerHTML = ' '; bk.start.style.display = ''; pastebin.style.cssText = "position:absolute;width:1px;height:1px;overflow:hidden;left:-1000px;white-space:nowrap;top:" + //要在现在光标平行的位置加入,否则会出现跳动的问题 domUtils.getXY(bk.start).y + 'px'; range.selectNodeContents(pastebin).select(true); setTimeout(function () { if (browser.webkit) { for (var i = 0, pastebins = doc.querySelectorAll('#baidu_pastebin'), pi; pi = pastebins[i++];) { if (domUtils.isEmptyNode(pi)) { domUtils.remove(pi); } else { pastebin = pi; break; } } } try { pastebin.parentNode.removeChild(pastebin); } catch (e) { } range.moveToBookmark(bk).select(true); callback(pastebin); }, 0); } var me = this; me.setOpt({ retainOnlyLabelPasted : false }); var txtContent, htmlContent, address; function getPureHtml(html){ return html.replace(/<(\/?)([\w\-]+)([^>]*)>/gi, function (a, b, tagName, attrs) { tagName = tagName.toLowerCase(); if ({img: 1}[tagName]) { return a; } attrs = attrs.replace(/([\w\-]*?)\s*=\s*(("([^"]*)")|('([^']*)')|([^\s>]+))/gi, function (str, atr, val) { if ({ 'src': 1, 'href': 1, 'name': 1 }[atr.toLowerCase()]) { return atr + '=' + val + ' ' } return '' }); if ({ 'span': 1, 'div': 1 }[tagName]) { return '' } else { return '<' + b + tagName + ' ' + utils.trim(attrs) + '>' } }); } function filter(div) { var html; if (div.firstChild) { //去掉cut中添加的边界值 var nodes = domUtils.getElementsByTagName(div, 'span'); for (var i = 0, ni; ni = nodes[i++];) { if (ni.id == '_baidu_cut_start' || ni.id == '_baidu_cut_end') { domUtils.remove(ni); } } if (browser.webkit) { var brs = div.querySelectorAll('div br'); for (var i = 0, bi; bi = brs[i++];) { var pN = bi.parentNode; if (pN.tagName == 'DIV' && pN.childNodes.length == 1) { pN.innerHTML = '


    '; domUtils.remove(pN); } } var divs = div.querySelectorAll('#baidu_pastebin'); for (var i = 0, di; di = divs[i++];) { var tmpP = me.document.createElement('p'); di.parentNode.insertBefore(tmpP, di); while (di.firstChild) { tmpP.appendChild(di.firstChild); } domUtils.remove(di); } var metas = div.querySelectorAll('meta'); for (var i = 0, ci; ci = metas[i++];) { domUtils.remove(ci); } var brs = div.querySelectorAll('br'); for (i = 0; ci = brs[i++];) { if (/^apple-/i.test(ci.className)) { domUtils.remove(ci); } } } if (browser.gecko) { var dirtyNodes = div.querySelectorAll('[_moz_dirty]'); for (i = 0; ci = dirtyNodes[i++];) { ci.removeAttribute('_moz_dirty'); } } if (!browser.ie) { var spans = div.querySelectorAll('span.Apple-style-span'); for (var i = 0, ci; ci = spans[i++];) { domUtils.remove(ci, true); } } //ie下使用innerHTML会产生多余的\r\n字符,也会产生 这里过滤掉 html = div.innerHTML;//.replace(/>(?:(\s| )*?)<'); //过滤word粘贴过来的冗余属性 html = UE.filterWord(html); //取消了忽略空白的第二个参数,粘贴过来的有些是有空白的,会被套上相关的标签 var root = UE.htmlparser(html); //如果给了过滤规则就先进行过滤 if (me.options.filterRules) { UE.filterNode(root, me.options.filterRules); } //执行默认的处理 me.filterInputRule(root); //针对chrome的处理 if (browser.webkit) { var br = root.lastChild(); if (br && br.type == 'element' && br.tagName == 'br') { root.removeChild(br) } utils.each(me.body.querySelectorAll('div'), function (node) { if (domUtils.isEmptyBlock(node)) { domUtils.remove(node,true) } }) } html = {'html': root.toHtml()}; me.fireEvent('beforepaste', html, root); //抢了默认的粘贴,那后边的内容就不执行了,比如表格粘贴 if(!html.html){ return; } root = UE.htmlparser(html.html,true); //如果开启了纯文本模式 if (me.queryCommandState('pasteplain') === 1) { me.execCommand('insertHtml', UE.filterNode(root, me.options.filterTxtRules).toHtml(), true); } else { //文本模式 UE.filterNode(root, me.options.filterTxtRules); txtContent = root.toHtml(); //完全模式 htmlContent = html.html; address = me.selection.getRange().createAddress(true); me.execCommand('insertHtml', me.getOpt('retainOnlyLabelPasted') === true ? getPureHtml(htmlContent) : htmlContent, true); } me.fireEvent("afterpaste", html); } } me.addListener('pasteTransfer', function (cmd, plainType) { if (address && txtContent && htmlContent && txtContent != htmlContent) { var range = me.selection.getRange(); range.moveToAddress(address, true); if (!range.collapsed) { while (!domUtils.isBody(range.startContainer) ) { var start = range.startContainer; if(start.nodeType == 1){ start = start.childNodes[range.startOffset]; if(!start){ range.setStartBefore(range.startContainer); continue; } var pre = start.previousSibling; if(pre && pre.nodeType == 3 && new RegExp('^[\n\r\t '+domUtils.fillChar+']*$').test(pre.nodeValue)){ range.setStartBefore(pre) } } if(range.startOffset == 0){ range.setStartBefore(range.startContainer); }else{ break; } } while (!domUtils.isBody(range.endContainer) ) { var end = range.endContainer; if(end.nodeType == 1){ end = end.childNodes[range.endOffset]; if(!end){ range.setEndAfter(range.endContainer); continue; } var next = end.nextSibling; if(next && next.nodeType == 3 && new RegExp('^[\n\r\t'+domUtils.fillChar+']*$').test(next.nodeValue)){ range.setEndAfter(next) } } if(range.endOffset == range.endContainer[range.endContainer.nodeType == 3 ? 'nodeValue' : 'childNodes'].length){ range.setEndAfter(range.endContainer); }else{ break; } } } range.deleteContents(); range.select(true); me.__hasEnterExecCommand = true; var html = htmlContent; if (plainType === 2 ) { html = getPureHtml(html); } else if (plainType) { html = txtContent; } me.execCommand('inserthtml', html, true); me.__hasEnterExecCommand = false; var rng = me.selection.getRange(); while (!domUtils.isBody(rng.startContainer) && !rng.startOffset && rng.startContainer[rng.startContainer.nodeType == 3 ? 'nodeValue' : 'childNodes'].length ) { rng.setStartBefore(rng.startContainer); } var tmpAddress = rng.createAddress(true); address.endAddress = tmpAddress.startAddress; } }); me.addListener('ready', function () { domUtils.on(me.body, 'cut', function () { var range = me.selection.getRange(); if (!range.collapsed && me.undoManger) { me.undoManger.save(); } }); //ie下beforepaste在点击右键时也会触发,所以用监控键盘才处理 domUtils.on(me.body, browser.ie || browser.opera ? 'keydown' : 'paste', function (e) { if ((browser.ie || browser.opera) && ((!e.ctrlKey && !e.metaKey) || e.keyCode != '86')) { return; } getClipboardData.call(me, function (div) { filter(div); }); }); }); me.commands['paste'] = { execCommand: function (cmd) { if (browser.ie) { getClipboardData.call(me, function (div) { filter(div); }); me.document.execCommand('paste'); } else { alert(me.getLang('pastemsg')); } } } }; // plugins/puretxtpaste.js /** * 纯文本粘贴插件 * @file * @since 1.2.6.1 */ UE.plugins['pasteplain'] = function(){ var me = this; me.setOpt({ 'pasteplain':false, 'filterTxtRules' : function(){ function transP(node){ node.tagName = 'p'; node.setStyle(); } function removeNode(node){ node.parentNode.removeChild(node,true) } return { //直接删除及其字节点内容 '-' : 'script style object iframe embed input select', 'p': {$:{}}, 'br':{$:{}}, div: function (node) { var tmpNode, p = UE.uNode.createElement('p'); while (tmpNode = node.firstChild()) { if (tmpNode.type == 'text' || !UE.dom.dtd.$block[tmpNode.tagName]) { p.appendChild(tmpNode); } else { if (p.firstChild()) { node.parentNode.insertBefore(p, node); p = UE.uNode.createElement('p'); } else { node.parentNode.insertBefore(tmpNode, node); } } } if (p.firstChild()) { node.parentNode.insertBefore(p, node); } node.parentNode.removeChild(node); }, ol: removeNode, ul: removeNode, dl:removeNode, dt:removeNode, dd:removeNode, 'li':removeNode, 'caption':transP, 'th':transP, 'tr':transP, 'h1':transP,'h2':transP,'h3':transP,'h4':transP,'h5':transP,'h6':transP, 'td':function(node){ //没有内容的td直接删掉 var txt = !!node.innerText(); if(txt){ node.parentNode.insertAfter(UE.uNode.createText('    '),node); } node.parentNode.removeChild(node,node.innerText()) } } }() }); //暂时这里支持一下老版本的属性 var pasteplain = me.options.pasteplain; /** * 启用或取消纯文本粘贴模式 * @command pasteplain * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.queryCommandState( 'pasteplain' ); * ``` */ /** * 查询当前是否处于纯文本粘贴模式 * @command pasteplain * @method queryCommandState * @param { String } cmd 命令字符串 * @return { int } 如果处于纯文本模式,返回1,否则,返回0 * @example * ```javascript * editor.queryCommandState( 'pasteplain' ); * ``` */ me.commands['pasteplain'] = { queryCommandState: function (){ return pasteplain ? 1 : 0; }, execCommand: function (){ pasteplain = !pasteplain|0; }, notNeedUndo : 1 }; }; // plugins/list.js /** * 有序列表,无序列表插件 * @file * @since 1.2.6.1 */ UE.plugins['list'] = function () { var me = this, notExchange = { 'TD':1, 'PRE':1, 'BLOCKQUOTE':1 }; var customStyle = { 'cn' : 'cn-1-', 'cn1' : 'cn-2-', 'cn2' : 'cn-3-', 'num': 'num-1-', 'num1' : 'num-2-', 'num2' : 'num-3-', 'dash' : 'dash', 'dot':'dot' }; me.setOpt( { 'autoTransWordToList':false, 'insertorderedlist':{ 'num':'', 'num1':'', 'num2':'', 'cn':'', 'cn1':'', 'cn2':'', 'decimal':'', 'lower-alpha':'', 'lower-roman':'', 'upper-alpha':'', 'upper-roman':'' }, 'insertunorderedlist':{ 'circle':'', 'disc':'', 'square':'', 'dash' : '', 'dot':'' }, listDefaultPaddingLeft : '30', listiconpath : 'http://bs.baidu.com/listicon/', maxListLevel : -1,//-1不限制 disablePInList:false } ); function listToArray(list){ var arr = []; for(var p in list){ arr.push(p) } return arr; } var listStyle = { 'OL':listToArray(me.options.insertorderedlist), 'UL':listToArray(me.options.insertunorderedlist) }; var liiconpath = me.options.listiconpath; //根据用户配置,调整customStyle for(var s in customStyle){ if(!me.options.insertorderedlist.hasOwnProperty(s) && !me.options.insertunorderedlist.hasOwnProperty(s)){ delete customStyle[s]; } } me.ready(function () { var customCss = []; for(var p in customStyle){ if(p == 'dash' || p == 'dot'){ customCss.push('li.list-' + customStyle[p] + '{background-image:url(' + liiconpath +customStyle[p]+'.gif)}'); customCss.push('ul.custom_'+p+'{list-style:none;}ul.custom_'+p+' li{background-position:0 3px;background-repeat:no-repeat}'); }else{ for(var i= 0;i<99;i++){ customCss.push('li.list-' + customStyle[p] + i + '{background-image:url(' + liiconpath + 'list-'+customStyle[p] + i + '.gif)}') } customCss.push('ol.custom_'+p+'{list-style:none;}ol.custom_'+p+' li{background-position:0 3px;background-repeat:no-repeat}'); } switch(p){ case 'cn': customCss.push('li.list-'+p+'-paddingleft-1{padding-left:25px}'); customCss.push('li.list-'+p+'-paddingleft-2{padding-left:40px}'); customCss.push('li.list-'+p+'-paddingleft-3{padding-left:55px}'); break; case 'cn1': customCss.push('li.list-'+p+'-paddingleft-1{padding-left:30px}'); customCss.push('li.list-'+p+'-paddingleft-2{padding-left:40px}'); customCss.push('li.list-'+p+'-paddingleft-3{padding-left:55px}'); break; case 'cn2': customCss.push('li.list-'+p+'-paddingleft-1{padding-left:40px}'); customCss.push('li.list-'+p+'-paddingleft-2{padding-left:55px}'); customCss.push('li.list-'+p+'-paddingleft-3{padding-left:68px}'); break; case 'num': case 'num1': customCss.push('li.list-'+p+'-paddingleft-1{padding-left:25px}'); break; case 'num2': customCss.push('li.list-'+p+'-paddingleft-1{padding-left:35px}'); customCss.push('li.list-'+p+'-paddingleft-2{padding-left:40px}'); break; case 'dash': customCss.push('li.list-'+p+'-paddingleft{padding-left:35px}'); break; case 'dot': customCss.push('li.list-'+p+'-paddingleft{padding-left:20px}'); } } customCss.push('.list-paddingleft-1{padding-left:0}'); customCss.push('.list-paddingleft-2{padding-left:'+me.options.listDefaultPaddingLeft+'px}'); customCss.push('.list-paddingleft-3{padding-left:'+me.options.listDefaultPaddingLeft*2+'px}'); //如果不给宽度会在自定应样式里出现滚动条 utils.cssRule('list', 'ol,ul{margin:0;pading:0;'+(browser.ie ? '' : 'width:95%')+'}li{clear:both;}'+customCss.join('\n'), me.document); }); //单独处理剪切的问题 me.ready(function(){ domUtils.on(me.body,'cut',function(){ setTimeout(function(){ var rng = me.selection.getRange(),li; //trace:3416 if(!rng.collapsed){ if(li = domUtils.findParentByTagName(rng.startContainer,'li',true)){ if(!li.nextSibling && domUtils.isEmptyBlock(li)){ var pn = li.parentNode,node; if(node = pn.previousSibling){ domUtils.remove(pn); rng.setStartAtLast(node).collapse(true); rng.select(true); }else if(node = pn.nextSibling){ domUtils.remove(pn); rng.setStartAtFirst(node).collapse(true); rng.select(true); }else{ var tmpNode = me.document.createElement('p'); domUtils.fillNode(me.document,tmpNode); pn.parentNode.insertBefore(tmpNode,pn); domUtils.remove(pn); rng.setStart(tmpNode,0).collapse(true); rng.select(true); } } } } }) }) }); function getStyle(node){ var cls = node.className; if(domUtils.hasClass(node,/custom_/)){ return cls.match(/custom_(\w+)/)[1] } return domUtils.getStyle(node, 'list-style-type') } me.addListener('beforepaste',function(type,html){ var me = this, rng = me.selection.getRange(),li; var root = UE.htmlparser(html.html,true); if(li = domUtils.findParentByTagName(rng.startContainer,'li',true)){ var list = li.parentNode,tagName = list.tagName == 'OL' ? 'ul':'ol'; utils.each(root.getNodesByTagName(tagName),function(n){ n.tagName = list.tagName; n.setAttr(); if(n.parentNode === root){ type = getStyle(list) || (list.tagName == 'OL' ? 'decimal' : 'disc') }else{ var className = n.parentNode.getAttr('class'); if(className && /custom_/.test(className)){ type = className.match(/custom_(\w+)/)[1] }else{ type = n.parentNode.getStyle('list-style-type'); } if(!type){ type = list.tagName == 'OL' ? 'decimal' : 'disc'; } } var index = utils.indexOf(listStyle[list.tagName], type); if(n.parentNode !== root) index = index + 1 == listStyle[list.tagName].length ? 0 : index + 1; var currentStyle = listStyle[list.tagName][index]; if(customStyle[currentStyle]){ n.setAttr('class', 'custom_' + currentStyle) }else{ n.setStyle('list-style-type',currentStyle) } }) } html.html = root.toHtml(); }); //导出时,去掉p标签 me.getOpt('disablePInList') === true && me.addOutputRule(function(root){ utils.each(root.getNodesByTagName('li'),function(li){ var newChildrens = [],index=0; utils.each(li.children,function(n){ if(n.tagName == 'p'){ var tmpNode; while(tmpNode = n.children.pop()) { newChildrens.splice(index,0,tmpNode); tmpNode.parentNode = li; lastNode = tmpNode; } tmpNode = newChildrens[newChildrens.length-1]; if(!tmpNode || tmpNode.type != 'element' || tmpNode.tagName != 'br'){ var br = UE.uNode.createElement('br'); br.parentNode = li; newChildrens.push(br); } index = newChildrens.length; } }); if(newChildrens.length){ li.children = newChildrens; } }); }); //进入编辑器的li要套p标签 me.addInputRule(function(root){ utils.each(root.getNodesByTagName('li'),function(li){ var tmpP = UE.uNode.createElement('p'); for(var i= 0,ci;ci=li.children[i];){ if(ci.type == 'text' || dtd.p[ci.tagName]){ tmpP.appendChild(ci); }else{ if(tmpP.firstChild()){ li.insertBefore(tmpP,ci); tmpP = UE.uNode.createElement('p'); i = i + 2; }else{ i++; } } } if(tmpP.firstChild() && !tmpP.parentNode || !li.firstChild()){ li.appendChild(tmpP); } //trace:3357 //p不能为空 if (!tmpP.firstChild()) { tmpP.innerHTML(browser.ie ? ' ' : '
    ') } //去掉末尾的空白 var p = li.firstChild(); var lastChild = p.lastChild(); if(lastChild && lastChild.type == 'text' && /^\s*$/.test(lastChild.data)){ p.removeChild(lastChild) } }); if(me.options.autoTransWordToList){ var orderlisttype = { 'num1':/^\d+\)/, 'decimal':/^\d+\./, 'lower-alpha':/^[a-z]+\)/, 'upper-alpha':/^[A-Z]+\./, 'cn':/^[\u4E00\u4E8C\u4E09\u56DB\u516d\u4e94\u4e03\u516b\u4e5d]+[\u3001]/, 'cn2':/^\([\u4E00\u4E8C\u4E09\u56DB\u516d\u4e94\u4e03\u516b\u4e5d]+\)/ }, unorderlisttype = { 'square':'n' }; function checkListType(content,container){ var span = container.firstChild(); if(span && span.type == 'element' && span.tagName == 'span' && /Wingdings|Symbol/.test(span.getStyle('font-family'))){ for(var p in unorderlisttype){ if(unorderlisttype[p] == span.data){ return p } } return 'disc' } for(var p in orderlisttype){ if(orderlisttype[p].test(content)){ return p; } } } utils.each(root.getNodesByTagName('p'),function(node){ if(node.getAttr('class') != 'MsoListParagraph'){ return } //word粘贴过来的会带有margin要去掉,但这样也可能会误命中一些央视 node.setStyle('margin',''); node.setStyle('margin-left',''); node.setAttr('class',''); function appendLi(list,p,type){ if(list.tagName == 'ol'){ if(browser.ie){ var first = p.firstChild(); if(first.type =='element' && first.tagName == 'span' && orderlisttype[type].test(first.innerText())){ p.removeChild(first); } }else{ p.innerHTML(p.innerHTML().replace(orderlisttype[type],'')); } }else{ p.removeChild(p.firstChild()) } var li = UE.uNode.createElement('li'); li.appendChild(p); list.appendChild(li); } var tmp = node,type,cacheNode = node; if(node.parentNode.tagName != 'li' && (type = checkListType(node.innerText(),node))){ var list = UE.uNode.createElement(me.options.insertorderedlist.hasOwnProperty(type) ? 'ol' : 'ul'); if(customStyle[type]){ list.setAttr('class','custom_'+type) }else{ list.setStyle('list-style-type',type) } while(node && node.parentNode.tagName != 'li' && checkListType(node.innerText(),node)){ tmp = node.nextSibling(); if(!tmp){ node.parentNode.insertBefore(list,node) } appendLi(list,node,type); node = tmp; } if(!list.parentNode && node && node.parentNode){ node.parentNode.insertBefore(list,node) } } var span = cacheNode.firstChild(); if(span && span.type == 'element' && span.tagName == 'span' && /^\s*( )+\s*$/.test(span.innerText())){ span.parentNode.removeChild(span) } }) } }); //调整索引标签 me.addListener('contentchange',function(){ adjustListStyle(me.document) }); function adjustListStyle(doc,ignore){ utils.each(domUtils.getElementsByTagName(doc,'ol ul'),function(node){ if(!domUtils.inDoc(node,doc)) return; var parent = node.parentNode; if(parent.tagName == node.tagName){ var nodeStyleType = getStyle(node) || (node.tagName == 'OL' ? 'decimal' : 'disc'), parentStyleType = getStyle(parent) || (parent.tagName == 'OL' ? 'decimal' : 'disc'); if(nodeStyleType == parentStyleType){ var styleIndex = utils.indexOf(listStyle[node.tagName], nodeStyleType); styleIndex = styleIndex + 1 == listStyle[node.tagName].length ? 0 : styleIndex + 1; setListStyle(node,listStyle[node.tagName][styleIndex]) } } var index = 0,type = 2; if( domUtils.hasClass(node,/custom_/)){ if(!(/[ou]l/i.test(parent.tagName) && domUtils.hasClass(parent,/custom_/))){ type = 1; } }else{ if(/[ou]l/i.test(parent.tagName) && domUtils.hasClass(parent,/custom_/)){ type = 3; } } var style = domUtils.getStyle(node, 'list-style-type'); style && (node.style.cssText = 'list-style-type:' + style); node.className = utils.trim(node.className.replace(/list-paddingleft-\w+/,'')) + ' list-paddingleft-' + type; utils.each(domUtils.getElementsByTagName(node,'li'),function(li){ li.style.cssText && (li.style.cssText = ''); if(!li.firstChild){ domUtils.remove(li); return; } if(li.parentNode !== node){ return; } index++; if(domUtils.hasClass(node,/custom_/) ){ var paddingLeft = 1,currentStyle = getStyle(node); if(node.tagName == 'OL'){ if(currentStyle){ switch(currentStyle){ case 'cn' : case 'cn1': case 'cn2': if(index > 10 && (index % 10 == 0 || index > 10 && index < 20)){ paddingLeft = 2 }else if(index > 20){ paddingLeft = 3 } break; case 'num2' : if(index > 9){ paddingLeft = 2 } } } li.className = 'list-'+customStyle[currentStyle]+ index + ' ' + 'list-'+currentStyle+'-paddingleft-' + paddingLeft; }else{ li.className = 'list-'+customStyle[currentStyle] + ' ' + 'list-'+currentStyle+'-paddingleft'; } }else{ li.className = li.className.replace(/list-[\w\-]+/gi,''); } var className = li.getAttribute('class'); if(className !== null && !className.replace(/\s/g,'')){ domUtils.removeAttributes(li,'class') } }); !ignore && adjustList(node,node.tagName.toLowerCase(),getStyle(node)||domUtils.getStyle(node, 'list-style-type'),true); }) } function adjustList(list, tag, style,ignoreEmpty) { var nextList = list.nextSibling; if (nextList && nextList.nodeType == 1 && nextList.tagName.toLowerCase() == tag && (getStyle(nextList) || domUtils.getStyle(nextList, 'list-style-type') || (tag == 'ol' ? 'decimal' : 'disc')) == style) { domUtils.moveChild(nextList, list); if (nextList.childNodes.length == 0) { domUtils.remove(nextList); } } if(nextList && domUtils.isFillChar(nextList)){ domUtils.remove(nextList); } var preList = list.previousSibling; if (preList && preList.nodeType == 1 && preList.tagName.toLowerCase() == tag && (getStyle(preList) || domUtils.getStyle(preList, 'list-style-type') || (tag == 'ol' ? 'decimal' : 'disc')) == style) { domUtils.moveChild(list, preList); } if(preList && domUtils.isFillChar(preList)){ domUtils.remove(preList); } !ignoreEmpty && domUtils.isEmptyBlock(list) && domUtils.remove(list); if(getStyle(list)){ adjustListStyle(list.ownerDocument,true) } } function setListStyle(list,style){ if(customStyle[style]){ list.className = 'custom_' + style; } try{ domUtils.setStyle(list, 'list-style-type', style); }catch(e){} } function clearEmptySibling(node) { var tmpNode = node.previousSibling; if (tmpNode && domUtils.isEmptyBlock(tmpNode)) { domUtils.remove(tmpNode); } tmpNode = node.nextSibling; if (tmpNode && domUtils.isEmptyBlock(tmpNode)) { domUtils.remove(tmpNode); } } me.addListener('keydown', function (type, evt) { function preventAndSave() { evt.preventDefault ? evt.preventDefault() : (evt.returnValue = false); me.fireEvent('contentchange'); me.undoManger && me.undoManger.save(); } function findList(node,filterFn){ while(node && !domUtils.isBody(node)){ if(filterFn(node)){ return null } if(node.nodeType == 1 && /[ou]l/i.test(node.tagName)){ return node; } node = node.parentNode; } return null; } var keyCode = evt.keyCode || evt.which; if (keyCode == 13 && !evt.shiftKey) {//回车 var rng = me.selection.getRange(), parent = domUtils.findParent(rng.startContainer,function(node){return domUtils.isBlockElm(node)},true), li = domUtils.findParentByTagName(rng.startContainer,'li',true); if(parent && parent.tagName != 'PRE' && !li){ var html = parent.innerHTML.replace(new RegExp(domUtils.fillChar, 'g'),''); if(/^\s*1\s*\.[^\d]/.test(html)){ parent.innerHTML = html.replace(/^\s*1\s*\./,''); rng.setStartAtLast(parent).collapse(true).select(); me.__hasEnterExecCommand = true; me.execCommand('insertorderedlist'); me.__hasEnterExecCommand = false; } } var range = me.selection.getRange(), start = findList(range.startContainer,function (node) { return node.tagName == 'TABLE'; }), end = range.collapsed ? start : findList(range.endContainer,function (node) { return node.tagName == 'TABLE'; }); if (start && end && start === end) { if (!range.collapsed) { start = domUtils.findParentByTagName(range.startContainer, 'li', true); end = domUtils.findParentByTagName(range.endContainer, 'li', true); if (start && end && start === end) { range.deleteContents(); li = domUtils.findParentByTagName(range.startContainer, 'li', true); if (li && domUtils.isEmptyBlock(li)) { pre = li.previousSibling; next = li.nextSibling; p = me.document.createElement('p'); domUtils.fillNode(me.document, p); parentList = li.parentNode; if (pre && next) { range.setStart(next, 0).collapse(true).select(true); domUtils.remove(li); } else { if (!pre && !next || !pre) { parentList.parentNode.insertBefore(p, parentList); } else { li.parentNode.parentNode.insertBefore(p, parentList.nextSibling); } domUtils.remove(li); if (!parentList.firstChild) { domUtils.remove(parentList); } range.setStart(p, 0).setCursor(); } preventAndSave(); return; } } else { var tmpRange = range.cloneRange(), bk = tmpRange.collapse(false).createBookmark(); range.deleteContents(); tmpRange.moveToBookmark(bk); var li = domUtils.findParentByTagName(tmpRange.startContainer, 'li', true); clearEmptySibling(li); tmpRange.select(); preventAndSave(); return; } } li = domUtils.findParentByTagName(range.startContainer, 'li', true); if (li) { if (domUtils.isEmptyBlock(li)) { bk = range.createBookmark(); var parentList = li.parentNode; if (li !== parentList.lastChild) { domUtils.breakParent(li, parentList); clearEmptySibling(li); } else { parentList.parentNode.insertBefore(li, parentList.nextSibling); if (domUtils.isEmptyNode(parentList)) { domUtils.remove(parentList); } } //嵌套不处理 if (!dtd.$list[li.parentNode.tagName]) { if (!domUtils.isBlockElm(li.firstChild)) { p = me.document.createElement('p'); li.parentNode.insertBefore(p, li); while (li.firstChild) { p.appendChild(li.firstChild); } domUtils.remove(li); } else { domUtils.remove(li, true); } } range.moveToBookmark(bk).select(); } else { var first = li.firstChild; if (!first || !domUtils.isBlockElm(first)) { var p = me.document.createElement('p'); !li.firstChild && domUtils.fillNode(me.document, p); while (li.firstChild) { p.appendChild(li.firstChild); } li.appendChild(p); first = p; } var span = me.document.createElement('span'); range.insertNode(span); domUtils.breakParent(span, li); var nextLi = span.nextSibling; first = nextLi.firstChild; if (!first) { p = me.document.createElement('p'); domUtils.fillNode(me.document, p); nextLi.appendChild(p); first = p; } if (domUtils.isEmptyNode(first)) { first.innerHTML = ''; domUtils.fillNode(me.document, first); } range.setStart(first, 0).collapse(true).shrinkBoundary().select(); domUtils.remove(span); var pre = nextLi.previousSibling; if (pre && domUtils.isEmptyBlock(pre)) { pre.innerHTML = '

    '; domUtils.fillNode(me.document, pre.firstChild); } } // } preventAndSave(); } } } if (keyCode == 8) { //修中ie中li下的问题 range = me.selection.getRange(); if (range.collapsed && domUtils.isStartInblock(range)) { tmpRange = range.cloneRange().trimBoundary(); li = domUtils.findParentByTagName(range.startContainer, 'li', true); //要在li的最左边,才能处理 if (li && domUtils.isStartInblock(tmpRange)) { start = domUtils.findParentByTagName(range.startContainer, 'p', true); if (start && start !== li.firstChild) { var parentList = domUtils.findParentByTagName(start,['ol','ul']); domUtils.breakParent(start,parentList); clearEmptySibling(start); me.fireEvent('contentchange'); range.setStart(start,0).setCursor(false,true); me.fireEvent('saveScene'); domUtils.preventDefault(evt); return; } if (li && (pre = li.previousSibling)) { if (keyCode == 46 && li.childNodes.length) { return; } //有可能上边的兄弟节点是个2级菜单,要追加到2级菜单的最后的li if (dtd.$list[pre.tagName]) { pre = pre.lastChild; } me.undoManger && me.undoManger.save(); first = li.firstChild; if (domUtils.isBlockElm(first)) { if (domUtils.isEmptyNode(first)) { // range.setEnd(pre, pre.childNodes.length).shrinkBoundary().collapse().select(true); pre.appendChild(first); range.setStart(first, 0).setCursor(false, true); //first不是唯一的节点 while (li.firstChild) { pre.appendChild(li.firstChild); } } else { span = me.document.createElement('span'); range.insertNode(span); //判断pre是否是空的节点,如果是


    类型的空节点,干掉p标签防止它占位 if (domUtils.isEmptyBlock(pre)) { pre.innerHTML = ''; } domUtils.moveChild(li, pre); range.setStartBefore(span).collapse(true).select(true); domUtils.remove(span); } } else { if (domUtils.isEmptyNode(li)) { var p = me.document.createElement('p'); pre.appendChild(p); range.setStart(p, 0).setCursor(); // range.setEnd(pre, pre.childNodes.length).shrinkBoundary().collapse().select(true); } else { range.setEnd(pre, pre.childNodes.length).collapse().select(true); while (li.firstChild) { pre.appendChild(li.firstChild); } } } domUtils.remove(li); me.fireEvent('contentchange'); me.fireEvent('saveScene'); domUtils.preventDefault(evt); return; } //trace:980 if (li && !li.previousSibling) { var parentList = li.parentNode; var bk = range.createBookmark(); if(domUtils.isTagNode(parentList.parentNode,'ol ul')){ parentList.parentNode.insertBefore(li,parentList); if(domUtils.isEmptyNode(parentList)){ domUtils.remove(parentList) } }else{ while(li.firstChild){ parentList.parentNode.insertBefore(li.firstChild,parentList); } domUtils.remove(li); if(domUtils.isEmptyNode(parentList)){ domUtils.remove(parentList) } } range.moveToBookmark(bk).setCursor(false,true); me.fireEvent('contentchange'); me.fireEvent('saveScene'); domUtils.preventDefault(evt); return; } } } } }); me.addListener('keyup',function(type, evt){ var keyCode = evt.keyCode || evt.which; if (keyCode == 8) { var rng = me.selection.getRange(),list; if(list = domUtils.findParentByTagName(rng.startContainer,['ol', 'ul'],true)){ adjustList(list,list.tagName.toLowerCase(),getStyle(list)||domUtils.getComputedStyle(list,'list-style-type'),true) } } }); //处理tab键 me.addListener('tabkeydown',function(){ var range = me.selection.getRange(); //控制级数 function checkLevel(li){ if(me.options.maxListLevel != -1){ var level = li.parentNode,levelNum = 0; while(/[ou]l/i.test(level.tagName)){ levelNum++; level = level.parentNode; } if(levelNum >= me.options.maxListLevel){ return true; } } } //只以开始为准 //todo 后续改进 var li = domUtils.findParentByTagName(range.startContainer, 'li', true); if(li){ var bk; if(range.collapsed){ if(checkLevel(li)) return true; var parentLi = li.parentNode, list = me.document.createElement(parentLi.tagName), index = utils.indexOf(listStyle[list.tagName], getStyle(parentLi)||domUtils.getComputedStyle(parentLi, 'list-style-type')); index = index + 1 == listStyle[list.tagName].length ? 0 : index + 1; var currentStyle = listStyle[list.tagName][index]; setListStyle(list,currentStyle); if(domUtils.isStartInblock(range)){ me.fireEvent('saveScene'); bk = range.createBookmark(); parentLi.insertBefore(list, li); list.appendChild(li); adjustList(list,list.tagName.toLowerCase(),currentStyle); me.fireEvent('contentchange'); range.moveToBookmark(bk).select(true); return true; } }else{ me.fireEvent('saveScene'); bk = range.createBookmark(); for(var i= 0,closeList,parents = domUtils.findParents(li),ci;ci=parents[i++];){ if(domUtils.isTagNode(ci,'ol ul')){ closeList = ci; break; } } var current = li; if(bk.end){ while(current && !(domUtils.getPosition(current, bk.end) & domUtils.POSITION_FOLLOWING)){ if(checkLevel(current)){ current = domUtils.getNextDomNode(current,false,null,function(node){return node !== closeList}); continue; } var parentLi = current.parentNode, list = me.document.createElement(parentLi.tagName), index = utils.indexOf(listStyle[list.tagName], getStyle(parentLi)||domUtils.getComputedStyle(parentLi, 'list-style-type')); var currentIndex = index + 1 == listStyle[list.tagName].length ? 0 : index + 1; var currentStyle = listStyle[list.tagName][currentIndex]; setListStyle(list,currentStyle); parentLi.insertBefore(list, current); while(current && !(domUtils.getPosition(current, bk.end) & domUtils.POSITION_FOLLOWING)){ li = current.nextSibling; list.appendChild(current); if(!li || domUtils.isTagNode(li,'ol ul')){ if(li){ while(li = li.firstChild){ if(li.tagName == 'LI'){ break; } } }else{ li = domUtils.getNextDomNode(current,false,null,function(node){return node !== closeList}); } break; } current = li; } adjustList(list,list.tagName.toLowerCase(),currentStyle); current = li; } } me.fireEvent('contentchange'); range.moveToBookmark(bk).select(); return true; } } }); function getLi(start){ while(start && !domUtils.isBody(start)){ if(start.nodeName == 'TABLE'){ return null; } if(start.nodeName == 'LI'){ return start } start = start.parentNode; } } /** * 有序列表,与“insertunorderedlist”命令互斥 * @command insertorderedlist * @method execCommand * @param { String } command 命令字符串 * @param { String } style 插入的有序列表类型,值为:decimal,lower-alpha,lower-roman,upper-alpha,upper-roman,cn,cn1,cn2,num,num1,num2 * @example * ```javascript * editor.execCommand( 'insertorderedlist','decimal'); * ``` */ /** * 查询当前选区内容是否有序列表 * @command insertorderedlist * @method queryCommandState * @param { String } cmd 命令字符串 * @return { int } 如果当前选区是有序列表返回1,否则返回0 * @example * ```javascript * editor.queryCommandState( 'insertorderedlist' ); * ``` */ /** * 查询当前选区内容是否有序列表 * @command insertorderedlist * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { String } 返回当前有序列表的类型,值为null或decimal,lower-alpha,lower-roman,upper-alpha,upper-roman,cn,cn1,cn2,num,num1,num2 * @example * ```javascript * editor.queryCommandValue( 'insertorderedlist' ); * ``` */ /** * 无序列表,与“insertorderedlist”命令互斥 * @command insertunorderedlist * @method execCommand * @param { String } command 命令字符串 * @param { String } style 插入的无序列表类型,值为:circle,disc,square,dash,dot * @example * ```javascript * editor.execCommand( 'insertunorderedlist','circle'); * ``` */ /** * 查询当前是否有word文档粘贴进来的图片 * @command insertunorderedlist * @method insertunorderedlist * @param { String } command 命令字符串 * @return { int } 如果当前选区是无序列表返回1,否则返回0 * @example * ```javascript * editor.queryCommandState( 'insertunorderedlist' ); * ``` */ /** * 查询当前选区内容是否有序列表 * @command insertunorderedlist * @method queryCommandValue * @param { String } command 命令字符串 * @return { String } 返回当前无序列表的类型,值为null或circle,disc,square,dash,dot * @example * ```javascript * editor.queryCommandValue( 'insertunorderedlist' ); * ``` */ me.commands['insertorderedlist'] = me.commands['insertunorderedlist'] = { execCommand:function (command, style) { if (!style) { style = command.toLowerCase() == 'insertorderedlist' ? 'decimal' : 'disc'; } var me = this, range = this.selection.getRange(), filterFn = function (node) { return node.nodeType == 1 ? node.tagName.toLowerCase() != 'br' : !domUtils.isWhitespace(node); }, tag = command.toLowerCase() == 'insertorderedlist' ? 'ol' : 'ul', frag = me.document.createDocumentFragment(); //去掉是因为会出现选到末尾,导致adjustmentBoundary缩到ol/ul的位置 //range.shrinkBoundary();//.adjustmentBoundary(); range.adjustmentBoundary().shrinkBoundary(); var bko = range.createBookmark(true), start = getLi(me.document.getElementById(bko.start)), modifyStart = 0, end = getLi(me.document.getElementById(bko.end)), modifyEnd = 0, startParent, endParent, list, tmp; if (start || end) { start && (startParent = start.parentNode); if (!bko.end) { end = start; } end && (endParent = end.parentNode); if (startParent === endParent) { while (start !== end) { tmp = start; start = start.nextSibling; if (!domUtils.isBlockElm(tmp.firstChild)) { var p = me.document.createElement('p'); while (tmp.firstChild) { p.appendChild(tmp.firstChild); } tmp.appendChild(p); } frag.appendChild(tmp); } tmp = me.document.createElement('span'); startParent.insertBefore(tmp, end); if (!domUtils.isBlockElm(end.firstChild)) { p = me.document.createElement('p'); while (end.firstChild) { p.appendChild(end.firstChild); } end.appendChild(p); } frag.appendChild(end); domUtils.breakParent(tmp, startParent); if (domUtils.isEmptyNode(tmp.previousSibling)) { domUtils.remove(tmp.previousSibling); } if (domUtils.isEmptyNode(tmp.nextSibling)) { domUtils.remove(tmp.nextSibling) } var nodeStyle = getStyle(startParent) || domUtils.getComputedStyle(startParent, 'list-style-type') || (command.toLowerCase() == 'insertorderedlist' ? 'decimal' : 'disc'); if (startParent.tagName.toLowerCase() == tag && nodeStyle == style) { for (var i = 0, ci, tmpFrag = me.document.createDocumentFragment(); ci = frag.firstChild;) { if(domUtils.isTagNode(ci,'ol ul')){ // 删除时,子列表不处理 // utils.each(domUtils.getElementsByTagName(ci,'li'),function(li){ // while(li.firstChild){ // tmpFrag.appendChild(li.firstChild); // } // // }); tmpFrag.appendChild(ci); }else{ while (ci.firstChild) { tmpFrag.appendChild(ci.firstChild); domUtils.remove(ci); } } } tmp.parentNode.insertBefore(tmpFrag, tmp); } else { list = me.document.createElement(tag); setListStyle(list,style); list.appendChild(frag); tmp.parentNode.insertBefore(list, tmp); } domUtils.remove(tmp); list && adjustList(list, tag, style); range.moveToBookmark(bko).select(); return; } //开始 if (start) { while (start) { tmp = start.nextSibling; if (domUtils.isTagNode(start, 'ol ul')) { frag.appendChild(start); } else { var tmpfrag = me.document.createDocumentFragment(), hasBlock = 0; while (start.firstChild) { if (domUtils.isBlockElm(start.firstChild)) { hasBlock = 1; } tmpfrag.appendChild(start.firstChild); } if (!hasBlock) { var tmpP = me.document.createElement('p'); tmpP.appendChild(tmpfrag); frag.appendChild(tmpP); } else { frag.appendChild(tmpfrag); } domUtils.remove(start); } start = tmp; } startParent.parentNode.insertBefore(frag, startParent.nextSibling); if (domUtils.isEmptyNode(startParent)) { range.setStartBefore(startParent); domUtils.remove(startParent); } else { range.setStartAfter(startParent); } modifyStart = 1; } if (end && domUtils.inDoc(endParent, me.document)) { //结束 start = endParent.firstChild; while (start && start !== end) { tmp = start.nextSibling; if (domUtils.isTagNode(start, 'ol ul')) { frag.appendChild(start); } else { tmpfrag = me.document.createDocumentFragment(); hasBlock = 0; while (start.firstChild) { if (domUtils.isBlockElm(start.firstChild)) { hasBlock = 1; } tmpfrag.appendChild(start.firstChild); } if (!hasBlock) { tmpP = me.document.createElement('p'); tmpP.appendChild(tmpfrag); frag.appendChild(tmpP); } else { frag.appendChild(tmpfrag); } domUtils.remove(start); } start = tmp; } var tmpDiv = domUtils.createElement(me.document, 'div', { 'tmpDiv':1 }); domUtils.moveChild(end, tmpDiv); frag.appendChild(tmpDiv); domUtils.remove(end); endParent.parentNode.insertBefore(frag, endParent); range.setEndBefore(endParent); if (domUtils.isEmptyNode(endParent)) { domUtils.remove(endParent); } modifyEnd = 1; } } if (!modifyStart) { range.setStartBefore(me.document.getElementById(bko.start)); } if (bko.end && !modifyEnd) { range.setEndAfter(me.document.getElementById(bko.end)); } range.enlarge(true, function (node) { return notExchange[node.tagName]; }); frag = me.document.createDocumentFragment(); var bk = range.createBookmark(), current = domUtils.getNextDomNode(bk.start, false, filterFn), tmpRange = range.cloneRange(), tmpNode, block = domUtils.isBlockElm; while (current && current !== bk.end && (domUtils.getPosition(current, bk.end) & domUtils.POSITION_PRECEDING)) { if (current.nodeType == 3 || dtd.li[current.tagName]) { if (current.nodeType == 1 && dtd.$list[current.tagName]) { while (current.firstChild) { frag.appendChild(current.firstChild); } tmpNode = domUtils.getNextDomNode(current, false, filterFn); domUtils.remove(current); current = tmpNode; continue; } tmpNode = current; tmpRange.setStartBefore(current); while (current && current !== bk.end && (!block(current) || domUtils.isBookmarkNode(current) )) { tmpNode = current; current = domUtils.getNextDomNode(current, false, null, function (node) { return !notExchange[node.tagName]; }); } if (current && block(current)) { tmp = domUtils.getNextDomNode(tmpNode, false, filterFn); if (tmp && domUtils.isBookmarkNode(tmp)) { current = domUtils.getNextDomNode(tmp, false, filterFn); tmpNode = tmp; } } tmpRange.setEndAfter(tmpNode); current = domUtils.getNextDomNode(tmpNode, false, filterFn); var li = range.document.createElement('li'); li.appendChild(tmpRange.extractContents()); if(domUtils.isEmptyNode(li)){ var tmpNode = range.document.createElement('p'); while(li.firstChild){ tmpNode.appendChild(li.firstChild) } li.appendChild(tmpNode); } frag.appendChild(li); } else { current = domUtils.getNextDomNode(current, true, filterFn); } } range.moveToBookmark(bk).collapse(true); list = me.document.createElement(tag); setListStyle(list,style); list.appendChild(frag); range.insertNode(list); //当前list上下看能否合并 adjustList(list, tag, style); //去掉冗余的tmpDiv for (var i = 0, ci, tmpDivs = domUtils.getElementsByTagName(list, 'div'); ci = tmpDivs[i++];) { if (ci.getAttribute('tmpDiv')) { domUtils.remove(ci, true) } } range.moveToBookmark(bko).select(); }, queryCommandState:function (command) { var tag = command.toLowerCase() == 'insertorderedlist' ? 'ol' : 'ul'; var path = this.selection.getStartElementPath(); for(var i= 0,ci;ci = path[i++];){ if(ci.nodeName == 'TABLE'){ return 0 } if(tag == ci.nodeName.toLowerCase()){ return 1 }; } return 0; }, queryCommandValue:function (command) { var tag = command.toLowerCase() == 'insertorderedlist' ? 'ol' : 'ul'; var path = this.selection.getStartElementPath(), node; for(var i= 0,ci;ci = path[i++];){ if(ci.nodeName == 'TABLE'){ node = null; break; } if(tag == ci.nodeName.toLowerCase()){ node = ci; break; }; } return node ? getStyle(node) || domUtils.getComputedStyle(node, 'list-style-type') : null; } }; }; // plugins/source.js /** * 源码编辑插件 * @file * @since 1.2.6.1 */ (function (){ var sourceEditors = { textarea: function (editor, holder){ var textarea = holder.ownerDocument.createElement('textarea'); textarea.style.cssText = 'position:absolute;resize:none;width:100%;height:100%;border:0;padding:0;margin:0;overflow-y:auto;'; // todo: IE下只有onresize属性可用... 很纠结 if (browser.ie && browser.version < 8) { textarea.style.width = holder.offsetWidth + 'px'; textarea.style.height = holder.offsetHeight + 'px'; holder.onresize = function (){ textarea.style.width = holder.offsetWidth + 'px'; textarea.style.height = holder.offsetHeight + 'px'; }; } holder.appendChild(textarea); return { setContent: function (content){ textarea.value = content; }, getContent: function (){ return textarea.value; }, select: function (){ var range; if (browser.ie) { range = textarea.createTextRange(); range.collapse(true); range.select(); } else { //todo: chrome下无法设置焦点 textarea.setSelectionRange(0, 0); textarea.focus(); } }, dispose: function (){ holder.removeChild(textarea); // todo holder.onresize = null; textarea = null; holder = null; } }; }, codemirror: function (editor, holder){ var codeEditor = window.CodeMirror(holder, { mode: "text/html", tabMode: "indent", lineNumbers: true, lineWrapping:true }); var dom = codeEditor.getWrapperElement(); dom.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:100%;font-family:consolas,"Courier new",monospace;font-size:13px;'; codeEditor.getScrollerElement().style.cssText = 'position:absolute;left:0;top:0;width:100%;height:100%;'; codeEditor.refresh(); return { getCodeMirror:function(){ return codeEditor; }, setContent: function (content){ codeEditor.setValue(content); }, getContent: function (){ return codeEditor.getValue(); }, select: function (){ codeEditor.focus(); }, dispose: function (){ holder.removeChild(dom); dom = null; codeEditor = null; } }; } }; UE.plugins['source'] = function (){ var me = this; var opt = this.options; var sourceMode = false; var sourceEditor; var orgSetContent; opt.sourceEditor = browser.ie ? 'textarea' : (opt.sourceEditor || 'codemirror'); me.setOpt({ sourceEditorFirst:false }); function createSourceEditor(holder){ return sourceEditors[opt.sourceEditor == 'codemirror' && window.CodeMirror ? 'codemirror' : 'textarea'](me, holder); } var bakCssText; //解决在源码模式下getContent不能得到最新的内容问题 var oldGetContent, bakAddress; /** * 切换源码模式和编辑模式 * @command source * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'source'); * ``` */ /** * 查询当前编辑区域的状态是源码模式还是可视化模式 * @command source * @method queryCommandState * @param { String } cmd 命令字符串 * @return { int } 如果当前是源码编辑模式,返回1,否则返回0 * @example * ```javascript * editor.queryCommandState( 'source' ); * ``` */ me.commands['source'] = { execCommand: function (){ sourceMode = !sourceMode; if (sourceMode) { bakAddress = me.selection.getRange().createAddress(false,true); me.undoManger && me.undoManger.save(true); if(browser.gecko){ me.body.contentEditable = false; } bakCssText = me.iframe.style.cssText; me.iframe.style.cssText += 'position:absolute;left:-32768px;top:-32768px;'; me.fireEvent('beforegetcontent'); var root = UE.htmlparser(me.body.innerHTML); me.filterOutputRule(root); root.traversal(function (node) { if (node.type == 'element') { switch (node.tagName) { case 'td': case 'th': case 'caption': if(node.children && node.children.length == 1){ if(node.firstChild().tagName == 'br' ){ node.removeChild(node.firstChild()) } }; break; case 'pre': node.innerText(node.innerText().replace(/ /g,' ')) } } }); me.fireEvent('aftergetcontent'); var content = root.toHtml(true); sourceEditor = createSourceEditor(me.iframe.parentNode); sourceEditor.setContent(content); orgSetContent = me.setContent; me.setContent = function(html){ //这里暂时不触发事件,防止报错 var root = UE.htmlparser(html); me.filterInputRule(root); html = root.toHtml(); sourceEditor.setContent(html); }; setTimeout(function (){ sourceEditor.select(); me.addListener('fullscreenchanged', function(){ try{ sourceEditor.getCodeMirror().refresh() }catch(e){} }); }); //重置getContent,源码模式下取值也能是最新的数据 oldGetContent = me.getContent; me.getContent = function (){ return sourceEditor.getContent() || '

    ' + (browser.ie ? '' : '
    ')+'

    '; }; } else { me.iframe.style.cssText = bakCssText; var cont = sourceEditor.getContent() || '

    ' + (browser.ie ? '' : '
    ')+'

    '; //处理掉block节点前后的空格,有可能会误命中,暂时不考虑 cont = cont.replace(new RegExp('[\\r\\t\\n ]*<\/?(\\w+)\\s*(?:[^>]*)>','g'), function(a,b){ if(b && !dtd.$inlineWithA[b.toLowerCase()]){ return a.replace(/(^[\n\r\t ]*)|([\n\r\t ]*$)/g,''); } return a.replace(/(^[\n\r\t]*)|([\n\r\t]*$)/g,'') }); me.setContent = orgSetContent; me.setContent(cont); sourceEditor.dispose(); sourceEditor = null; //还原getContent方法 me.getContent = oldGetContent; var first = me.body.firstChild; //trace:1106 都删除空了,下边会报错,所以补充一个p占位 if(!first){ me.body.innerHTML = '

    '+(browser.ie?'':'
    ')+'

    '; first = me.body.firstChild; } //要在ifm为显示时ff才能取到selection,否则报错 //这里不能比较位置了 me.undoManger && me.undoManger.save(true); if(browser.gecko){ var input = document.createElement('input'); input.style.cssText = 'position:absolute;left:0;top:-32768px'; document.body.appendChild(input); me.body.contentEditable = false; setTimeout(function(){ domUtils.setViewportOffset(input, { left: -32768, top: 0 }); input.focus(); setTimeout(function(){ me.body.contentEditable = true; me.selection.getRange().moveToAddress(bakAddress).select(true); domUtils.remove(input); }); }); }else{ //ie下有可能报错,比如在代码顶头的情况 try{ me.selection.getRange().moveToAddress(bakAddress).select(true); }catch(e){} } } this.fireEvent('sourcemodechanged', sourceMode); }, queryCommandState: function (){ return sourceMode|0; }, notNeedUndo : 1 }; var oldQueryCommandState = me.queryCommandState; me.queryCommandState = function (cmdName){ cmdName = cmdName.toLowerCase(); if (sourceMode) { //源码模式下可以开启的命令 return cmdName in { 'source' : 1, 'fullscreen' : 1 } ? 1 : -1 } return oldQueryCommandState.apply(this, arguments); }; if(opt.sourceEditor == "codemirror"){ me.addListener("ready",function(){ utils.loadFile(document,{ src : opt.codeMirrorJsUrl || opt.UEDITOR_HOME_URL + "third-party/codemirror/codemirror.js", tag : "script", type : "text/javascript", defer : "defer" },function(){ if(opt.sourceEditorFirst){ setTimeout(function(){ me.execCommand("source"); },0); } }); utils.loadFile(document,{ tag : "link", rel : "stylesheet", type : "text/css", href : opt.codeMirrorCssUrl || opt.UEDITOR_HOME_URL + "third-party/codemirror/codemirror.css" }); }); } }; })(); // plugins/enterkey.js ///import core ///import plugins/undo.js ///commands 设置回车标签p或br ///commandsName EnterKey ///commandsTitle 设置回车标签p或br /** * @description 处理回车 * @author zhanyi */ UE.plugins['enterkey'] = function() { var hTag, me = this, tag = me.options.enterTag; me.addListener('keyup', function(type, evt) { var keyCode = evt.keyCode || evt.which; if (keyCode == 13) { var range = me.selection.getRange(), start = range.startContainer, doSave; //修正在h1-h6里边回车后不能嵌套p的问题 if (!browser.ie) { if (/h\d/i.test(hTag)) { if (browser.gecko) { var h = domUtils.findParentByTagName(start, [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6','blockquote','caption','table'], true); if (!h) { me.document.execCommand('formatBlock', false, '

    '); doSave = 1; } } else { //chrome remove div if (start.nodeType == 1) { var tmp = me.document.createTextNode(''),div; range.insertNode(tmp); div = domUtils.findParentByTagName(tmp, 'div', true); if (div) { var p = me.document.createElement('p'); while (div.firstChild) { p.appendChild(div.firstChild); } div.parentNode.insertBefore(p, div); domUtils.remove(div); range.setStartBefore(tmp).setCursor(); doSave = 1; } domUtils.remove(tmp); } } if (me.undoManger && doSave) { me.undoManger.save(); } } //没有站位符,会出现多行的问题 browser.opera && range.select(); }else{ me.fireEvent('saveScene',true,true) } } }); me.addListener('keydown', function(type, evt) { var keyCode = evt.keyCode || evt.which; if (keyCode == 13) {//回车 if(me.fireEvent('beforeenterkeydown')){ domUtils.preventDefault(evt); return; } me.fireEvent('saveScene',true,true); hTag = ''; var range = me.selection.getRange(); if (!range.collapsed) { //跨td不能删 var start = range.startContainer, end = range.endContainer, startTd = domUtils.findParentByTagName(start, 'td', true), endTd = domUtils.findParentByTagName(end, 'td', true); if (startTd && endTd && startTd !== endTd || !startTd && endTd || startTd && !endTd) { evt.preventDefault ? evt.preventDefault() : ( evt.returnValue = false); return; } } if (tag == 'p') { if (!browser.ie) { start = domUtils.findParentByTagName(range.startContainer, ['ol','ul','p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6','blockquote','caption'], true); //opera下执行formatblock会在table的场景下有问题,回车在opera原生支持很好,所以暂时在opera去掉调用这个原生的command //trace:2431 if (!start && !browser.opera) { me.document.execCommand('formatBlock', false, '

    '); if (browser.gecko) { range = me.selection.getRange(); start = domUtils.findParentByTagName(range.startContainer, 'p', true); start && domUtils.removeDirtyAttr(start); } } else { hTag = start.tagName; start.tagName.toLowerCase() == 'p' && browser.gecko && domUtils.removeDirtyAttr(start); } } } else { evt.preventDefault ? evt.preventDefault() : ( evt.returnValue = false); if (!range.collapsed) { range.deleteContents(); start = range.startContainer; if (start.nodeType == 1 && (start = start.childNodes[range.startOffset])) { while (start.nodeType == 1) { if (dtd.$empty[start.tagName]) { range.setStartBefore(start).setCursor(); if (me.undoManger) { me.undoManger.save(); } return false; } if (!start.firstChild) { var br = range.document.createElement('br'); start.appendChild(br); range.setStart(start, 0).setCursor(); if (me.undoManger) { me.undoManger.save(); } return false; } start = start.firstChild; } if (start === range.startContainer.childNodes[range.startOffset]) { br = range.document.createElement('br'); range.insertNode(br).setCursor(); } else { range.setStart(start, 0).setCursor(); } } else { br = range.document.createElement('br'); range.insertNode(br).setStartAfter(br).setCursor(); } } else { br = range.document.createElement('br'); range.insertNode(br); var parent = br.parentNode; if (parent.lastChild === br) { br.parentNode.insertBefore(br.cloneNode(true), br); range.setStartBefore(br); } else { range.setStartAfter(br); } range.setCursor(); } } } }); }; // plugins/keystrokes.js /* 处理特殊键的兼容性问题 */ UE.plugins['keystrokes'] = function() { var me = this; var collapsed = true; me.addListener('keydown', function(type, evt) { var keyCode = evt.keyCode || evt.which, rng = me.selection.getRange(); //处理全选的情况 if(!rng.collapsed && !(evt.ctrlKey || evt.shiftKey || evt.altKey || evt.metaKey) && (keyCode >= 65 && keyCode <=90 || keyCode >= 48 && keyCode <= 57 || keyCode >= 96 && keyCode <= 111 || { 13:1, 8:1, 46:1 }[keyCode]) ){ var tmpNode = rng.startContainer; if(domUtils.isFillChar(tmpNode)){ rng.setStartBefore(tmpNode) } tmpNode = rng.endContainer; if(domUtils.isFillChar(tmpNode)){ rng.setEndAfter(tmpNode) } rng.txtToElmBoundary(); //结束边界可能放到了br的前边,要把br包含进来 // x[xxx]
    if(rng.endContainer && rng.endContainer.nodeType == 1){ tmpNode = rng.endContainer.childNodes[rng.endOffset]; if(tmpNode && domUtils.isBr(tmpNode)){ rng.setEndAfter(tmpNode); } } if(rng.startOffset == 0){ tmpNode = rng.startContainer; if(domUtils.isBoundaryNode(tmpNode,'firstChild') ){ tmpNode = rng.endContainer; if(rng.endOffset == (tmpNode.nodeType == 3 ? tmpNode.nodeValue.length : tmpNode.childNodes.length) && domUtils.isBoundaryNode(tmpNode,'lastChild')){ me.fireEvent('saveScene'); me.body.innerHTML = '

    '+(browser.ie ? '' : '
    ')+'

    '; rng.setStart(me.body.firstChild,0).setCursor(false,true); me._selectionChange(); return; } } } } //处理backspace if (keyCode == keymap.Backspace) { rng = me.selection.getRange(); collapsed = rng.collapsed; if(me.fireEvent('delkeydown',evt)){ return; } var start,end; //避免按两次删除才能生效的问题 if(rng.collapsed && rng.inFillChar()){ start = rng.startContainer; if(domUtils.isFillChar(start)){ rng.setStartBefore(start).shrinkBoundary(true).collapse(true); domUtils.remove(start) }else{ start.nodeValue = start.nodeValue.replace(new RegExp('^' + domUtils.fillChar ),''); rng.startOffset--; rng.collapse(true).select(true) } } //解决选中control元素不能删除的问题 if (start = rng.getClosedNode()) { me.fireEvent('saveScene'); rng.setStartBefore(start); domUtils.remove(start); rng.setCursor(); me.fireEvent('saveScene'); domUtils.preventDefault(evt); return; } //阻止在table上的删除 if (!browser.ie) { start = domUtils.findParentByTagName(rng.startContainer, 'table', true); end = domUtils.findParentByTagName(rng.endContainer, 'table', true); if (start && !end || !start && end || start !== end) { evt.preventDefault(); return; } } } //处理tab键的逻辑 if (keyCode == keymap.Tab) { //不处理以下标签 var excludeTagNameForTabKey = { 'ol' : 1, 'ul' : 1, 'table':1 }; //处理组件里的tab按下事件 if(me.fireEvent('tabkeydown',evt)){ domUtils.preventDefault(evt); return; } var range = me.selection.getRange(); me.fireEvent('saveScene'); for (var i = 0,txt = '',tabSize = me.options.tabSize|| 4,tabNode = me.options.tabNode || ' '; i < tabSize; i++) { txt += tabNode; } var span = me.document.createElement('span'); span.innerHTML = txt + domUtils.fillChar; if (range.collapsed) { range.insertNode(span.cloneNode(true).firstChild).setCursor(true); } else { var filterFn = function(node) { return domUtils.isBlockElm(node) && !excludeTagNameForTabKey[node.tagName.toLowerCase()] }; //普通的情况 start = domUtils.findParent(range.startContainer, filterFn,true); end = domUtils.findParent(range.endContainer, filterFn,true); if (start && end && start === end) { range.deleteContents(); range.insertNode(span.cloneNode(true).firstChild).setCursor(true); } else { var bookmark = range.createBookmark(); range.enlarge(true); var bookmark2 = range.createBookmark(), current = domUtils.getNextDomNode(bookmark2.start, false, filterFn); while (current && !(domUtils.getPosition(current, bookmark2.end) & domUtils.POSITION_FOLLOWING)) { current.insertBefore(span.cloneNode(true).firstChild, current.firstChild); current = domUtils.getNextDomNode(current, false, filterFn); } range.moveToBookmark(bookmark2).moveToBookmark(bookmark).select(); } } domUtils.preventDefault(evt) } //trace:1634 //ff的del键在容器空的时候,也会删除 if(browser.gecko && keyCode == 46){ range = me.selection.getRange(); if(range.collapsed){ start = range.startContainer; if(domUtils.isEmptyBlock(start)){ var parent = start.parentNode; while(domUtils.getChildCount(parent) == 1 && !domUtils.isBody(parent)){ start = parent; parent = parent.parentNode; } if(start === parent.lastChild) evt.preventDefault(); return; } } } }); me.addListener('keyup', function(type, evt) { var keyCode = evt.keyCode || evt.which, rng,me = this; if(keyCode == keymap.Backspace){ if(me.fireEvent('delkeyup')){ return; } rng = me.selection.getRange(); if(rng.collapsed){ var tmpNode, autoClearTagName = ['h1','h2','h3','h4','h5','h6']; if(tmpNode = domUtils.findParentByTagName(rng.startContainer,autoClearTagName,true)){ if(domUtils.isEmptyBlock(tmpNode)){ var pre = tmpNode.previousSibling; if(pre && pre.nodeName != 'TABLE'){ domUtils.remove(tmpNode); rng.setStartAtLast(pre).setCursor(false,true); return; }else{ var next = tmpNode.nextSibling; if(next && next.nodeName != 'TABLE'){ domUtils.remove(tmpNode); rng.setStartAtFirst(next).setCursor(false,true); return; } } } } //处理当删除到body时,要重新给p标签展位 if(domUtils.isBody(rng.startContainer)){ var tmpNode = domUtils.createElement(me.document,'p',{ 'innerHTML' : browser.ie ? domUtils.fillChar : '
    ' }); rng.insertNode(tmpNode).setStart(tmpNode,0).setCursor(false,true); } } //chrome下如果删除了inline标签,浏览器会有记忆,在输入文字还是会套上刚才删除的标签,所以这里再选一次就不会了 if( !collapsed && (rng.startContainer.nodeType == 3 || rng.startContainer.nodeType == 1 && domUtils.isEmptyBlock(rng.startContainer))){ if(browser.ie){ var span = rng.document.createElement('span'); rng.insertNode(span).setStartBefore(span).collapse(true); rng.select(); domUtils.remove(span) }else{ rng.select() } } } }) }; // plugins/fiximgclick.js ///import core ///commands 修复chrome下图片不能点击的问题,出现八个角可改变大小 ///commandsName FixImgClick ///commandsTitle 修复chrome下图片不能点击的问题,出现八个角可改变大小 //修复chrome下图片不能点击的问题,出现八个角可改变大小 UE.plugins['fiximgclick'] = (function () { var elementUpdated = false; function Scale() { this.editor = null; this.resizer = null; this.cover = null; this.doc = document; this.prePos = {x: 0, y: 0}; this.startPos = {x: 0, y: 0}; } (function () { var rect = [ //[left, top, width, height] [0, 0, -1, -1], [0, 0, 0, -1], [0, 0, 1, -1], [0, 0, -1, 0], [0, 0, 1, 0], [0, 0, -1, 1], [0, 0, 0, 1], [0, 0, 1, 1] ]; Scale.prototype = { init: function (editor) { var me = this; me.editor = editor; me.startPos = this.prePos = {x: 0, y: 0}; me.dragId = -1; var hands = [], cover = me.cover = document.createElement('div'), resizer = me.resizer = document.createElement('div'); cover.id = me.editor.ui.id + '_imagescale_cover'; cover.style.cssText = 'position:absolute;display:none;z-index:' + (me.editor.options.zIndex) + ';filter:alpha(opacity=0); opacity:0;background:#CCC;'; domUtils.on(cover, 'mousedown click', function () { me.hide(); }); for (i = 0; i < 8; i++) { hands.push(''); } resizer.id = me.editor.ui.id + '_imagescale'; resizer.className = 'edui-editor-imagescale'; resizer.innerHTML = hands.join(''); resizer.style.cssText += ';display:none;border:1px solid #3b77ff;z-index:' + (me.editor.options.zIndex) + ';'; me.editor.ui.getDom().appendChild(cover); me.editor.ui.getDom().appendChild(resizer); me.initStyle(); me.initEvents(); }, initStyle: function () { utils.cssRule('imagescale', '.edui-editor-imagescale{display:none;position:absolute;border:1px solid #38B2CE;cursor:hand;-webkit-box-sizing: content-box;-moz-box-sizing: content-box;box-sizing: content-box;}' + '.edui-editor-imagescale span{position:absolute;width:6px;height:6px;overflow:hidden;font-size:0px;display:block;background-color:#3C9DD0;}' + '.edui-editor-imagescale .edui-editor-imagescale-hand0{cursor:nw-resize;top:0;margin-top:-4px;left:0;margin-left:-4px;}' + '.edui-editor-imagescale .edui-editor-imagescale-hand1{cursor:n-resize;top:0;margin-top:-4px;left:50%;margin-left:-4px;}' + '.edui-editor-imagescale .edui-editor-imagescale-hand2{cursor:ne-resize;top:0;margin-top:-4px;left:100%;margin-left:-3px;}' + '.edui-editor-imagescale .edui-editor-imagescale-hand3{cursor:w-resize;top:50%;margin-top:-4px;left:0;margin-left:-4px;}' + '.edui-editor-imagescale .edui-editor-imagescale-hand4{cursor:e-resize;top:50%;margin-top:-4px;left:100%;margin-left:-3px;}' + '.edui-editor-imagescale .edui-editor-imagescale-hand5{cursor:sw-resize;top:100%;margin-top:-3px;left:0;margin-left:-4px;}' + '.edui-editor-imagescale .edui-editor-imagescale-hand6{cursor:s-resize;top:100%;margin-top:-3px;left:50%;margin-left:-4px;}' + '.edui-editor-imagescale .edui-editor-imagescale-hand7{cursor:se-resize;top:100%;margin-top:-3px;left:100%;margin-left:-3px;}'); }, initEvents: function () { var me = this; me.startPos.x = me.startPos.y = 0; me.isDraging = false; }, _eventHandler: function (e) { var me = this; switch (e.type) { case 'mousedown': var hand = e.target || e.srcElement, hand; if (hand.className.indexOf('edui-editor-imagescale-hand') != -1 && me.dragId == -1) { me.dragId = hand.className.slice(-1); me.startPos.x = me.prePos.x = e.clientX; me.startPos.y = me.prePos.y = e.clientY; domUtils.on(me.doc,'mousemove', me.proxy(me._eventHandler, me)); } break; case 'mousemove': if (me.dragId != -1) { me.updateContainerStyle(me.dragId, {x: e.clientX - me.prePos.x, y: e.clientY - me.prePos.y}); me.prePos.x = e.clientX; me.prePos.y = e.clientY; elementUpdated = true; me.updateTargetElement(); } break; case 'mouseup': if (me.dragId != -1) { me.updateContainerStyle(me.dragId, {x: e.clientX - me.prePos.x, y: e.clientY - me.prePos.y}); me.updateTargetElement(); if (me.target.parentNode) me.attachTo(me.target); me.dragId = -1; } domUtils.un(me.doc,'mousemove', me.proxy(me._eventHandler, me)); //修复只是点击挪动点,但没有改变大小,不应该触发contentchange if(elementUpdated){ elementUpdated = false; me.editor.fireEvent('contentchange'); } break; default: break; } }, updateTargetElement: function () { var me = this; domUtils.setStyles(me.target, { 'width': me.resizer.style.width, 'height': me.resizer.style.height }); me.target.width = parseInt(me.resizer.style.width); me.target.height = parseInt(me.resizer.style.height); me.attachTo(me.target); }, updateContainerStyle: function (dir, offset) { var me = this, dom = me.resizer, tmp; if (rect[dir][0] != 0) { tmp = parseInt(dom.style.left) + offset.x; dom.style.left = me._validScaledProp('left', tmp) + 'px'; } if (rect[dir][1] != 0) { tmp = parseInt(dom.style.top) + offset.y; dom.style.top = me._validScaledProp('top', tmp) + 'px'; } if (rect[dir][2] != 0) { tmp = dom.clientWidth + rect[dir][2] * offset.x; dom.style.width = me._validScaledProp('width', tmp) + 'px'; } if (rect[dir][3] != 0) { tmp = dom.clientHeight + rect[dir][3] * offset.y; dom.style.height = me._validScaledProp('height', tmp) + 'px'; } }, _validScaledProp: function (prop, value) { var ele = this.resizer, wrap = document; value = isNaN(value) ? 0 : value; switch (prop) { case 'left': return value < 0 ? 0 : (value + ele.clientWidth) > wrap.clientWidth ? wrap.clientWidth - ele.clientWidth : value; case 'top': return value < 0 ? 0 : (value + ele.clientHeight) > wrap.clientHeight ? wrap.clientHeight - ele.clientHeight : value; case 'width': return value <= 0 ? 1 : (value + ele.offsetLeft) > wrap.clientWidth ? wrap.clientWidth - ele.offsetLeft : value; case 'height': return value <= 0 ? 1 : (value + ele.offsetTop) > wrap.clientHeight ? wrap.clientHeight - ele.offsetTop : value; } }, hideCover: function () { this.cover.style.display = 'none'; }, showCover: function () { var me = this, editorPos = domUtils.getXY(me.editor.ui.getDom()), iframePos = domUtils.getXY(me.editor.iframe); domUtils.setStyles(me.cover, { 'width': me.editor.iframe.offsetWidth + 'px', 'height': me.editor.iframe.offsetHeight + 'px', 'top': iframePos.y - editorPos.y + 'px', 'left': iframePos.x - editorPos.x + 'px', 'position': 'absolute', 'display': '' }) }, show: function (targetObj) { var me = this; me.resizer.style.display = 'block'; if(targetObj) me.attachTo(targetObj); domUtils.on(this.resizer, 'mousedown', me.proxy(me._eventHandler, me)); domUtils.on(me.doc, 'mouseup', me.proxy(me._eventHandler, me)); me.showCover(); me.editor.fireEvent('afterscaleshow', me); me.editor.fireEvent('saveScene'); }, hide: function () { var me = this; me.hideCover(); me.resizer.style.display = 'none'; domUtils.un(me.resizer, 'mousedown', me.proxy(me._eventHandler, me)); domUtils.un(me.doc, 'mouseup', me.proxy(me._eventHandler, me)); me.editor.fireEvent('afterscalehide', me); }, proxy: function( fn, context ) { return function(e) { return fn.apply( context || this, arguments); }; }, attachTo: function (targetObj) { var me = this, target = me.target = targetObj, resizer = this.resizer, imgPos = domUtils.getXY(target), iframePos = domUtils.getXY(me.editor.iframe), editorPos = domUtils.getXY(resizer.parentNode); domUtils.setStyles(resizer, { 'width': target.width + 'px', 'height': target.height + 'px', 'left': iframePos.x + imgPos.x - me.editor.document.body.scrollLeft - editorPos.x - parseInt(resizer.style.borderLeftWidth) + 'px', 'top': iframePos.y + imgPos.y - me.editor.document.body.scrollTop - editorPos.y - parseInt(resizer.style.borderTopWidth) + 'px' }); } } })(); return function () { var me = this, imageScale; me.setOpt('imageScaleEnabled', true); if ( !browser.ie && me.options.imageScaleEnabled) { me.addListener('click', function (type, e) { var range = me.selection.getRange(), img = range.getClosedNode(); if (img && img.tagName == 'IMG' && me.body.contentEditable!="false") { if (img.className.indexOf("edui-faked-music") != -1 || img.getAttribute("anchorname") || domUtils.hasClass(img, 'loadingclass') || domUtils.hasClass(img, 'loaderrorclass')) { return } if (!imageScale) { imageScale = new Scale(); imageScale.init(me); me.ui.getDom().appendChild(imageScale.resizer); var _keyDownHandler = function (e) { imageScale.hide(); if(imageScale.target) me.selection.getRange().selectNode(imageScale.target).select(); }, _mouseDownHandler = function (e) { var ele = e.target || e.srcElement; if (ele && (ele.className===undefined || ele.className.indexOf('edui-editor-imagescale') == -1)) { _keyDownHandler(e); } }, timer; me.addListener('afterscaleshow', function (e) { me.addListener('beforekeydown', _keyDownHandler); me.addListener('beforemousedown', _mouseDownHandler); domUtils.on(document, 'keydown', _keyDownHandler); domUtils.on(document,'mousedown', _mouseDownHandler); me.selection.getNative().removeAllRanges(); }); me.addListener('afterscalehide', function (e) { me.removeListener('beforekeydown', _keyDownHandler); me.removeListener('beforemousedown', _mouseDownHandler); domUtils.un(document, 'keydown', _keyDownHandler); domUtils.un(document,'mousedown', _mouseDownHandler); var target = imageScale.target; if (target.parentNode) { me.selection.getRange().selectNode(target).select(); } }); //TODO 有iframe的情况,mousedown不能往下传。。 domUtils.on(imageScale.resizer, 'mousedown', function (e) { me.selection.getNative().removeAllRanges(); var ele = e.target || e.srcElement; if (ele && ele.className.indexOf('edui-editor-imagescale-hand') == -1) { timer = setTimeout(function () { imageScale.hide(); if(imageScale.target) me.selection.getRange().selectNode(ele).select(); }, 200); } }); domUtils.on(imageScale.resizer, 'mouseup', function (e) { var ele = e.target || e.srcElement; if (ele && ele.className.indexOf('edui-editor-imagescale-hand') == -1) { clearTimeout(timer); } }); } imageScale.show(img); } else { if (imageScale && imageScale.resizer.style.display != 'none') imageScale.hide(); } }); } if (browser.webkit) { me.addListener('click', function (type, e) { if (e.target.tagName == 'IMG' && me.body.contentEditable!="false") { var range = new dom.Range(me.document); range.selectNode(e.target).select(); } }); } } })(); // plugins/autolink.js ///import core ///commands 为非ie浏览器自动添加a标签 ///commandsName AutoLink ///commandsTitle 自动增加链接 /** * @description 为非ie浏览器自动添加a标签 * @author zhanyi */ UE.plugin.register('autolink',function(){ var cont = 0; return !browser.ie ? { bindEvents:{ 'reset' : function(){ cont = 0; }, 'keydown':function(type, evt) { var me = this; var keyCode = evt.keyCode || evt.which; if (keyCode == 32 || keyCode == 13) { var sel = me.selection.getNative(), range = sel.getRangeAt(0).cloneRange(), offset, charCode; var start = range.startContainer; while (start.nodeType == 1 && range.startOffset > 0) { start = range.startContainer.childNodes[range.startOffset - 1]; if (!start){ break; } range.setStart(start, start.nodeType == 1 ? start.childNodes.length : start.nodeValue.length); range.collapse(true); start = range.startContainer; } do{ if (range.startOffset == 0) { start = range.startContainer.previousSibling; while (start && start.nodeType == 1) { start = start.lastChild; } if (!start || domUtils.isFillChar(start)){ break; } offset = start.nodeValue.length; } else { start = range.startContainer; offset = range.startOffset; } range.setStart(start, offset - 1); charCode = range.toString().charCodeAt(0); } while (charCode != 160 && charCode != 32); if (range.toString().replace(new RegExp(domUtils.fillChar, 'g'), '').match(/(?:https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.)/i)) { while(range.toString().length){ if(/^(?:https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.)/i.test(range.toString())){ break; } try{ range.setStart(range.startContainer,range.startOffset+1); }catch(e){ //trace:2121 var start = range.startContainer; while(!(next = start.nextSibling)){ if(domUtils.isBody(start)){ return; } start = start.parentNode; } range.setStart(next,0); } } //range的开始边界已经在a标签里的不再处理 if(domUtils.findParentByTagName(range.startContainer,'a',true)){ return; } var a = me.document.createElement('a'),text = me.document.createTextNode(' '),href; me.undoManger && me.undoManger.save(); a.appendChild(range.extractContents()); a.href = a.innerHTML = a.innerHTML.replace(/<[^>]+>/g,''); href = a.getAttribute("href").replace(new RegExp(domUtils.fillChar,'g'),''); href = /^(?:https?:\/\/)/ig.test(href) ? href : "http://"+ href; a.setAttribute('_src',utils.html(href)); a.href = utils.html(href); range.insertNode(a); a.parentNode.insertBefore(text, a.nextSibling); range.setStart(text, 0); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); me.undoManger && me.undoManger.save(); } } } } }:{} },function(){ var keyCodes = { 37:1, 38:1, 39:1, 40:1, 13:1,32:1 }; function checkIsCludeLink(node){ if(node.nodeType == 3){ return null } if(node.nodeName == 'A'){ return node; } var lastChild = node.lastChild; while(lastChild){ if(lastChild.nodeName == 'A'){ return lastChild; } if(lastChild.nodeType == 3){ if(domUtils.isWhitespace(lastChild)){ lastChild = lastChild.previousSibling; continue; } return null } lastChild = lastChild.lastChild; } } browser.ie && this.addListener('keyup',function(cmd,evt){ var me = this,keyCode = evt.keyCode; if(keyCodes[keyCode]){ var rng = me.selection.getRange(); var start = rng.startContainer; if(keyCode == 13){ while(start && !domUtils.isBody(start) && !domUtils.isBlockElm(start)){ start = start.parentNode; } if(start && !domUtils.isBody(start) && start.nodeName == 'P'){ var pre = start.previousSibling; if(pre && pre.nodeType == 1){ var pre = checkIsCludeLink(pre); if(pre && !pre.getAttribute('_href')){ domUtils.remove(pre,true); } } } }else if(keyCode == 32 ){ if(start.nodeType == 3 && /^\s$/.test(start.nodeValue)){ start = start.previousSibling; if(start && start.nodeName == 'A' && !start.getAttribute('_href')){ domUtils.remove(start,true); } } }else { start = domUtils.findParentByTagName(start,'a',true); if(start && !start.getAttribute('_href')){ var bk = rng.createBookmark(); domUtils.remove(start,true); rng.moveToBookmark(bk).select(true) } } } }); } ); // plugins/autoheight.js ///import core ///commands 当输入内容超过编辑器高度时,编辑器自动增高 ///commandsName AutoHeight,autoHeightEnabled ///commandsTitle 自动增高 /** * @description 自动伸展 * @author zhanyi */ UE.plugins['autoheight'] = function () { var me = this; //提供开关,就算加载也可以关闭 me.autoHeightEnabled = me.options.autoHeightEnabled !== false; if (!me.autoHeightEnabled) { return; } var bakOverflow, lastHeight = 0, options = me.options, currentHeight, timer; function adjustHeight() { var me = this; clearTimeout(timer); if(isFullscreen)return; if (!me.queryCommandState || me.queryCommandState && me.queryCommandState('source') != 1) { timer = setTimeout(function(){ var node = me.body.lastChild; while(node && node.nodeType != 1){ node = node.previousSibling; } if(node && node.nodeType == 1){ node.style.clear = 'both'; currentHeight = Math.max(domUtils.getXY(node).y + node.offsetHeight + 25 ,Math.max(options.minFrameHeight, options.initialFrameHeight)) ; if (currentHeight != lastHeight) { if (currentHeight !== parseInt(me.iframe.parentNode.style.height)) { me.iframe.parentNode.style.height = currentHeight + 'px'; } me.body.style.height = currentHeight + 'px'; lastHeight = currentHeight; } domUtils.removeStyle(node,'clear'); } },50) } } var isFullscreen; me.addListener('fullscreenchanged',function(cmd,f){ isFullscreen = f }); me.addListener('destroy', function () { me.removeListener('contentchange afterinserthtml keyup mouseup',adjustHeight) }); me.enableAutoHeight = function () { var me = this; if (!me.autoHeightEnabled) { return; } var doc = me.document; me.autoHeightEnabled = true; bakOverflow = doc.body.style.overflowY; doc.body.style.overflowY = 'hidden'; me.addListener('contentchange afterinserthtml keyup mouseup',adjustHeight); //ff不给事件算得不对 setTimeout(function () { adjustHeight.call(me); }, browser.gecko ? 100 : 0); me.fireEvent('autoheightchanged', me.autoHeightEnabled); }; me.disableAutoHeight = function () { me.body.style.overflowY = bakOverflow || ''; me.removeListener('contentchange', adjustHeight); me.removeListener('keyup', adjustHeight); me.removeListener('mouseup', adjustHeight); me.autoHeightEnabled = false; me.fireEvent('autoheightchanged', me.autoHeightEnabled); }; me.on('setHeight',function(){ me.disableAutoHeight() }); me.addListener('ready', function () { me.enableAutoHeight(); //trace:1764 var timer; domUtils.on(browser.ie ? me.body : me.document, browser.webkit ? 'dragover' : 'drop', function () { clearTimeout(timer); timer = setTimeout(function () { //trace:3681 adjustHeight.call(me); }, 100); }); //修复内容过多时,回到顶部,顶部内容被工具栏遮挡问题 var lastScrollY; window.onscroll = function(){ if(lastScrollY === null){ lastScrollY = this.scrollY }else if(this.scrollY == 0 && lastScrollY != 0){ me.window.scrollTo(0,0); lastScrollY = null; } } }); }; // plugins/autofloat.js ///import core ///commands 悬浮工具栏 ///commandsName AutoFloat,autoFloatEnabled ///commandsTitle 悬浮工具栏 /** * modified by chengchao01 * 注意: 引入此功能后,在IE6下会将body的背景图片覆盖掉! */ UE.plugins['autofloat'] = function() { var me = this, lang = me.getLang(); me.setOpt({ topOffset:0 }); var optsAutoFloatEnabled = me.options.autoFloatEnabled !== false, topOffset = me.options.topOffset; //如果不固定toolbar的位置,则直接退出 if(!optsAutoFloatEnabled){ return; } var uiUtils = UE.ui.uiUtils, LteIE6 = browser.ie && browser.version <= 6, quirks = browser.quirks; function checkHasUI(){ if(!UE.ui){ alert(lang.autofloatMsg); return 0; } return 1; } function fixIE6FixedPos(){ var docStyle = document.body.style; docStyle.backgroundImage = 'url("about:blank")'; docStyle.backgroundAttachment = 'fixed'; } var bakCssText, placeHolder = document.createElement('div'), toolbarBox,orgTop, getPosition, flag =true; //ie7模式下需要偏移 function setFloating(){ var toobarBoxPos = domUtils.getXY(toolbarBox), origalFloat = domUtils.getComputedStyle(toolbarBox,'position'), origalLeft = domUtils.getComputedStyle(toolbarBox,'left'); toolbarBox.style.width = toolbarBox.offsetWidth + 'px'; toolbarBox.style.zIndex = me.options.zIndex * 1 + 1; toolbarBox.parentNode.insertBefore(placeHolder, toolbarBox); if (LteIE6 || (quirks && browser.ie)) { if(toolbarBox.style.position != 'absolute'){ toolbarBox.style.position = 'absolute'; } toolbarBox.style.top = (document.body.scrollTop||document.documentElement.scrollTop) - orgTop + topOffset + 'px'; } else { if (browser.ie7Compat && flag) { flag = false; toolbarBox.style.left = domUtils.getXY(toolbarBox).x - document.documentElement.getBoundingClientRect().left+2 + 'px'; } if(toolbarBox.style.position != 'fixed'){ toolbarBox.style.position = 'fixed'; toolbarBox.style.top = topOffset +"px"; ((origalFloat == 'absolute' || origalFloat == 'relative') && parseFloat(origalLeft)) && (toolbarBox.style.left = toobarBoxPos.x + 'px'); } } } function unsetFloating(){ flag = true; if(placeHolder.parentNode){ placeHolder.parentNode.removeChild(placeHolder); } toolbarBox.style.cssText = bakCssText; } function updateFloating(){ var rect3 = getPosition(me.container); var offset=me.options.toolbarTopOffset||0; if (rect3.top < 0 && rect3.bottom - toolbarBox.offsetHeight > offset) { setFloating(); }else{ unsetFloating(); } } var defer_updateFloating = utils.defer(function(){ updateFloating(); },browser.ie ? 200 : 100,true); me.addListener('destroy',function(){ domUtils.un(window, ['scroll','resize'], updateFloating); me.removeListener('keydown', defer_updateFloating); }); me.addListener('ready', function(){ if(checkHasUI(me)){ //加载了ui组件,但在new时,没有加载ui,导致编辑器实例上没有ui类,所以这里做判断 if(!me.ui){ return; } getPosition = uiUtils.getClientRect; toolbarBox = me.ui.getDom('toolbarbox'); orgTop = getPosition(toolbarBox).top; bakCssText = toolbarBox.style.cssText; placeHolder.style.height = toolbarBox.offsetHeight + 'px'; if(LteIE6){ fixIE6FixedPos(); } domUtils.on(window, ['scroll','resize'], updateFloating); me.addListener('keydown', defer_updateFloating); me.addListener('beforefullscreenchange', function (t, enabled){ if (enabled) { unsetFloating(); } }); me.addListener('fullscreenchanged', function (t, enabled){ if (!enabled) { updateFloating(); } }); me.addListener('sourcemodechanged', function (t, enabled){ setTimeout(function (){ updateFloating(); },0); }); me.addListener("clearDoc",function(){ setTimeout(function(){ updateFloating(); },0); }) } }); }; // plugins/video.js /** * video插件, 为UEditor提供视频插入支持 * @file * @since 1.2.6.1 */ UE.plugins['video'] = function (){ var me =this; /** * 创建插入视频字符窜 * @param url 视频地址 * @param width 视频宽度 * @param height 视频高度 * @param align 视频对齐 * @param toEmbed 是否以flash代替显示 * @param addParagraph 是否需要添加P 标签 */ function creatInsertStr(url,width,height,id,align,classname,type){ url = utils.unhtmlForUrl(url); align = utils.unhtml(align); classname = utils.unhtml(classname).trim(); width = parseInt(width, 10) || 0; height = parseInt(height, 10) || 0; var str; switch (type){ case 'image': str = '' break; case 'embed': str = ''; break; case 'video': var ext = url.substr(url.lastIndexOf('.') + 1); if(ext == 'ogv') ext = 'ogg'; str = '' + ''; break; } return str; } function switchImgAndVideo(root,img2video){ utils.each(root.getNodesByTagName(img2video ? 'img' : 'embed video'),function(node){ var className = node.getAttr('class'); if(className && className.indexOf('edui-faked-video') != -1){ var html = creatInsertStr( img2video ? node.getAttr('_url') : node.getAttr('src'),node.getAttr('width'),node.getAttr('height'),null,node.getStyle('float') || '',className,img2video ? 'embed':'image'); node.parentNode.replaceChild(UE.uNode.createElement(html),node); } if(className && className.indexOf('edui-upload-video') != -1){ var html = creatInsertStr( img2video ? node.getAttr('_url') : node.getAttr('src'),node.getAttr('width'),node.getAttr('height'),null,node.getStyle('float') || '',className,img2video ? 'video':'image'); node.parentNode.replaceChild(UE.uNode.createElement(html),node); } }) } me.addOutputRule(function(root){ switchImgAndVideo(root,true) }); me.addInputRule(function(root){ switchImgAndVideo(root) }); /** * 插入视频 * @command insertvideo * @method execCommand * @param { String } cmd 命令字符串 * @param { Object } videoAttr 键值对对象, 描述一个视频的所有属性 * @example * ```javascript * * var videoAttr = { * //视频地址 * url: 'http://www.youku.com/xxx', * //视频宽高值, 单位px * width: 200, * height: 100 * }; * * //editor 是编辑器实例 * //向编辑器插入单个视频 * editor.execCommand( 'insertvideo', videoAttr ); * ``` */ /** * 插入视频 * @command insertvideo * @method execCommand * @param { String } cmd 命令字符串 * @param { Array } videoArr 需要插入的视频的数组, 其中的每一个元素都是一个键值对对象, 描述了一个视频的所有属性 * @example * ```javascript * * var videoAttr1 = { * //视频地址 * url: 'http://www.youku.com/xxx', * //视频宽高值, 单位px * width: 200, * height: 100 * }, * videoAttr2 = { * //视频地址 * url: 'http://www.youku.com/xxx', * //视频宽高值, 单位px * width: 200, * height: 100 * } * * //editor 是编辑器实例 * //该方法将会向编辑器内插入两个视频 * editor.execCommand( 'insertvideo', [ videoAttr1, videoAttr2 ] ); * ``` */ /** * 查询当前光标所在处是否是一个视频 * @command insertvideo * @method queryCommandState * @param { String } cmd 需要查询的命令字符串 * @return { int } 如果当前光标所在处的元素是一个视频对象, 则返回1,否则返回0 * @example * ```javascript * * //editor 是编辑器实例 * editor.queryCommandState( 'insertvideo' ); * ``` */ me.commands["insertvideo"] = { execCommand: function (cmd, videoObjs, type){ videoObjs = utils.isArray(videoObjs)?videoObjs:[videoObjs]; var html = [],id = 'tmpVedio', cl; for(var i=0,vi,len = videoObjs.length;i 0) { return 0; } for (var i in dtd.$isNotEmpty) if (dtd.$isNotEmpty.hasOwnProperty(i)) { if (node.getElementsByTagName(i).length) { return 0; } } return 1; }; UETable.getWidth = function (cell) { if (!cell)return 0; return parseInt(domUtils.getComputedStyle(cell, "width"), 10); }; /** * 获取单元格或者单元格组的“对齐”状态。 如果当前的检测对象是一个单元格组, 只有在满足所有单元格的 水平和竖直 对齐属性都相同的 * 条件时才会返回其状态值,否则将返回null; 如果当前只检测了一个单元格, 则直接返回当前单元格的对齐状态; * @param table cell or table cells , 支持单个单元格dom对象 或者 单元格dom对象数组 * @return { align: 'left' || 'right' || 'center', valign: 'top' || 'middle' || 'bottom' } 或者 null */ UETable.getTableCellAlignState = function ( cells ) { !utils.isArray( cells ) && ( cells = [cells] ); var result = {}, status = ['align', 'valign'], tempStatus = null, isSame = true;//状态是否相同 utils.each( cells, function( cellNode ){ utils.each( status, function( currentState ){ tempStatus = cellNode.getAttribute( currentState ); if( !result[ currentState ] && tempStatus ) { result[ currentState ] = tempStatus; } else if( !result[ currentState ] || ( tempStatus !== result[ currentState ] ) ) { isSame = false; return false; } } ); return isSame; }); return isSame ? result : null; }; /** * 根据当前选区获取相关的table信息 * @return {Object} */ UETable.getTableItemsByRange = function (editor) { var start = editor.selection.getStart(); //ff下会选中bookmark if( start && start.id && start.id.indexOf('_baidu_bookmark_start_') === 0 && start.nextSibling) { start = start.nextSibling; } //在table或者td边缘有可能存在选中tr的情况 var cell = start && domUtils.findParentByTagName(start, ["td", "th"], true), tr = cell && cell.parentNode, caption = start && domUtils.findParentByTagName(start, 'caption', true), table = caption ? caption.parentNode : tr && tr.parentNode.parentNode; return { cell:cell, tr:tr, table:table, caption:caption } }; UETable.getUETableBySelected = function (editor) { var table = UETable.getTableItemsByRange(editor).table; if (table && table.ueTable && table.ueTable.selectedTds.length) { return table.ueTable; } return null; }; UETable.getDefaultValue = function (editor, table) { var borderMap = { thin:'0px', medium:'1px', thick:'2px' }, tableBorder, tdPadding, tdBorder, tmpValue; if (!table) { table = editor.document.createElement('table'); table.insertRow(0).insertCell(0).innerHTML = 'xxx'; editor.body.appendChild(table); var td = table.getElementsByTagName('td')[0]; tmpValue = domUtils.getComputedStyle(table, 'border-left-width'); tableBorder = parseInt(borderMap[tmpValue] || tmpValue, 10); tmpValue = domUtils.getComputedStyle(td, 'padding-left'); tdPadding = parseInt(borderMap[tmpValue] || tmpValue, 10); tmpValue = domUtils.getComputedStyle(td, 'border-left-width'); tdBorder = parseInt(borderMap[tmpValue] || tmpValue, 10); domUtils.remove(table); return { tableBorder:tableBorder, tdPadding:tdPadding, tdBorder:tdBorder }; } else { td = table.getElementsByTagName('td')[0]; tmpValue = domUtils.getComputedStyle(table, 'border-left-width'); tableBorder = parseInt(borderMap[tmpValue] || tmpValue, 10); tmpValue = domUtils.getComputedStyle(td, 'padding-left'); tdPadding = parseInt(borderMap[tmpValue] || tmpValue, 10); tmpValue = domUtils.getComputedStyle(td, 'border-left-width'); tdBorder = parseInt(borderMap[tmpValue] || tmpValue, 10); return { tableBorder:tableBorder, tdPadding:tdPadding, tdBorder:tdBorder }; } }; /** * 根据当前点击的td或者table获取索引对象 * @param tdOrTable */ UETable.getUETable = function (tdOrTable) { var tag = tdOrTable.tagName.toLowerCase(); tdOrTable = (tag == "td" || tag == "th" || tag == 'caption') ? domUtils.findParentByTagName(tdOrTable, "table", true) : tdOrTable; if (!tdOrTable.ueTable) { tdOrTable.ueTable = new UETable(tdOrTable); } return tdOrTable.ueTable; }; UETable.cloneCell = function(cell,ignoreMerge,keepPro){ if (!cell || utils.isString(cell)) { return this.table.ownerDocument.createElement(cell || 'td'); } var flag = domUtils.hasClass(cell, "selectTdClass"); flag && domUtils.removeClasses(cell, "selectTdClass"); var tmpCell = cell.cloneNode(true); if (ignoreMerge) { tmpCell.rowSpan = tmpCell.colSpan = 1; } //去掉宽高 !keepPro && domUtils.removeAttributes(tmpCell,'width height'); !keepPro && domUtils.removeAttributes(tmpCell,'style'); tmpCell.style.borderLeftStyle = ""; tmpCell.style.borderTopStyle = ""; tmpCell.style.borderLeftColor = cell.style.borderRightColor; tmpCell.style.borderLeftWidth = cell.style.borderRightWidth; tmpCell.style.borderTopColor = cell.style.borderBottomColor; tmpCell.style.borderTopWidth = cell.style.borderBottomWidth; flag && domUtils.addClass(cell, "selectTdClass"); return tmpCell; } UETable.prototype = { getMaxRows:function () { var rows = this.table.rows, maxLen = 1; for (var i = 0, row; row = rows[i]; i++) { var currentMax = 1; for (var j = 0, cj; cj = row.cells[j++];) { currentMax = Math.max(cj.rowSpan || 1, currentMax); } maxLen = Math.max(currentMax + i, maxLen); } return maxLen; }, /** * 获取当前表格的最大列数 */ getMaxCols:function () { var rows = this.table.rows, maxLen = 0, cellRows = {}; for (var i = 0, row; row = rows[i]; i++) { var cellsNum = 0; for (var j = 0, cj; cj = row.cells[j++];) { cellsNum += (cj.colSpan || 1); if (cj.rowSpan && cj.rowSpan > 1) { for (var k = 1; k < cj.rowSpan; k++) { if (!cellRows['row_' + (i + k)]) { cellRows['row_' + (i + k)] = (cj.colSpan || 1); } else { cellRows['row_' + (i + k)]++ } } } } cellsNum += cellRows['row_' + i] || 0; maxLen = Math.max(cellsNum, maxLen); } return maxLen; }, getCellColIndex:function (cell) { }, /** * 获取当前cell旁边的单元格, * @param cell * @param right */ getHSideCell:function (cell, right) { try { var cellInfo = this.getCellInfo(cell), previewRowIndex, previewColIndex; var len = this.selectedTds.length, range = this.cellsRange; //首行或者首列没有前置单元格 if ((!right && (!len ? !cellInfo.colIndex : !range.beginColIndex)) || (right && (!len ? (cellInfo.colIndex == (this.colsNum - 1)) : (range.endColIndex == this.colsNum - 1)))) return null; previewRowIndex = !len ? cellInfo.rowIndex : range.beginRowIndex; previewColIndex = !right ? ( !len ? (cellInfo.colIndex < 1 ? 0 : (cellInfo.colIndex - 1)) : range.beginColIndex - 1) : ( !len ? cellInfo.colIndex + 1 : range.endColIndex + 1); return this.getCell(this.indexTable[previewRowIndex][previewColIndex].rowIndex, this.indexTable[previewRowIndex][previewColIndex].cellIndex); } catch (e) { showError(e); } }, getTabNextCell:function (cell, preRowIndex) { var cellInfo = this.getCellInfo(cell), rowIndex = preRowIndex || cellInfo.rowIndex, colIndex = cellInfo.colIndex + 1 + (cellInfo.colSpan - 1), nextCell; try { nextCell = this.getCell(this.indexTable[rowIndex][colIndex].rowIndex, this.indexTable[rowIndex][colIndex].cellIndex); } catch (e) { try { rowIndex = rowIndex * 1 + 1; colIndex = 0; nextCell = this.getCell(this.indexTable[rowIndex][colIndex].rowIndex, this.indexTable[rowIndex][colIndex].cellIndex); } catch (e) { } } return nextCell; }, /** * 获取视觉上的后置单元格 * @param cell * @param bottom */ getVSideCell:function (cell, bottom, ignoreRange) { try { var cellInfo = this.getCellInfo(cell), nextRowIndex, nextColIndex; var len = this.selectedTds.length && !ignoreRange, range = this.cellsRange; //末行或者末列没有后置单元格 if ((!bottom && (cellInfo.rowIndex == 0)) || (bottom && (!len ? (cellInfo.rowIndex + cellInfo.rowSpan > this.rowsNum - 1) : (range.endRowIndex == this.rowsNum - 1)))) return null; nextRowIndex = !bottom ? ( !len ? cellInfo.rowIndex - 1 : range.beginRowIndex - 1) : ( !len ? (cellInfo.rowIndex + cellInfo.rowSpan) : range.endRowIndex + 1); nextColIndex = !len ? cellInfo.colIndex : range.beginColIndex; return this.getCell(this.indexTable[nextRowIndex][nextColIndex].rowIndex, this.indexTable[nextRowIndex][nextColIndex].cellIndex); } catch (e) { showError(e); } }, /** * 获取相同结束位置的单元格,xOrY指代了是获取x轴相同还是y轴相同 */ getSameEndPosCells:function (cell, xOrY) { try { var flag = (xOrY.toLowerCase() === "x"), end = domUtils.getXY(cell)[flag ? 'x' : 'y'] + cell["offset" + (flag ? 'Width' : 'Height')], rows = this.table.rows, cells = null, returns = []; for (var i = 0; i < this.rowsNum; i++) { cells = rows[i].cells; for (var j = 0, tmpCell; tmpCell = cells[j++];) { var tmpEnd = domUtils.getXY(tmpCell)[flag ? 'x' : 'y'] + tmpCell["offset" + (flag ? 'Width' : 'Height')]; //对应行的td已经被上面行rowSpan了 if (tmpEnd > end && flag) break; if (cell == tmpCell || end == tmpEnd) { //只获取单一的单元格 //todo 仅获取单一单元格在特定情况下会造成returns为空,从而影响后续的拖拽实现,修正这个。需考虑性能 if (tmpCell[flag ? "colSpan" : "rowSpan"] == 1) { returns.push(tmpCell); } if (flag) break; } } } return returns; } catch (e) { showError(e); } }, setCellContent:function (cell, content) { cell.innerHTML = content || (browser.ie ? domUtils.fillChar : "
    "); }, cloneCell:UETable.cloneCell, /** * 获取跟当前单元格的右边竖线为左边的所有未合并单元格 */ getSameStartPosXCells:function (cell) { try { var start = domUtils.getXY(cell).x + cell.offsetWidth, rows = this.table.rows, cells , returns = []; for (var i = 0; i < this.rowsNum; i++) { cells = rows[i].cells; for (var j = 0, tmpCell; tmpCell = cells[j++];) { var tmpStart = domUtils.getXY(tmpCell).x; if (tmpStart > start) break; if (tmpStart == start && tmpCell.colSpan == 1) { returns.push(tmpCell); break; } } } return returns; } catch (e) { showError(e); } }, /** * 更新table对应的索引表 */ update:function (table) { this.table = table || this.table; this.selectedTds = []; this.cellsRange = {}; this.indexTable = []; var rows = this.table.rows, rowsNum = this.getMaxRows(), dNum = rowsNum - rows.length, colsNum = this.getMaxCols(); while (dNum--) { this.table.insertRow(rows.length); } this.rowsNum = rowsNum; this.colsNum = colsNum; for (var i = 0, len = rows.length; i < len; i++) { this.indexTable[i] = new Array(colsNum); } //填充索引表 for (var rowIndex = 0, row; row = rows[rowIndex]; rowIndex++) { for (var cellIndex = 0, cell, cells = row.cells; cell = cells[cellIndex]; cellIndex++) { //修正整行被rowSpan时导致的行数计算错误 if (cell.rowSpan > rowsNum) { cell.rowSpan = rowsNum; } var colIndex = cellIndex, rowSpan = cell.rowSpan || 1, colSpan = cell.colSpan || 1; //当已经被上一行rowSpan或者被前一列colSpan了,则跳到下一个单元格进行 while (this.indexTable[rowIndex][colIndex]) colIndex++; for (var j = 0; j < rowSpan; j++) { for (var k = 0; k < colSpan; k++) { this.indexTable[rowIndex + j][colIndex + k] = { rowIndex:rowIndex, cellIndex:cellIndex, colIndex:colIndex, rowSpan:rowSpan, colSpan:colSpan } } } } } //修复残缺td for (j = 0; j < rowsNum; j++) { for (k = 0; k < colsNum; k++) { if (this.indexTable[j][k] === undefined) { row = rows[j]; cell = row.cells[row.cells.length - 1]; cell = cell ? cell.cloneNode(true) : this.table.ownerDocument.createElement("td"); this.setCellContent(cell); if (cell.colSpan !== 1)cell.colSpan = 1; if (cell.rowSpan !== 1)cell.rowSpan = 1; row.appendChild(cell); this.indexTable[j][k] = { rowIndex:j, cellIndex:cell.cellIndex, colIndex:k, rowSpan:1, colSpan:1 } } } } //当框选后删除行或者列后撤销,需要重建选区。 var tds = domUtils.getElementsByTagName(this.table, "td"), selectTds = []; utils.each(tds, function (td) { if (domUtils.hasClass(td, "selectTdClass")) { selectTds.push(td); } }); if (selectTds.length) { var start = selectTds[0], end = selectTds[selectTds.length - 1], startInfo = this.getCellInfo(start), endInfo = this.getCellInfo(end); this.selectedTds = selectTds; this.cellsRange = { beginRowIndex:startInfo.rowIndex, beginColIndex:startInfo.colIndex, endRowIndex:endInfo.rowIndex + endInfo.rowSpan - 1, endColIndex:endInfo.colIndex + endInfo.colSpan - 1 }; } //给第一行设置firstRow的样式名称,在排序图标的样式上使用到 if(!domUtils.hasClass(this.table.rows[0], "firstRow")) { domUtils.addClass(this.table.rows[0], "firstRow"); for(var i = 1; i< this.table.rows.length; i++) { domUtils.removeClasses(this.table.rows[i], "firstRow"); } } }, /** * 获取单元格的索引信息 */ getCellInfo:function (cell) { if (!cell) return; var cellIndex = cell.cellIndex, rowIndex = cell.parentNode.rowIndex, rowInfo = this.indexTable[rowIndex], numCols = this.colsNum; for (var colIndex = cellIndex; colIndex < numCols; colIndex++) { var cellInfo = rowInfo[colIndex]; if (cellInfo.rowIndex === rowIndex && cellInfo.cellIndex === cellIndex) { return cellInfo; } } }, /** * 根据行列号获取单元格 */ getCell:function (rowIndex, cellIndex) { return rowIndex < this.rowsNum && this.table.rows[rowIndex].cells[cellIndex] || null; }, /** * 删除单元格 */ deleteCell:function (cell, rowIndex) { rowIndex = typeof rowIndex == 'number' ? rowIndex : cell.parentNode.rowIndex; var row = this.table.rows[rowIndex]; row.deleteCell(cell.cellIndex); }, /** * 根据始末两个单元格获取被框选的所有单元格范围 */ getCellsRange:function (cellA, cellB) { function checkRange(beginRowIndex, beginColIndex, endRowIndex, endColIndex) { var tmpBeginRowIndex = beginRowIndex, tmpBeginColIndex = beginColIndex, tmpEndRowIndex = endRowIndex, tmpEndColIndex = endColIndex, cellInfo, colIndex, rowIndex; // 通过indexTable检查是否存在超出TableRange上边界的情况 if (beginRowIndex > 0) { for (colIndex = beginColIndex; colIndex < endColIndex; colIndex++) { cellInfo = me.indexTable[beginRowIndex][colIndex]; rowIndex = cellInfo.rowIndex; if (rowIndex < beginRowIndex) { tmpBeginRowIndex = Math.min(rowIndex, tmpBeginRowIndex); } } } // 通过indexTable检查是否存在超出TableRange右边界的情况 if (endColIndex < me.colsNum) { for (rowIndex = beginRowIndex; rowIndex < endRowIndex; rowIndex++) { cellInfo = me.indexTable[rowIndex][endColIndex]; colIndex = cellInfo.colIndex + cellInfo.colSpan - 1; if (colIndex > endColIndex) { tmpEndColIndex = Math.max(colIndex, tmpEndColIndex); } } } // 检查是否有超出TableRange下边界的情况 if (endRowIndex < me.rowsNum) { for (colIndex = beginColIndex; colIndex < endColIndex; colIndex++) { cellInfo = me.indexTable[endRowIndex][colIndex]; rowIndex = cellInfo.rowIndex + cellInfo.rowSpan - 1; if (rowIndex > endRowIndex) { tmpEndRowIndex = Math.max(rowIndex, tmpEndRowIndex); } } } // 检查是否有超出TableRange左边界的情况 if (beginColIndex > 0) { for (rowIndex = beginRowIndex; rowIndex < endRowIndex; rowIndex++) { cellInfo = me.indexTable[rowIndex][beginColIndex]; colIndex = cellInfo.colIndex; if (colIndex < beginColIndex) { tmpBeginColIndex = Math.min(cellInfo.colIndex, tmpBeginColIndex); } } } //递归调用直至所有完成所有框选单元格的扩展 if (tmpBeginRowIndex != beginRowIndex || tmpBeginColIndex != beginColIndex || tmpEndRowIndex != endRowIndex || tmpEndColIndex != endColIndex) { return checkRange(tmpBeginRowIndex, tmpBeginColIndex, tmpEndRowIndex, tmpEndColIndex); } else { // 不需要扩展TableRange的情况 return { beginRowIndex:beginRowIndex, beginColIndex:beginColIndex, endRowIndex:endRowIndex, endColIndex:endColIndex }; } } try { var me = this, cellAInfo = me.getCellInfo(cellA); if (cellA === cellB) { return { beginRowIndex:cellAInfo.rowIndex, beginColIndex:cellAInfo.colIndex, endRowIndex:cellAInfo.rowIndex + cellAInfo.rowSpan - 1, endColIndex:cellAInfo.colIndex + cellAInfo.colSpan - 1 }; } var cellBInfo = me.getCellInfo(cellB); // 计算TableRange的四个边 var beginRowIndex = Math.min(cellAInfo.rowIndex, cellBInfo.rowIndex), beginColIndex = Math.min(cellAInfo.colIndex, cellBInfo.colIndex), endRowIndex = Math.max(cellAInfo.rowIndex + cellAInfo.rowSpan - 1, cellBInfo.rowIndex + cellBInfo.rowSpan - 1), endColIndex = Math.max(cellAInfo.colIndex + cellAInfo.colSpan - 1, cellBInfo.colIndex + cellBInfo.colSpan - 1); return checkRange(beginRowIndex, beginColIndex, endRowIndex, endColIndex); } catch (e) { //throw e; } }, /** * 依据cellsRange获取对应的单元格集合 */ getCells:function (range) { //每次获取cells之前必须先清除上次的选择,否则会对后续获取操作造成影响 this.clearSelected(); var beginRowIndex = range.beginRowIndex, beginColIndex = range.beginColIndex, endRowIndex = range.endRowIndex, endColIndex = range.endColIndex, cellInfo, rowIndex, colIndex, tdHash = {}, returnTds = []; for (var i = beginRowIndex; i <= endRowIndex; i++) { for (var j = beginColIndex; j <= endColIndex; j++) { cellInfo = this.indexTable[i][j]; rowIndex = cellInfo.rowIndex; colIndex = cellInfo.colIndex; // 如果Cells里已经包含了此Cell则跳过 var key = rowIndex + '|' + colIndex; if (tdHash[key]) continue; tdHash[key] = 1; if (rowIndex < i || colIndex < j || rowIndex + cellInfo.rowSpan - 1 > endRowIndex || colIndex + cellInfo.colSpan - 1 > endColIndex) { return null; } returnTds.push(this.getCell(rowIndex, cellInfo.cellIndex)); } } return returnTds; }, /** * 清理已经选中的单元格 */ clearSelected:function () { UETable.removeSelectedClass(this.selectedTds); this.selectedTds = []; this.cellsRange = {}; }, /** * 根据range设置已经选中的单元格 */ setSelected:function (range) { var cells = this.getCells(range); UETable.addSelectedClass(cells); this.selectedTds = cells; this.cellsRange = range; }, isFullRow:function () { var range = this.cellsRange; return (range.endColIndex - range.beginColIndex + 1) == this.colsNum; }, isFullCol:function () { var range = this.cellsRange, table = this.table, ths = table.getElementsByTagName("th"), rows = range.endRowIndex - range.beginRowIndex + 1; return !ths.length ? rows == this.rowsNum : rows == this.rowsNum || (rows == this.rowsNum - 1); }, /** * 获取视觉上的前置单元格,默认是左边,top传入时 * @param cell * @param top */ getNextCell:function (cell, bottom, ignoreRange) { try { var cellInfo = this.getCellInfo(cell), nextRowIndex, nextColIndex; var len = this.selectedTds.length && !ignoreRange, range = this.cellsRange; //末行或者末列没有后置单元格 if ((!bottom && (cellInfo.rowIndex == 0)) || (bottom && (!len ? (cellInfo.rowIndex + cellInfo.rowSpan > this.rowsNum - 1) : (range.endRowIndex == this.rowsNum - 1)))) return null; nextRowIndex = !bottom ? ( !len ? cellInfo.rowIndex - 1 : range.beginRowIndex - 1) : ( !len ? (cellInfo.rowIndex + cellInfo.rowSpan) : range.endRowIndex + 1); nextColIndex = !len ? cellInfo.colIndex : range.beginColIndex; return this.getCell(this.indexTable[nextRowIndex][nextColIndex].rowIndex, this.indexTable[nextRowIndex][nextColIndex].cellIndex); } catch (e) { showError(e); } }, getPreviewCell:function (cell, top) { try { var cellInfo = this.getCellInfo(cell), previewRowIndex, previewColIndex; var len = this.selectedTds.length, range = this.cellsRange; //首行或者首列没有前置单元格 if ((!top && (!len ? !cellInfo.colIndex : !range.beginColIndex)) || (top && (!len ? (cellInfo.rowIndex > (this.colsNum - 1)) : (range.endColIndex == this.colsNum - 1)))) return null; previewRowIndex = !top ? ( !len ? cellInfo.rowIndex : range.beginRowIndex ) : ( !len ? (cellInfo.rowIndex < 1 ? 0 : (cellInfo.rowIndex - 1)) : range.beginRowIndex); previewColIndex = !top ? ( !len ? (cellInfo.colIndex < 1 ? 0 : (cellInfo.colIndex - 1)) : range.beginColIndex - 1) : ( !len ? cellInfo.colIndex : range.endColIndex + 1); return this.getCell(this.indexTable[previewRowIndex][previewColIndex].rowIndex, this.indexTable[previewRowIndex][previewColIndex].cellIndex); } catch (e) { showError(e); } }, /** * 移动单元格中的内容 */ moveContent:function (cellTo, cellFrom) { if (UETable.isEmptyBlock(cellFrom)) return; if (UETable.isEmptyBlock(cellTo)) { cellTo.innerHTML = cellFrom.innerHTML; return; } var child = cellTo.lastChild; if (child.nodeType == 3 || !dtd.$block[child.tagName]) { cellTo.appendChild(cellTo.ownerDocument.createElement('br')) } while (child = cellFrom.firstChild) { cellTo.appendChild(child); } }, /** * 向右合并单元格 */ mergeRight:function (cell) { var cellInfo = this.getCellInfo(cell), rightColIndex = cellInfo.colIndex + cellInfo.colSpan, rightCellInfo = this.indexTable[cellInfo.rowIndex][rightColIndex], rightCell = this.getCell(rightCellInfo.rowIndex, rightCellInfo.cellIndex); //合并 cell.colSpan = cellInfo.colSpan + rightCellInfo.colSpan; //被合并的单元格不应存在宽度属性 cell.removeAttribute("width"); //移动内容 this.moveContent(cell, rightCell); //删掉被合并的Cell this.deleteCell(rightCell, rightCellInfo.rowIndex); this.update(); }, /** * 向下合并单元格 */ mergeDown:function (cell) { var cellInfo = this.getCellInfo(cell), downRowIndex = cellInfo.rowIndex + cellInfo.rowSpan, downCellInfo = this.indexTable[downRowIndex][cellInfo.colIndex], downCell = this.getCell(downCellInfo.rowIndex, downCellInfo.cellIndex); cell.rowSpan = cellInfo.rowSpan + downCellInfo.rowSpan; cell.removeAttribute("height"); this.moveContent(cell, downCell); this.deleteCell(downCell, downCellInfo.rowIndex); this.update(); }, /** * 合并整个range中的内容 */ mergeRange:function () { //由于合并操作可以在任意时刻进行,所以无法通过鼠标位置等信息实时生成range,只能通过缓存实例中的cellsRange对象来访问 var range = this.cellsRange, leftTopCell = this.getCell(range.beginRowIndex, this.indexTable[range.beginRowIndex][range.beginColIndex].cellIndex); if (leftTopCell.tagName == "TH" && range.endRowIndex !== range.beginRowIndex) { var index = this.indexTable, info = this.getCellInfo(leftTopCell); leftTopCell = this.getCell(1, index[1][info.colIndex].cellIndex); range = this.getCellsRange(leftTopCell, this.getCell(index[this.rowsNum - 1][info.colIndex].rowIndex, index[this.rowsNum - 1][info.colIndex].cellIndex)); } // 删除剩余的Cells var cells = this.getCells(range); for(var i= 0,ci;ci=cells[i++];){ if (ci !== leftTopCell) { this.moveContent(leftTopCell, ci); this.deleteCell(ci); } } // 修改左上角Cell的rowSpan和colSpan,并调整宽度属性设置 leftTopCell.rowSpan = range.endRowIndex - range.beginRowIndex + 1; leftTopCell.rowSpan > 1 && leftTopCell.removeAttribute("height"); leftTopCell.colSpan = range.endColIndex - range.beginColIndex + 1; leftTopCell.colSpan > 1 && leftTopCell.removeAttribute("width"); if (leftTopCell.rowSpan == this.rowsNum && leftTopCell.colSpan != 1) { leftTopCell.colSpan = 1; } if (leftTopCell.colSpan == this.colsNum && leftTopCell.rowSpan != 1) { var rowIndex = leftTopCell.parentNode.rowIndex; //解决IE下的表格操作问题 if( this.table.deleteRow ) { for (var i = rowIndex+ 1, curIndex=rowIndex+ 1, len=leftTopCell.rowSpan; i < len; i++) { this.table.deleteRow(curIndex); } } else { for (var i = 0, len=leftTopCell.rowSpan - 1; i < len; i++) { var row = this.table.rows[rowIndex + 1]; row.parentNode.removeChild(row); } } leftTopCell.rowSpan = 1; } this.update(); }, /** * 插入一行单元格 */ insertRow:function (rowIndex, sourceCell) { var numCols = this.colsNum, table = this.table, row = table.insertRow(rowIndex), cell, isInsertTitle = typeof sourceCell == 'string' && sourceCell.toUpperCase() == 'TH'; function replaceTdToTh(colIndex, cell, tableRow) { if (colIndex == 0) { var tr = tableRow.nextSibling || tableRow.previousSibling, th = tr.cells[colIndex]; if (th.tagName == 'TH') { th = cell.ownerDocument.createElement("th"); th.appendChild(cell.firstChild); tableRow.insertBefore(th, cell); domUtils.remove(cell) } }else{ if (cell.tagName == 'TH') { var td = cell.ownerDocument.createElement("td"); td.appendChild(cell.firstChild); tableRow.insertBefore(td, cell); domUtils.remove(cell) } } } //首行直接插入,无需考虑部分单元格被rowspan的情况 if (rowIndex == 0 || rowIndex == this.rowsNum) { for (var colIndex = 0; colIndex < numCols; colIndex++) { cell = this.cloneCell(sourceCell, true); this.setCellContent(cell); cell.getAttribute('vAlign') && cell.setAttribute('vAlign', cell.getAttribute('vAlign')); row.appendChild(cell); if(!isInsertTitle) replaceTdToTh(colIndex, cell, row); } } else { var infoRow = this.indexTable[rowIndex], cellIndex = 0; for (colIndex = 0; colIndex < numCols; colIndex++) { var cellInfo = infoRow[colIndex]; //如果存在某个单元格的rowspan穿过待插入行的位置,则修改该单元格的rowspan即可,无需插入单元格 if (cellInfo.rowIndex < rowIndex) { cell = this.getCell(cellInfo.rowIndex, cellInfo.cellIndex); cell.rowSpan = cellInfo.rowSpan + 1; } else { cell = this.cloneCell(sourceCell, true); this.setCellContent(cell); row.appendChild(cell); } if(!isInsertTitle) replaceTdToTh(colIndex, cell, row); } } //框选时插入不触发contentchange,需要手动更新索引。 this.update(); return row; }, /** * 删除一行单元格 * @param rowIndex */ deleteRow:function (rowIndex) { var row = this.table.rows[rowIndex], infoRow = this.indexTable[rowIndex], colsNum = this.colsNum, count = 0; //处理计数 for (var colIndex = 0; colIndex < colsNum;) { var cellInfo = infoRow[colIndex], cell = this.getCell(cellInfo.rowIndex, cellInfo.cellIndex); if (cell.rowSpan > 1) { if (cellInfo.rowIndex == rowIndex) { var clone = cell.cloneNode(true); clone.rowSpan = cell.rowSpan - 1; clone.innerHTML = ""; cell.rowSpan = 1; var nextRowIndex = rowIndex + 1, nextRow = this.table.rows[nextRowIndex], insertCellIndex, preMerged = this.getPreviewMergedCellsNum(nextRowIndex, colIndex) - count; if (preMerged < colIndex) { insertCellIndex = colIndex - preMerged - 1; //nextRow.insertCell(insertCellIndex); domUtils.insertAfter(nextRow.cells[insertCellIndex], clone); } else { if (nextRow.cells.length) nextRow.insertBefore(clone, nextRow.cells[0]) } count += 1; //cell.parentNode.removeChild(cell); } } colIndex += cell.colSpan || 1; } var deleteTds = [], cacheMap = {}; for (colIndex = 0; colIndex < colsNum; colIndex++) { var tmpRowIndex = infoRow[colIndex].rowIndex, tmpCellIndex = infoRow[colIndex].cellIndex, key = tmpRowIndex + "_" + tmpCellIndex; if (cacheMap[key])continue; cacheMap[key] = 1; cell = this.getCell(tmpRowIndex, tmpCellIndex); deleteTds.push(cell); } var mergeTds = []; utils.each(deleteTds, function (td) { if (td.rowSpan == 1) { td.parentNode.removeChild(td); } else { mergeTds.push(td); } }); utils.each(mergeTds, function (td) { td.rowSpan--; }); row.parentNode.removeChild(row); //浏览器方法本身存在bug,采用自定义方法删除 //this.table.deleteRow(rowIndex); this.update(); }, insertCol:function (colIndex, sourceCell, defaultValue) { var rowsNum = this.rowsNum, rowIndex = 0, tableRow, cell, backWidth = parseInt((this.table.offsetWidth - (this.colsNum + 1) * 20 - (this.colsNum + 1)) / (this.colsNum + 1), 10), isInsertTitleCol = typeof sourceCell == 'string' && sourceCell.toUpperCase() == 'TH'; function replaceTdToTh(rowIndex, cell, tableRow) { if (rowIndex == 0) { var th = cell.nextSibling || cell.previousSibling; if (th.tagName == 'TH') { th = cell.ownerDocument.createElement("th"); th.appendChild(cell.firstChild); tableRow.insertBefore(th, cell); domUtils.remove(cell) } }else{ if (cell.tagName == 'TH') { var td = cell.ownerDocument.createElement("td"); td.appendChild(cell.firstChild); tableRow.insertBefore(td, cell); domUtils.remove(cell) } } } var preCell; if (colIndex == 0 || colIndex == this.colsNum) { for (; rowIndex < rowsNum; rowIndex++) { tableRow = this.table.rows[rowIndex]; preCell = tableRow.cells[colIndex == 0 ? colIndex : tableRow.cells.length]; cell = this.cloneCell(sourceCell, true); //tableRow.insertCell(colIndex == 0 ? colIndex : tableRow.cells.length); this.setCellContent(cell); cell.setAttribute('vAlign', cell.getAttribute('vAlign')); preCell && cell.setAttribute('width', preCell.getAttribute('width')); if (!colIndex) { tableRow.insertBefore(cell, tableRow.cells[0]); } else { domUtils.insertAfter(tableRow.cells[tableRow.cells.length - 1], cell); } if(!isInsertTitleCol) replaceTdToTh(rowIndex, cell, tableRow) } } else { for (; rowIndex < rowsNum; rowIndex++) { var cellInfo = this.indexTable[rowIndex][colIndex]; if (cellInfo.colIndex < colIndex) { cell = this.getCell(cellInfo.rowIndex, cellInfo.cellIndex); cell.colSpan = cellInfo.colSpan + 1; } else { tableRow = this.table.rows[rowIndex]; preCell = tableRow.cells[cellInfo.cellIndex]; cell = this.cloneCell(sourceCell, true);//tableRow.insertCell(cellInfo.cellIndex); this.setCellContent(cell); cell.setAttribute('vAlign', cell.getAttribute('vAlign')); preCell && cell.setAttribute('width', preCell.getAttribute('width')); //防止IE下报错 preCell ? tableRow.insertBefore(cell, preCell) : tableRow.appendChild(cell); } if(!isInsertTitleCol) replaceTdToTh(rowIndex, cell, tableRow); } } //框选时插入不触发contentchange,需要手动更新索引 this.update(); this.updateWidth(backWidth, defaultValue || {tdPadding:10, tdBorder:1}); }, updateWidth:function (width, defaultValue) { var table = this.table, tmpWidth = UETable.getWidth(table) - defaultValue.tdPadding * 2 - defaultValue.tdBorder + width; if (tmpWidth < table.ownerDocument.body.offsetWidth) { table.setAttribute("width", tmpWidth); return; } var tds = domUtils.getElementsByTagName(this.table, "td th"); utils.each(tds, function (td) { td.setAttribute("width", width); }) }, deleteCol:function (colIndex) { var indexTable = this.indexTable, tableRows = this.table.rows, backTableWidth = this.table.getAttribute("width"), backTdWidth = 0, rowsNum = this.rowsNum, cacheMap = {}; for (var rowIndex = 0; rowIndex < rowsNum;) { var infoRow = indexTable[rowIndex], cellInfo = infoRow[colIndex], key = cellInfo.rowIndex + '_' + cellInfo.colIndex; // 跳过已经处理过的Cell if (cacheMap[key])continue; cacheMap[key] = 1; var cell = this.getCell(cellInfo.rowIndex, cellInfo.cellIndex); if (!backTdWidth) backTdWidth = cell && parseInt(cell.offsetWidth / cell.colSpan, 10).toFixed(0); // 如果Cell的colSpan大于1, 就修改colSpan, 否则就删掉这个Cell if (cell.colSpan > 1) { cell.colSpan--; } else { tableRows[rowIndex].deleteCell(cellInfo.cellIndex); } rowIndex += cellInfo.rowSpan || 1; } this.table.setAttribute("width", backTableWidth - backTdWidth); this.update(); }, splitToCells:function (cell) { var me = this, cells = this.splitToRows(cell); utils.each(cells, function (cell) { me.splitToCols(cell); }) }, splitToRows:function (cell) { var cellInfo = this.getCellInfo(cell), rowIndex = cellInfo.rowIndex, colIndex = cellInfo.colIndex, results = []; // 修改Cell的rowSpan cell.rowSpan = 1; results.push(cell); // 补齐单元格 for (var i = rowIndex, endRow = rowIndex + cellInfo.rowSpan; i < endRow; i++) { if (i == rowIndex)continue; var tableRow = this.table.rows[i], tmpCell = tableRow.insertCell(colIndex - this.getPreviewMergedCellsNum(i, colIndex)); tmpCell.colSpan = cellInfo.colSpan; this.setCellContent(tmpCell); tmpCell.setAttribute('vAlign', cell.getAttribute('vAlign')); tmpCell.setAttribute('align', cell.getAttribute('align')); if (cell.style.cssText) { tmpCell.style.cssText = cell.style.cssText; } results.push(tmpCell); } this.update(); return results; }, getPreviewMergedCellsNum:function (rowIndex, colIndex) { var indexRow = this.indexTable[rowIndex], num = 0; for (var i = 0; i < colIndex;) { var colSpan = indexRow[i].colSpan, tmpRowIndex = indexRow[i].rowIndex; num += (colSpan - (tmpRowIndex == rowIndex ? 1 : 0)); i += colSpan; } return num; }, splitToCols:function (cell) { var backWidth = (cell.offsetWidth / cell.colSpan - 22).toFixed(0), cellInfo = this.getCellInfo(cell), rowIndex = cellInfo.rowIndex, colIndex = cellInfo.colIndex, results = []; // 修改Cell的rowSpan cell.colSpan = 1; cell.setAttribute("width", backWidth); results.push(cell); // 补齐单元格 for (var j = colIndex, endCol = colIndex + cellInfo.colSpan; j < endCol; j++) { if (j == colIndex)continue; var tableRow = this.table.rows[rowIndex], tmpCell = tableRow.insertCell(this.indexTable[rowIndex][j].cellIndex + 1); tmpCell.rowSpan = cellInfo.rowSpan; this.setCellContent(tmpCell); tmpCell.setAttribute('vAlign', cell.getAttribute('vAlign')); tmpCell.setAttribute('align', cell.getAttribute('align')); tmpCell.setAttribute('width', backWidth); if (cell.style.cssText) { tmpCell.style.cssText = cell.style.cssText; } //处理th的情况 if (cell.tagName == 'TH') { var th = cell.ownerDocument.createElement('th'); th.appendChild(tmpCell.firstChild); th.setAttribute('vAlign', cell.getAttribute('vAlign')); th.rowSpan = tmpCell.rowSpan; tableRow.insertBefore(th, tmpCell); domUtils.remove(tmpCell); } results.push(tmpCell); } this.update(); return results; }, isLastCell:function (cell, rowsNum, colsNum) { rowsNum = rowsNum || this.rowsNum; colsNum = colsNum || this.colsNum; var cellInfo = this.getCellInfo(cell); return ((cellInfo.rowIndex + cellInfo.rowSpan) == rowsNum) && ((cellInfo.colIndex + cellInfo.colSpan) == colsNum); }, getLastCell:function (cells) { cells = cells || this.table.getElementsByTagName("td"); var firstInfo = this.getCellInfo(cells[0]); var me = this, last = cells[0], tr = last.parentNode, cellsNum = 0, cols = 0, rows; utils.each(cells, function (cell) { if (cell.parentNode == tr)cols += cell.colSpan || 1; cellsNum += cell.rowSpan * cell.colSpan || 1; }); rows = cellsNum / cols; utils.each(cells, function (cell) { if (me.isLastCell(cell, rows, cols)) { last = cell; return false; } }); return last; }, selectRow:function (rowIndex) { var indexRow = this.indexTable[rowIndex], start = this.getCell(indexRow[0].rowIndex, indexRow[0].cellIndex), end = this.getCell(indexRow[this.colsNum - 1].rowIndex, indexRow[this.colsNum - 1].cellIndex), range = this.getCellsRange(start, end); this.setSelected(range); }, selectTable:function () { var tds = this.table.getElementsByTagName("td"), range = this.getCellsRange(tds[0], tds[tds.length - 1]); this.setSelected(range); }, setBackground:function (cells, value) { if (typeof value === "string") { utils.each(cells, function (cell) { cell.style.backgroundColor = value; }) } else if (typeof value === "object") { value = utils.extend({ repeat:true, colorList:["#ddd", "#fff"] }, value); var rowIndex = this.getCellInfo(cells[0]).rowIndex, count = 0, colors = value.colorList, getColor = function (list, index, repeat) { return list[index] ? list[index] : repeat ? list[index % list.length] : ""; }; for (var i = 0, cell; cell = cells[i++];) { var cellInfo = this.getCellInfo(cell); cell.style.backgroundColor = getColor(colors, ((rowIndex + count) == cellInfo.rowIndex) ? count : ++count, value.repeat); } } }, removeBackground:function (cells) { utils.each(cells, function (cell) { cell.style.backgroundColor = ""; }) } }; function showError(e) { } })(); // plugins/table.cmds.js /** * Created with JetBrains PhpStorm. * User: taoqili * Date: 13-2-20 * Time: 下午6:25 * To change this template use File | Settings | File Templates. */ ; (function () { var UT = UE.UETable, getTableItemsByRange = function (editor) { return UT.getTableItemsByRange(editor); }, getUETableBySelected = function (editor) { return UT.getUETableBySelected(editor) }, getDefaultValue = function (editor, table) { return UT.getDefaultValue(editor, table); }, getUETable = function (tdOrTable) { return UT.getUETable(tdOrTable); }; UE.commands['inserttable'] = { queryCommandState: function () { return getTableItemsByRange(this).table ? -1 : 0; }, execCommand: function (cmd, opt) { function createTable(opt, tdWidth) { var html = [], rowsNum = opt.numRows, colsNum = opt.numCols; for (var r = 0; r < rowsNum; r++) { html.push(''); for (var c = 0; c < colsNum; c++) { html.push('
  • ' + (browser.ie && browser.version < 11 ? domUtils.fillChar : '
    ') + '
    ' + html.join('') + '
    ' } if (!opt) { opt = utils.extend({}, { numCols: this.options.defaultCols, numRows: this.options.defaultRows, tdvalign: this.options.tdvalign }) } var me = this; var range = this.selection.getRange(), start = range.startContainer, firstParentBlock = domUtils.findParent(start, function (node) { return domUtils.isBlockElm(node); }, true) || me.body; var defaultValue = getDefaultValue(me), tableWidth = firstParentBlock.offsetWidth, tdWidth = Math.floor(tableWidth / opt.numCols - defaultValue.tdPadding * 2 - defaultValue.tdBorder); //todo其他属性 !opt.tdvalign && (opt.tdvalign = me.options.tdvalign); me.execCommand("inserthtml", createTable(opt, tdWidth)); } }; UE.commands['insertparagraphbeforetable'] = { queryCommandState: function () { return getTableItemsByRange(this).cell ? 0 : -1; }, execCommand: function () { var table = getTableItemsByRange(this).table; if (table) { var p = this.document.createElement("p"); p.innerHTML = browser.ie ? ' ' : '
    '; table.parentNode.insertBefore(p, table); this.selection.getRange().setStart(p, 0).setCursor(); } } }; UE.commands['deletetable'] = { queryCommandState: function () { var rng = this.selection.getRange(); return domUtils.findParentByTagName(rng.startContainer, 'table', true) ? 0 : -1; }, execCommand: function (cmd, table) { var rng = this.selection.getRange(); table = table || domUtils.findParentByTagName(rng.startContainer, 'table', true); if (table) { var next = table.nextSibling; if (!next) { next = domUtils.createElement(this.document, 'p', { 'innerHTML': browser.ie ? domUtils.fillChar : '
    ' }); table.parentNode.insertBefore(next, table); } domUtils.remove(table); rng = this.selection.getRange(); if (next.nodeType == 3) { rng.setStartBefore(next) } else { rng.setStart(next, 0) } rng.setCursor(false, true) this.fireEvent("tablehasdeleted") } } }; UE.commands['cellalign'] = { queryCommandState: function () { return getSelectedArr(this).length ? 0 : -1 }, execCommand: function (cmd, align) { var selectedTds = getSelectedArr(this); if (selectedTds.length) { for (var i = 0, ci; ci = selectedTds[i++];) { ci.setAttribute('align', align); } } } }; UE.commands['cellvalign'] = { queryCommandState: function () { return getSelectedArr(this).length ? 0 : -1; }, execCommand: function (cmd, valign) { var selectedTds = getSelectedArr(this); if (selectedTds.length) { for (var i = 0, ci; ci = selectedTds[i++];) { ci.setAttribute('vAlign', valign); } } } }; UE.commands['insertcaption'] = { queryCommandState: function () { var table = getTableItemsByRange(this).table; if (table) { return table.getElementsByTagName('caption').length == 0 ? 1 : -1; } return -1; }, execCommand: function () { var table = getTableItemsByRange(this).table; if (table) { var caption = this.document.createElement('caption'); caption.innerHTML = browser.ie ? domUtils.fillChar : '
    '; table.insertBefore(caption, table.firstChild); var range = this.selection.getRange(); range.setStart(caption, 0).setCursor(); } } }; UE.commands['deletecaption'] = { queryCommandState: function () { var rng = this.selection.getRange(), table = domUtils.findParentByTagName(rng.startContainer, 'table'); if (table) { return table.getElementsByTagName('caption').length == 0 ? -1 : 1; } return -1; }, execCommand: function () { var rng = this.selection.getRange(), table = domUtils.findParentByTagName(rng.startContainer, 'table'); if (table) { domUtils.remove(table.getElementsByTagName('caption')[0]); var range = this.selection.getRange(); range.setStart(table.rows[0].cells[0], 0).setCursor(); } } }; UE.commands['inserttitle'] = { queryCommandState: function () { var table = getTableItemsByRange(this).table; if (table) { var firstRow = table.rows[0]; return firstRow.cells[firstRow.cells.length-1].tagName.toLowerCase() != 'th' ? 0 : -1 } return -1; }, execCommand: function () { var table = getTableItemsByRange(this).table; if (table) { getUETable(table).insertRow(0, 'th'); } var th = table.getElementsByTagName('th')[0]; this.selection.getRange().setStart(th, 0).setCursor(false, true); } }; UE.commands['deletetitle'] = { queryCommandState: function () { var table = getTableItemsByRange(this).table; if (table) { var firstRow = table.rows[0]; return firstRow.cells[firstRow.cells.length-1].tagName.toLowerCase() == 'th' ? 0 : -1 } return -1; }, execCommand: function () { var table = getTableItemsByRange(this).table; if (table) { domUtils.remove(table.rows[0]) } var td = table.getElementsByTagName('td')[0]; this.selection.getRange().setStart(td, 0).setCursor(false, true); } }; UE.commands['inserttitlecol'] = { queryCommandState: function () { var table = getTableItemsByRange(this).table; if (table) { var lastRow = table.rows[table.rows.length-1]; return lastRow.getElementsByTagName('th').length ? -1 : 0; } return -1; }, execCommand: function (cmd) { var table = getTableItemsByRange(this).table; if (table) { getUETable(table).insertCol(0, 'th'); } resetTdWidth(table, this); var th = table.getElementsByTagName('th')[0]; this.selection.getRange().setStart(th, 0).setCursor(false, true); } }; UE.commands['deletetitlecol'] = { queryCommandState: function () { var table = getTableItemsByRange(this).table; if (table) { var lastRow = table.rows[table.rows.length-1]; return lastRow.getElementsByTagName('th').length ? 0 : -1; } return -1; }, execCommand: function () { var table = getTableItemsByRange(this).table; if (table) { for(var i = 0; i< table.rows.length; i++ ){ domUtils.remove(table.rows[i].children[0]) } } resetTdWidth(table, this); var td = table.getElementsByTagName('td')[0]; this.selection.getRange().setStart(td, 0).setCursor(false, true); } }; UE.commands["mergeright"] = { queryCommandState: function (cmd) { var tableItems = getTableItemsByRange(this), table = tableItems.table, cell = tableItems.cell; if (!table || !cell) return -1; var ut = getUETable(table); if (ut.selectedTds.length) return -1; var cellInfo = ut.getCellInfo(cell), rightColIndex = cellInfo.colIndex + cellInfo.colSpan; if (rightColIndex >= ut.colsNum) return -1; // 如果处于最右边则不能向右合并 var rightCellInfo = ut.indexTable[cellInfo.rowIndex][rightColIndex], rightCell = table.rows[rightCellInfo.rowIndex].cells[rightCellInfo.cellIndex]; if (!rightCell || cell.tagName != rightCell.tagName) return -1; // TH和TD不能相互合并 // 当且仅当两个Cell的开始列号和结束列号一致时能进行合并 return (rightCellInfo.rowIndex == cellInfo.rowIndex && rightCellInfo.rowSpan == cellInfo.rowSpan) ? 0 : -1; }, execCommand: function (cmd) { var rng = this.selection.getRange(), bk = rng.createBookmark(true); var cell = getTableItemsByRange(this).cell, ut = getUETable(cell); ut.mergeRight(cell); rng.moveToBookmark(bk).select(); } }; UE.commands["mergedown"] = { queryCommandState: function (cmd) { var tableItems = getTableItemsByRange(this), table = tableItems.table, cell = tableItems.cell; if (!table || !cell) return -1; var ut = getUETable(table); if (ut.selectedTds.length)return -1; var cellInfo = ut.getCellInfo(cell), downRowIndex = cellInfo.rowIndex + cellInfo.rowSpan; if (downRowIndex >= ut.rowsNum) return -1; // 如果处于最下边则不能向下合并 var downCellInfo = ut.indexTable[downRowIndex][cellInfo.colIndex], downCell = table.rows[downCellInfo.rowIndex].cells[downCellInfo.cellIndex]; if (!downCell || cell.tagName != downCell.tagName) return -1; // TH和TD不能相互合并 // 当且仅当两个Cell的开始列号和结束列号一致时能进行合并 return (downCellInfo.colIndex == cellInfo.colIndex && downCellInfo.colSpan == cellInfo.colSpan) ? 0 : -1; }, execCommand: function () { var rng = this.selection.getRange(), bk = rng.createBookmark(true); var cell = getTableItemsByRange(this).cell, ut = getUETable(cell); ut.mergeDown(cell); rng.moveToBookmark(bk).select(); } }; UE.commands["mergecells"] = { queryCommandState: function () { return getUETableBySelected(this) ? 0 : -1; }, execCommand: function () { var ut = getUETableBySelected(this); if (ut && ut.selectedTds.length) { var cell = ut.selectedTds[0]; ut.mergeRange(); var rng = this.selection.getRange(); if (domUtils.isEmptyBlock(cell)) { rng.setStart(cell, 0).collapse(true) } else { rng.selectNodeContents(cell) } rng.select(); } } }; UE.commands["insertrow"] = { queryCommandState: function () { var tableItems = getTableItemsByRange(this), cell = tableItems.cell; return cell && (cell.tagName == "TD" || (cell.tagName == 'TH' && tableItems.tr !== tableItems.table.rows[0])) && getUETable(tableItems.table).rowsNum < this.options.maxRowNum ? 0 : -1; }, execCommand: function () { var rng = this.selection.getRange(), bk = rng.createBookmark(true); var tableItems = getTableItemsByRange(this), cell = tableItems.cell, table = tableItems.table, ut = getUETable(table), cellInfo = ut.getCellInfo(cell); //ut.insertRow(!ut.selectedTds.length ? cellInfo.rowIndex:ut.cellsRange.beginRowIndex,''); if (!ut.selectedTds.length) { ut.insertRow(cellInfo.rowIndex, cell); } else { var range = ut.cellsRange; for (var i = 0, len = range.endRowIndex - range.beginRowIndex + 1; i < len; i++) { ut.insertRow(range.beginRowIndex, cell); } } rng.moveToBookmark(bk).select(); if (table.getAttribute("interlaced") === "enabled")this.fireEvent("interlacetable", table); } }; //后插入行 UE.commands["insertrownext"] = { queryCommandState: function () { var tableItems = getTableItemsByRange(this), cell = tableItems.cell; return cell && (cell.tagName == "TD") && getUETable(tableItems.table).rowsNum < this.options.maxRowNum ? 0 : -1; }, execCommand: function () { var rng = this.selection.getRange(), bk = rng.createBookmark(true); var tableItems = getTableItemsByRange(this), cell = tableItems.cell, table = tableItems.table, ut = getUETable(table), cellInfo = ut.getCellInfo(cell); //ut.insertRow(!ut.selectedTds.length? cellInfo.rowIndex + cellInfo.rowSpan : ut.cellsRange.endRowIndex + 1,''); if (!ut.selectedTds.length) { ut.insertRow(cellInfo.rowIndex + cellInfo.rowSpan, cell); } else { var range = ut.cellsRange; for (var i = 0, len = range.endRowIndex - range.beginRowIndex + 1; i < len; i++) { ut.insertRow(range.endRowIndex + 1, cell); } } rng.moveToBookmark(bk).select(); if (table.getAttribute("interlaced") === "enabled")this.fireEvent("interlacetable", table); } }; UE.commands["deleterow"] = { queryCommandState: function () { var tableItems = getTableItemsByRange(this); return tableItems.cell ? 0 : -1; }, execCommand: function () { var cell = getTableItemsByRange(this).cell, ut = getUETable(cell), cellsRange = ut.cellsRange, cellInfo = ut.getCellInfo(cell), preCell = ut.getVSideCell(cell), nextCell = ut.getVSideCell(cell, true), rng = this.selection.getRange(); if (utils.isEmptyObject(cellsRange)) { ut.deleteRow(cellInfo.rowIndex); } else { for (var i = cellsRange.beginRowIndex; i < cellsRange.endRowIndex + 1; i++) { ut.deleteRow(cellsRange.beginRowIndex); } } var table = ut.table; if (!table.getElementsByTagName('td').length) { var nextSibling = table.nextSibling; domUtils.remove(table); if (nextSibling) { rng.setStart(nextSibling, 0).setCursor(false, true); } } else { if (cellInfo.rowSpan == 1 || cellInfo.rowSpan == cellsRange.endRowIndex - cellsRange.beginRowIndex + 1) { if (nextCell || preCell) rng.selectNodeContents(nextCell || preCell).setCursor(false, true); } else { var newCell = ut.getCell(cellInfo.rowIndex, ut.indexTable[cellInfo.rowIndex][cellInfo.colIndex].cellIndex); if (newCell) rng.selectNodeContents(newCell).setCursor(false, true); } } if (table.getAttribute("interlaced") === "enabled")this.fireEvent("interlacetable", table); } }; UE.commands["insertcol"] = { queryCommandState: function (cmd) { var tableItems = getTableItemsByRange(this), cell = tableItems.cell; return cell && (cell.tagName == "TD" || (cell.tagName == 'TH' && cell !== tableItems.tr.cells[0])) && getUETable(tableItems.table).colsNum < this.options.maxColNum ? 0 : -1; }, execCommand: function (cmd) { var rng = this.selection.getRange(), bk = rng.createBookmark(true); if (this.queryCommandState(cmd) == -1)return; var cell = getTableItemsByRange(this).cell, ut = getUETable(cell), cellInfo = ut.getCellInfo(cell); //ut.insertCol(!ut.selectedTds.length ? cellInfo.colIndex:ut.cellsRange.beginColIndex); if (!ut.selectedTds.length) { ut.insertCol(cellInfo.colIndex, cell); } else { var range = ut.cellsRange; for (var i = 0, len = range.endColIndex - range.beginColIndex + 1; i < len; i++) { ut.insertCol(range.beginColIndex, cell); } } rng.moveToBookmark(bk).select(true); } }; UE.commands["insertcolnext"] = { queryCommandState: function () { var tableItems = getTableItemsByRange(this), cell = tableItems.cell; return cell && getUETable(tableItems.table).colsNum < this.options.maxColNum ? 0 : -1; }, execCommand: function () { var rng = this.selection.getRange(), bk = rng.createBookmark(true); var cell = getTableItemsByRange(this).cell, ut = getUETable(cell), cellInfo = ut.getCellInfo(cell); //ut.insertCol(!ut.selectedTds.length ? cellInfo.colIndex + cellInfo.colSpan:ut.cellsRange.endColIndex +1); if (!ut.selectedTds.length) { ut.insertCol(cellInfo.colIndex + cellInfo.colSpan, cell); } else { var range = ut.cellsRange; for (var i = 0, len = range.endColIndex - range.beginColIndex + 1; i < len; i++) { ut.insertCol(range.endColIndex + 1, cell); } } rng.moveToBookmark(bk).select(); } }; UE.commands["deletecol"] = { queryCommandState: function () { var tableItems = getTableItemsByRange(this); return tableItems.cell ? 0 : -1; }, execCommand: function () { var cell = getTableItemsByRange(this).cell, ut = getUETable(cell), range = ut.cellsRange, cellInfo = ut.getCellInfo(cell), preCell = ut.getHSideCell(cell), nextCell = ut.getHSideCell(cell, true); if (utils.isEmptyObject(range)) { ut.deleteCol(cellInfo.colIndex); } else { for (var i = range.beginColIndex; i < range.endColIndex + 1; i++) { ut.deleteCol(range.beginColIndex); } } var table = ut.table, rng = this.selection.getRange(); if (!table.getElementsByTagName('td').length) { var nextSibling = table.nextSibling; domUtils.remove(table); if (nextSibling) { rng.setStart(nextSibling, 0).setCursor(false, true); } } else { if (domUtils.inDoc(cell, this.document)) { rng.setStart(cell, 0).setCursor(false, true); } else { if (nextCell && domUtils.inDoc(nextCell, this.document)) { rng.selectNodeContents(nextCell).setCursor(false, true); } else { if (preCell && domUtils.inDoc(preCell, this.document)) { rng.selectNodeContents(preCell).setCursor(true, true); } } } } } }; UE.commands["splittocells"] = { queryCommandState: function () { var tableItems = getTableItemsByRange(this), cell = tableItems.cell; if (!cell) return -1; var ut = getUETable(tableItems.table); if (ut.selectedTds.length > 0) return -1; return cell && (cell.colSpan > 1 || cell.rowSpan > 1) ? 0 : -1; }, execCommand: function () { var rng = this.selection.getRange(), bk = rng.createBookmark(true); var cell = getTableItemsByRange(this).cell, ut = getUETable(cell); ut.splitToCells(cell); rng.moveToBookmark(bk).select(); } }; UE.commands["splittorows"] = { queryCommandState: function () { var tableItems = getTableItemsByRange(this), cell = tableItems.cell; if (!cell) return -1; var ut = getUETable(tableItems.table); if (ut.selectedTds.length > 0) return -1; return cell && cell.rowSpan > 1 ? 0 : -1; }, execCommand: function () { var rng = this.selection.getRange(), bk = rng.createBookmark(true); var cell = getTableItemsByRange(this).cell, ut = getUETable(cell); ut.splitToRows(cell); rng.moveToBookmark(bk).select(); } }; UE.commands["splittocols"] = { queryCommandState: function () { var tableItems = getTableItemsByRange(this), cell = tableItems.cell; if (!cell) return -1; var ut = getUETable(tableItems.table); if (ut.selectedTds.length > 0) return -1; return cell && cell.colSpan > 1 ? 0 : -1; }, execCommand: function () { var rng = this.selection.getRange(), bk = rng.createBookmark(true); var cell = getTableItemsByRange(this).cell, ut = getUETable(cell); ut.splitToCols(cell); rng.moveToBookmark(bk).select(); } }; UE.commands["adaptbytext"] = UE.commands["adaptbywindow"] = { queryCommandState: function () { return getTableItemsByRange(this).table ? 0 : -1 }, execCommand: function (cmd) { var tableItems = getTableItemsByRange(this), table = tableItems.table; if (table) { if (cmd == 'adaptbywindow') { resetTdWidth(table, this); } else { var cells = domUtils.getElementsByTagName(table, "td th"); utils.each(cells, function (cell) { cell.removeAttribute("width"); }); table.removeAttribute("width"); } } } }; //平均分配各列 UE.commands['averagedistributecol'] = { queryCommandState: function () { var ut = getUETableBySelected(this); if (!ut) return -1; return ut.isFullRow() || ut.isFullCol() ? 0 : -1; }, execCommand: function (cmd) { var me = this, ut = getUETableBySelected(me); function getAverageWidth() { var tb = ut.table, averageWidth, sumWidth = 0, colsNum = 0, tbAttr = getDefaultValue(me, tb); if (ut.isFullRow()) { sumWidth = tb.offsetWidth; colsNum = ut.colsNum; } else { var begin = ut.cellsRange.beginColIndex, end = ut.cellsRange.endColIndex, node; for (var i = begin; i <= end;) { node = ut.selectedTds[i]; sumWidth += node.offsetWidth; i += node.colSpan; colsNum += 1; } } averageWidth = Math.ceil(sumWidth / colsNum) - tbAttr.tdBorder * 2 - tbAttr.tdPadding * 2; return averageWidth; } function setAverageWidth(averageWidth) { utils.each(domUtils.getElementsByTagName(ut.table, "th"), function (node) { node.setAttribute("width", ""); }); var cells = ut.isFullRow() ? domUtils.getElementsByTagName(ut.table, "td") : ut.selectedTds; utils.each(cells, function (node) { if (node.colSpan == 1) { node.setAttribute("width", averageWidth); } }); } if (ut && ut.selectedTds.length) { setAverageWidth(getAverageWidth()); } } }; //平均分配各行 UE.commands['averagedistributerow'] = { queryCommandState: function () { var ut = getUETableBySelected(this); if (!ut) return -1; if (ut.selectedTds && /th/ig.test(ut.selectedTds[0].tagName)) return -1; return ut.isFullRow() || ut.isFullCol() ? 0 : -1; }, execCommand: function (cmd) { var me = this, ut = getUETableBySelected(me); function getAverageHeight() { var averageHeight, rowNum, sumHeight = 0, tb = ut.table, tbAttr = getDefaultValue(me, tb), tdpadding = parseInt(domUtils.getComputedStyle(tb.getElementsByTagName('td')[0], "padding-top")); if (ut.isFullCol()) { var captionArr = domUtils.getElementsByTagName(tb, "caption"), thArr = domUtils.getElementsByTagName(tb, "th"), captionHeight, thHeight; if (captionArr.length > 0) { captionHeight = captionArr[0].offsetHeight; } if (thArr.length > 0) { thHeight = thArr[0].offsetHeight; } sumHeight = tb.offsetHeight - (captionHeight || 0) - (thHeight || 0); rowNum = thArr.length == 0 ? ut.rowsNum : (ut.rowsNum - 1); } else { var begin = ut.cellsRange.beginRowIndex, end = ut.cellsRange.endRowIndex, count = 0, trs = domUtils.getElementsByTagName(tb, "tr"); for (var i = begin; i <= end; i++) { sumHeight += trs[i].offsetHeight; count += 1; } rowNum = count; } //ie8下是混杂模式 if (browser.ie && browser.version < 9) { averageHeight = Math.ceil(sumHeight / rowNum); } else { averageHeight = Math.ceil(sumHeight / rowNum) - tbAttr.tdBorder * 2 - tdpadding * 2; } return averageHeight; } function setAverageHeight(averageHeight) { var cells = ut.isFullCol() ? domUtils.getElementsByTagName(ut.table, "td") : ut.selectedTds; utils.each(cells, function (node) { if (node.rowSpan == 1) { node.setAttribute("height", averageHeight); } }); } if (ut && ut.selectedTds.length) { setAverageHeight(getAverageHeight()); } } }; //单元格对齐方式 UE.commands['cellalignment'] = { queryCommandState: function () { return getTableItemsByRange(this).table ? 0 : -1 }, execCommand: function (cmd, data) { var me = this, ut = getUETableBySelected(me); if (!ut) { var start = me.selection.getStart(), cell = start && domUtils.findParentByTagName(start, ["td", "th", "caption"], true); if (!/caption/ig.test(cell.tagName)) { domUtils.setAttributes(cell, data); } else { cell.style.textAlign = data.align; cell.style.verticalAlign = data.vAlign; } me.selection.getRange().setCursor(true); } else { utils.each(ut.selectedTds, function (cell) { domUtils.setAttributes(cell, data); }); } }, /** * 查询当前点击的单元格的对齐状态, 如果当前已经选择了多个单元格, 则会返回所有单元格经过统一协调过后的状态 * @see UE.UETable.getTableCellAlignState */ queryCommandValue: function (cmd) { var activeMenuCell = getTableItemsByRange( this).cell; if( !activeMenuCell ) { activeMenuCell = getSelectedArr(this)[0]; } if (!activeMenuCell) { return null; } else { //获取同时选中的其他单元格 var cells = UE.UETable.getUETable(activeMenuCell).selectedTds; !cells.length && ( cells = activeMenuCell ); return UE.UETable.getTableCellAlignState(cells); } } }; //表格对齐方式 UE.commands['tablealignment'] = { queryCommandState: function () { if (browser.ie && browser.version < 8) { return -1; } return getTableItemsByRange(this).table ? 0 : -1 }, execCommand: function (cmd, value) { var me = this, start = me.selection.getStart(), table = start && domUtils.findParentByTagName(start, ["table"], true); if (table) { table.setAttribute("align",value); } } }; //表格属性 UE.commands['edittable'] = { queryCommandState: function () { return getTableItemsByRange(this).table ? 0 : -1 }, execCommand: function (cmd, color) { var rng = this.selection.getRange(), table = domUtils.findParentByTagName(rng.startContainer, 'table'); if (table) { var arr = domUtils.getElementsByTagName(table, "td").concat( domUtils.getElementsByTagName(table, "th"), domUtils.getElementsByTagName(table, "caption") ); utils.each(arr, function (node) { node.style.borderColor = color; }); } } }; //单元格属性 UE.commands['edittd'] = { queryCommandState: function () { return getTableItemsByRange(this).table ? 0 : -1 }, execCommand: function (cmd, bkColor) { var me = this, ut = getUETableBySelected(me); if (!ut) { var start = me.selection.getStart(), cell = start && domUtils.findParentByTagName(start, ["td", "th", "caption"], true); if (cell) { cell.style.backgroundColor = bkColor; } } else { utils.each(ut.selectedTds, function (cell) { cell.style.backgroundColor = bkColor; }); } } }; UE.commands["settablebackground"] = { queryCommandState: function () { return getSelectedArr(this).length > 1 ? 0 : -1; }, execCommand: function (cmd, value) { var cells, ut; cells = getSelectedArr(this); ut = getUETable(cells[0]); ut.setBackground(cells, value); } }; UE.commands["cleartablebackground"] = { queryCommandState: function () { var cells = getSelectedArr(this); if (!cells.length)return -1; for (var i = 0, cell; cell = cells[i++];) { if (cell.style.backgroundColor !== "") return 0; } return -1; }, execCommand: function () { var cells = getSelectedArr(this), ut = getUETable(cells[0]); ut.removeBackground(cells); } }; UE.commands["interlacetable"] = UE.commands["uninterlacetable"] = { queryCommandState: function (cmd) { var table = getTableItemsByRange(this).table; if (!table) return -1; var interlaced = table.getAttribute("interlaced"); if (cmd == "interlacetable") { //TODO 待定 //是否需要待定,如果设置,则命令只能单次执行成功,但反射具备toggle效果;否则可以覆盖前次命令,但反射将不存在toggle效果 return (interlaced === "enabled") ? -1 : 0; } else { return (!interlaced || interlaced === "disabled") ? -1 : 0; } }, execCommand: function (cmd, classList) { var table = getTableItemsByRange(this).table; if (cmd == "interlacetable") { table.setAttribute("interlaced", "enabled"); this.fireEvent("interlacetable", table, classList); } else { table.setAttribute("interlaced", "disabled"); this.fireEvent("uninterlacetable", table); } } }; UE.commands["setbordervisible"] = { queryCommandState: function (cmd) { var table = getTableItemsByRange(this).table; if (!table) return -1; return 0; }, execCommand: function () { var table = getTableItemsByRange(this).table; utils.each(domUtils.getElementsByTagName(table,'td'),function(td){ td.style.borderWidth = '1px'; td.style.borderStyle = 'solid'; }) } }; function resetTdWidth(table, editor) { var tds = domUtils.getElementsByTagName(table,'td th'); utils.each(tds, function (td) { td.removeAttribute("width"); }); table.setAttribute('width', getTableWidth(editor, true, getDefaultValue(editor, table))); var tdsWidths = []; setTimeout(function () { utils.each(tds, function (td) { (td.colSpan == 1) && tdsWidths.push(td.offsetWidth) }) utils.each(tds, function (td,i) { (td.colSpan == 1) && td.setAttribute("width", tdsWidths[i] + ""); }) }, 0); } function getTableWidth(editor, needIEHack, defaultValue) { var body = editor.body; return body.offsetWidth - (needIEHack ? parseInt(domUtils.getComputedStyle(body, 'margin-left'), 10) * 2 : 0) - defaultValue.tableBorder * 2 - (editor.options.offsetWidth || 0); } function getSelectedArr(editor) { var cell = getTableItemsByRange(editor).cell; if (cell) { var ut = getUETable(cell); return ut.selectedTds.length ? ut.selectedTds : [cell]; } else { return []; } } })(); // plugins/table.action.js /** * Created with JetBrains PhpStorm. * User: taoqili * Date: 12-10-12 * Time: 上午10:05 * To change this template use File | Settings | File Templates. */ UE.plugins['table'] = function () { var me = this, tabTimer = null, //拖动计时器 tableDragTimer = null, //双击计时器 tableResizeTimer = null, //单元格最小宽度 cellMinWidth = 5, isInResizeBuffer = false, //单元格边框大小 cellBorderWidth = 5, //鼠标偏移距离 offsetOfTableCell = 10, //记录在有限时间内的点击状态, 共有3个取值, 0, 1, 2。 0代表未初始化, 1代表单击了1次,2代表2次 singleClickState = 0, userActionStatus = null, //双击允许的时间范围 dblclickTime = 360, UT = UE.UETable, getUETable = function (tdOrTable) { return UT.getUETable(tdOrTable); }, getUETableBySelected = function (editor) { return UT.getUETableBySelected(editor); }, getDefaultValue = function (editor, table) { return UT.getDefaultValue(editor, table); }, removeSelectedClass = function (cells) { return UT.removeSelectedClass(cells); }; function showError(e) { // throw e; } me.ready(function(){ var me = this; var orgGetText = me.selection.getText; me.selection.getText = function(){ var table = getUETableBySelected(me); if(table){ var str = ''; utils.each(table.selectedTds,function(td){ str += td[browser.ie?'innerText':'textContent']; }) return str; }else{ return orgGetText.call(me.selection) } } }) //处理拖动及框选相关方法 var startTd = null, //鼠标按下时的锚点td currentTd = null, //当前鼠标经过时的td onDrag = "", //指示当前拖动状态,其值可为"","h","v" ,分别表示未拖动状态,横向拖动状态,纵向拖动状态,用于鼠标移动过程中的判断 onBorder = false, //检测鼠标按下时是否处在单元格边缘位置 dragButton = null, dragOver = false, dragLine = null, //模拟的拖动线 dragTd = null; //发生拖动的目标td var mousedown = false, //todo 判断混乱模式 needIEHack = true; me.setOpt({ 'maxColNum':20, 'maxRowNum':100, 'defaultCols':5, 'defaultRows':5, 'tdvalign':'top', 'cursorpath':me.options.UEDITOR_HOME_URL + "themes/default/images/cursor_", 'tableDragable':false, 'classList':["ue-table-interlace-color-single","ue-table-interlace-color-double"] }); me.getUETable = getUETable; var commands = { 'deletetable':1, 'inserttable':1, 'cellvalign':1, 'insertcaption':1, 'deletecaption':1, 'inserttitle':1, 'deletetitle':1, "mergeright":1, "mergedown":1, "mergecells":1, "insertrow":1, "insertrownext":1, "deleterow":1, "insertcol":1, "insertcolnext":1, "deletecol":1, "splittocells":1, "splittorows":1, "splittocols":1, "adaptbytext":1, "adaptbywindow":1, "adaptbycustomer":1, "insertparagraph":1, "insertparagraphbeforetable":1, "averagedistributecol":1, "averagedistributerow":1 }; me.ready(function () { utils.cssRule('table', //选中的td上的样式 '.selectTdClass{background-color:#edf5fa !important}' + 'table.noBorderTable td,table.noBorderTable th,table.noBorderTable caption{border:1px dashed #ddd !important}' + //插入的表格的默认样式 'table{margin-bottom:10px;border-collapse:collapse;display:table;}' + 'td,th{padding: 5px 10px;border: 1px solid #DDD;}' + 'caption{border:1px dashed #DDD;border-bottom:0;padding:3px;text-align:center;}' + 'th{border-top:1px solid #BBB;background-color:#F7F7F7;}' + 'table tr.firstRow th{border-top-width:2px;}' + '.ue-table-interlace-color-single{ background-color: #fcfcfc; } .ue-table-interlace-color-double{ background-color: #f7faff; }' + 'td p{margin:0;padding:0;}', me.document); var tableCopyList, isFullCol, isFullRow; //注册del/backspace事件 me.addListener('keydown', function (cmd, evt) { var me = this; var keyCode = evt.keyCode || evt.which; if (keyCode == 8) { var ut = getUETableBySelected(me); if (ut && ut.selectedTds.length) { if (ut.isFullCol()) { me.execCommand('deletecol') } else if (ut.isFullRow()) { me.execCommand('deleterow') } else { me.fireEvent('delcells'); } domUtils.preventDefault(evt); } var caption = domUtils.findParentByTagName(me.selection.getStart(), 'caption', true), range = me.selection.getRange(); if (range.collapsed && caption && isEmptyBlock(caption)) { me.fireEvent('saveScene'); var table = caption.parentNode; domUtils.remove(caption); if (table) { range.setStart(table.rows[0].cells[0], 0).setCursor(false, true); } me.fireEvent('saveScene'); } } if (keyCode == 46) { ut = getUETableBySelected(me); if (ut) { me.fireEvent('saveScene'); for (var i = 0, ci; ci = ut.selectedTds[i++];) { domUtils.fillNode(me.document, ci) } me.fireEvent('saveScene'); domUtils.preventDefault(evt); } } if (keyCode == 13) { var rng = me.selection.getRange(), caption = domUtils.findParentByTagName(rng.startContainer, 'caption', true); if (caption) { var table = domUtils.findParentByTagName(caption, 'table'); if (!rng.collapsed) { rng.deleteContents(); me.fireEvent('saveScene'); } else { if (caption) { rng.setStart(table.rows[0].cells[0], 0).setCursor(false, true); } } domUtils.preventDefault(evt); return; } if (rng.collapsed) { var table = domUtils.findParentByTagName(rng.startContainer, 'table'); if (table) { var cell = table.rows[0].cells[0], start = domUtils.findParentByTagName(me.selection.getStart(), ['td', 'th'], true), preNode = table.previousSibling; if (cell === start && (!preNode || preNode.nodeType == 1 && preNode.tagName == 'TABLE' ) && domUtils.isStartInblock(rng)) { var first = domUtils.findParent(me.selection.getStart(), function(n){return domUtils.isBlockElm(n)}, true); if(first && ( /t(h|d)/i.test(first.tagName) || first === start.firstChild )){ me.execCommand('insertparagraphbeforetable'); domUtils.preventDefault(evt); } } } } } if ((evt.ctrlKey || evt.metaKey) && evt.keyCode == '67') { tableCopyList = null; var ut = getUETableBySelected(me); if (ut) { var tds = ut.selectedTds; isFullCol = ut.isFullCol(); isFullRow = ut.isFullRow(); tableCopyList = [ [ut.cloneCell(tds[0],null,true)] ]; for (var i = 1, ci; ci = tds[i]; i++) { if (ci.parentNode !== tds[i - 1].parentNode) { tableCopyList.push([ut.cloneCell(ci,null,true)]); } else { tableCopyList[tableCopyList.length - 1].push(ut.cloneCell(ci,null,true)); } } } } }); me.addListener("tablehasdeleted",function(){ toggleDraggableState(this, false, "", null); if (dragButton)domUtils.remove(dragButton); }); me.addListener('beforepaste', function (cmd, html) { var me = this; var rng = me.selection.getRange(); if (domUtils.findParentByTagName(rng.startContainer, 'caption', true)) { var div = me.document.createElement("div"); div.innerHTML = html.html; //trace:3729 html.html = div[browser.ie9below ? 'innerText' : 'textContent']; return; } var table = getUETableBySelected(me); if (tableCopyList) { me.fireEvent('saveScene'); var rng = me.selection.getRange(); var td = domUtils.findParentByTagName(rng.startContainer, ['td', 'th'], true), tmpNode, preNode; if (td) { var ut = getUETable(td); if (isFullRow) { var rowIndex = ut.getCellInfo(td).rowIndex; if (td.tagName == 'TH') { rowIndex++; } for (var i = 0, ci; ci = tableCopyList[i++];) { var tr = ut.insertRow(rowIndex++, "td"); for (var j = 0, cj; cj = ci[j]; j++) { var cell = tr.cells[j]; if (!cell) { cell = tr.insertCell(j) } cell.innerHTML = cj.innerHTML; cj.getAttribute('width') && cell.setAttribute('width', cj.getAttribute('width')); cj.getAttribute('vAlign') && cell.setAttribute('vAlign', cj.getAttribute('vAlign')); cj.getAttribute('align') && cell.setAttribute('align', cj.getAttribute('align')); cj.style.cssText && (cell.style.cssText = cj.style.cssText) } for (var j = 0, cj; cj = tr.cells[j]; j++) { if (!ci[j]) break; cj.innerHTML = ci[j].innerHTML; ci[j].getAttribute('width') && cj.setAttribute('width', ci[j].getAttribute('width')); ci[j].getAttribute('vAlign') && cj.setAttribute('vAlign', ci[j].getAttribute('vAlign')); ci[j].getAttribute('align') && cj.setAttribute('align', ci[j].getAttribute('align')); ci[j].style.cssText && (cj.style.cssText = ci[j].style.cssText) } } } else { if (isFullCol) { cellInfo = ut.getCellInfo(td); var maxColNum = 0; for (var j = 0, ci = tableCopyList[0], cj; cj = ci[j++];) { maxColNum += cj.colSpan || 1; } me.__hasEnterExecCommand = true; for (i = 0; i < maxColNum; i++) { me.execCommand('insertcol'); } me.__hasEnterExecCommand = false; td = ut.table.rows[0].cells[cellInfo.cellIndex]; if (td.tagName == 'TH') { td = ut.table.rows[1].cells[cellInfo.cellIndex]; } } for (var i = 0, ci; ci = tableCopyList[i++];) { tmpNode = td; for (var j = 0, cj; cj = ci[j++];) { if (td) { td.innerHTML = cj.innerHTML; //todo 定制处理 cj.getAttribute('width') && td.setAttribute('width', cj.getAttribute('width')); cj.getAttribute('vAlign') && td.setAttribute('vAlign', cj.getAttribute('vAlign')); cj.getAttribute('align') && td.setAttribute('align', cj.getAttribute('align')); cj.style.cssText && (td.style.cssText = cj.style.cssText); preNode = td; td = td.nextSibling; } else { var cloneTd = cj.cloneNode(true); domUtils.removeAttributes(cloneTd, ['class', 'rowSpan', 'colSpan']); preNode.parentNode.appendChild(cloneTd) } } td = ut.getNextCell(tmpNode, true, true); if (!tableCopyList[i]) break; if (!td) { var cellInfo = ut.getCellInfo(tmpNode); ut.table.insertRow(ut.table.rows.length); ut.update(); td = ut.getVSideCell(tmpNode, true); } } } ut.update(); } else { table = me.document.createElement('table'); for (var i = 0, ci; ci = tableCopyList[i++];) { var tr = table.insertRow(table.rows.length); for (var j = 0, cj; cj = ci[j++];) { cloneTd = UT.cloneCell(cj,null,true); domUtils.removeAttributes(cloneTd, ['class']); tr.appendChild(cloneTd) } if (j == 2 && cloneTd.rowSpan > 1) { cloneTd.rowSpan = 1; } } var defaultValue = getDefaultValue(me), width = me.body.offsetWidth - (needIEHack ? parseInt(domUtils.getComputedStyle(me.body, 'margin-left'), 10) * 2 : 0) - defaultValue.tableBorder * 2 - (me.options.offsetWidth || 0); me.execCommand('insertHTML', '' + table.innerHTML.replace(/>\s*<').replace(/\bth\b/gi, "td") + '
    ') } me.fireEvent('contentchange'); me.fireEvent('saveScene'); html.html = ''; return true; } else { var div = me.document.createElement("div"), tables; div.innerHTML = html.html; tables = div.getElementsByTagName("table"); if (domUtils.findParentByTagName(me.selection.getStart(), 'table')) { utils.each(tables, function (t) { domUtils.remove(t) }); if (domUtils.findParentByTagName(me.selection.getStart(), 'caption', true)) { div.innerHTML = div[browser.ie ? 'innerText' : 'textContent']; } } else { utils.each(tables, function (table) { removeStyleSize(table, true); domUtils.removeAttributes(table, ['style', 'border']); utils.each(domUtils.getElementsByTagName(table, "td"), function (td) { if (isEmptyBlock(td)) { domUtils.fillNode(me.document, td); } removeStyleSize(td, true); // domUtils.removeAttributes(td, ['style']) }); }); } html.html = div.innerHTML; } }); me.addListener('afterpaste', function () { utils.each(domUtils.getElementsByTagName(me.body, "table"), function (table) { if (table.offsetWidth > me.body.offsetWidth) { var defaultValue = getDefaultValue(me, table); table.style.width = me.body.offsetWidth - (needIEHack ? parseInt(domUtils.getComputedStyle(me.body, 'margin-left'), 10) * 2 : 0) - defaultValue.tableBorder * 2 - (me.options.offsetWidth || 0) + 'px' } }) }); me.addListener('blur', function () { tableCopyList = null; }); var timer; me.addListener('keydown', function () { clearTimeout(timer); timer = setTimeout(function () { var rng = me.selection.getRange(), cell = domUtils.findParentByTagName(rng.startContainer, ['th', 'td'], true); if (cell) { var table = cell.parentNode.parentNode.parentNode; if (table.offsetWidth > table.getAttribute("width")) { cell.style.wordBreak = "break-all"; } } }, 100); }); me.addListener("selectionchange", function () { toggleDraggableState(me, false, "", null); }); //内容变化时触发索引更新 //todo 可否考虑标记检测,如果不涉及表格的变化就不进行索引重建和更新 me.addListener("contentchange", function () { var me = this; //尽可能排除一些不需要更新的状况 hideDragLine(me); if (getUETableBySelected(me))return; var rng = me.selection.getRange(); var start = rng.startContainer; start = domUtils.findParentByTagName(start, ['td', 'th'], true); utils.each(domUtils.getElementsByTagName(me.document, 'table'), function (table) { if (me.fireEvent("excludetable", table) === true) return; table.ueTable = new UT(table); //trace:3742 // utils.each(domUtils.getElementsByTagName(me.document, 'td'), function (td) { // // if (domUtils.isEmptyBlock(td) && td !== start) { // domUtils.fillNode(me.document, td); // if (browser.ie && browser.version == 6) { // td.innerHTML = ' ' // } // } // }); // utils.each(domUtils.getElementsByTagName(me.document, 'th'), function (th) { // if (domUtils.isEmptyBlock(th) && th !== start) { // domUtils.fillNode(me.document, th); // if (browser.ie && browser.version == 6) { // th.innerHTML = ' ' // } // } // }); table.onmouseover = function () { me.fireEvent('tablemouseover', table); }; table.onmousemove = function () { me.fireEvent('tablemousemove', table); me.options.tableDragable && toggleDragButton(true, this, me); utils.defer(function(){ me.fireEvent('contentchange',50) },true) }; table.onmouseout = function () { me.fireEvent('tablemouseout', table); toggleDraggableState(me, false, "", null); hideDragLine(me); }; table.onclick = function (evt) { evt = me.window.event || evt; var target = getParentTdOrTh(evt.target || evt.srcElement); if (!target)return; var ut = getUETable(target), table = ut.table, cellInfo = ut.getCellInfo(target), cellsRange, rng = me.selection.getRange(); // if ("topLeft" == inPosition(table, mouseCoords(evt))) { // cellsRange = ut.getCellsRange(ut.table.rows[0].cells[0], ut.getLastCell()); // ut.setSelected(cellsRange); // return; // } // if ("bottomRight" == inPosition(table, mouseCoords(evt))) { // // return; // } if (inTableSide(table, target, evt, true)) { var endTdCol = ut.getCell(ut.indexTable[ut.rowsNum - 1][cellInfo.colIndex].rowIndex, ut.indexTable[ut.rowsNum - 1][cellInfo.colIndex].cellIndex); if (evt.shiftKey && ut.selectedTds.length) { if (ut.selectedTds[0] !== endTdCol) { cellsRange = ut.getCellsRange(ut.selectedTds[0], endTdCol); ut.setSelected(cellsRange); } else { rng && rng.selectNodeContents(endTdCol).select(); } } else { if (target !== endTdCol) { cellsRange = ut.getCellsRange(target, endTdCol); ut.setSelected(cellsRange); } else { rng && rng.selectNodeContents(endTdCol).select(); } } return; } if (inTableSide(table, target, evt)) { var endTdRow = ut.getCell(ut.indexTable[cellInfo.rowIndex][ut.colsNum - 1].rowIndex, ut.indexTable[cellInfo.rowIndex][ut.colsNum - 1].cellIndex); if (evt.shiftKey && ut.selectedTds.length) { if (ut.selectedTds[0] !== endTdRow) { cellsRange = ut.getCellsRange(ut.selectedTds[0], endTdRow); ut.setSelected(cellsRange); } else { rng && rng.selectNodeContents(endTdRow).select(); } } else { if (target !== endTdRow) { cellsRange = ut.getCellsRange(target, endTdRow); ut.setSelected(cellsRange); } else { rng && rng.selectNodeContents(endTdRow).select(); } } } }; }); switchBorderColor(me, true); }); domUtils.on(me.document, "mousemove", mouseMoveEvent); domUtils.on(me.document, "mouseout", function (evt) { var target = evt.target || evt.srcElement; if (target.tagName == "TABLE") { toggleDraggableState(me, false, "", null); } }); /** * 表格隔行变色 */ me.addListener("interlacetable",function(type,table,classList){ if(!table) return; var me = this, rows = table.rows, len = rows.length, getClass = function(list,index,repeat){ return list[index] ? list[index] : repeat ? list[index % list.length]: ""; }; for(var i = 0;i 1 ? currentRowIndex : ua.getCellInfo(cell).rowIndex; var nextCell = ua.getTabNextCell(cell, currentRowIndex); if (nextCell) { if (isEmptyBlock(nextCell)) { range.setStart(nextCell, 0).setCursor(false, true) } else { range.selectNodeContents(nextCell).select() } } else { me.fireEvent('saveScene'); me.__hasEnterExecCommand = true; this.execCommand('insertrownext'); me.__hasEnterExecCommand = false; range = this.selection.getRange(); range.setStart(table.rows[table.rows.length - 1].cells[0], 0).setCursor(); me.fireEvent('saveScene'); } } return true; } }); browser.ie && me.addListener('selectionchange', function () { toggleDraggableState(this, false, "", null); }); me.addListener("keydown", function (type, evt) { var me = this; //处理在表格的最后一个输入tab产生新的表格 var keyCode = evt.keyCode || evt.which; if (keyCode == 8 || keyCode == 46) { return; } var notCtrlKey = !evt.ctrlKey && !evt.metaKey && !evt.shiftKey && !evt.altKey; notCtrlKey && removeSelectedClass(domUtils.getElementsByTagName(me.body, "td")); var ut = getUETableBySelected(me); if (!ut) return; notCtrlKey && ut.clearSelected(); }); me.addListener("beforegetcontent", function () { switchBorderColor(this, false); browser.ie && utils.each(this.document.getElementsByTagName('caption'), function (ci) { if (domUtils.isEmptyNode(ci)) { ci.innerHTML = ' ' } }); }); me.addListener("aftergetcontent", function () { switchBorderColor(this, true); }); me.addListener("getAllHtml", function () { removeSelectedClass(me.document.getElementsByTagName("td")); }); //修正全屏状态下插入的表格宽度在非全屏状态下撑开编辑器的情况 me.addListener("fullscreenchanged", function (type, fullscreen) { if (!fullscreen) { var ratio = this.body.offsetWidth / document.body.offsetWidth, tables = domUtils.getElementsByTagName(this.body, "table"); utils.each(tables, function (table) { if (table.offsetWidth < me.body.offsetWidth) return false; var tds = domUtils.getElementsByTagName(table, "td"), backWidths = []; utils.each(tds, function (td) { backWidths.push(td.offsetWidth); }); for (var i = 0, td; td = tds[i]; i++) { td.setAttribute("width", Math.floor(backWidths[i] * ratio)); } table.setAttribute("width", Math.floor(getTableWidth(me, needIEHack, getDefaultValue(me)))) }); } }); //重写execCommand命令,用于处理框选时的处理 var oldExecCommand = me.execCommand; me.execCommand = function (cmd, datatat) { var me = this, args = arguments; cmd = cmd.toLowerCase(); var ut = getUETableBySelected(me), tds, range = new dom.Range(me.document), cmdFun = me.commands[cmd] || UE.commands[cmd], result; if (!cmdFun) return; if (ut && !commands[cmd] && !cmdFun.notNeedUndo && !me.__hasEnterExecCommand) { me.__hasEnterExecCommand = true; me.fireEvent("beforeexeccommand", cmd); tds = ut.selectedTds; var lastState = -2, lastValue = -2, value, state; for (var i = 0, td; td = tds[i]; i++) { if (isEmptyBlock(td)) { range.setStart(td, 0).setCursor(false, true) } else { range.selectNode(td).select(true); } state = me.queryCommandState(cmd); value = me.queryCommandValue(cmd); if (state != -1) { if (lastState !== state || lastValue !== value) { me._ignoreContentChange = true; result = oldExecCommand.apply(me, arguments); me._ignoreContentChange = false; } lastState = me.queryCommandState(cmd); lastValue = me.queryCommandValue(cmd); if (domUtils.isEmptyBlock(td)) { domUtils.fillNode(me.document, td) } } } range.setStart(tds[0], 0).shrinkBoundary(true).setCursor(false, true); me.fireEvent('contentchange'); me.fireEvent("afterexeccommand", cmd); me.__hasEnterExecCommand = false; me._selectionChange(); } else { result = oldExecCommand.apply(me, arguments); } return result; }; }); /** * 删除obj的宽高style,改成属性宽高 * @param obj * @param replaceToProperty */ function removeStyleSize(obj, replaceToProperty) { removeStyle(obj, "width", true); removeStyle(obj, "height", true); } function removeStyle(obj, styleName, replaceToProperty) { if (obj.style[styleName]) { replaceToProperty && obj.setAttribute(styleName, parseInt(obj.style[styleName], 10)); obj.style[styleName] = ""; } } function getParentTdOrTh(ele) { if (ele.tagName == "TD" || ele.tagName == "TH") return ele; var td; if (td = domUtils.findParentByTagName(ele, "td", true) || domUtils.findParentByTagName(ele, "th", true)) return td; return null; } function isEmptyBlock(node) { var reg = new RegExp(domUtils.fillChar, 'g'); if (node[browser.ie ? 'innerText' : 'textContent'].replace(/^\s*$/, '').replace(reg, '').length > 0) { return 0; } for (var n in dtd.$isNotEmpty) { if (node.getElementsByTagName(n).length) { return 0; } } return 1; } function mouseCoords(evt) { if (evt.pageX || evt.pageY) { return { x:evt.pageX, y:evt.pageY }; } return { x:evt.clientX + me.document.body.scrollLeft - me.document.body.clientLeft, y:evt.clientY + me.document.body.scrollTop - me.document.body.clientTop }; } function mouseMoveEvent(evt) { if( isEditorDisabled() ) { return; } try { //普通状态下鼠标移动 var target = getParentTdOrTh(evt.target || evt.srcElement), pos; //区分用户的行为是拖动还是双击 if( isInResizeBuffer ) { me.body.style.webkitUserSelect = 'none'; if( Math.abs( userActionStatus.x - evt.clientX ) > offsetOfTableCell || Math.abs( userActionStatus.y - evt.clientY ) > offsetOfTableCell ) { clearTableDragTimer(); isInResizeBuffer = false; singleClickState = 0; //drag action tableBorderDrag(evt); } } //修改单元格大小时的鼠标移动 if (onDrag && dragTd) { singleClickState = 0; me.body.style.webkitUserSelect = 'none'; me.selection.getNative()[browser.ie9below ? 'empty' : 'removeAllRanges'](); pos = mouseCoords(evt); toggleDraggableState(me, true, onDrag, pos, target); if (onDrag == "h") { dragLine.style.left = getPermissionX(dragTd, evt) + "px"; } else if (onDrag == "v") { dragLine.style.top = getPermissionY(dragTd, evt) + "px"; } return; } //当鼠标处于table上时,修改移动过程中的光标状态 if (target) { //针对使用table作为容器的组件不触发拖拽效果 if (me.fireEvent('excludetable', target) === true) return; pos = mouseCoords(evt); var state = getRelation(target, pos), table = domUtils.findParentByTagName(target, "table", true); if (inTableSide(table, target, evt, true)) { if (me.fireEvent("excludetable", table) === true) return; me.body.style.cursor = "url(" + me.options.cursorpath + "h.png),pointer"; } else if (inTableSide(table, target, evt)) { if (me.fireEvent("excludetable", table) === true) return; me.body.style.cursor = "url(" + me.options.cursorpath + "v.png),pointer"; } else { me.body.style.cursor = "text"; var curCell = target; if (/\d/.test(state)) { state = state.replace(/\d/, ''); target = getUETable(target).getPreviewCell(target, state == "v"); } //位于第一行的顶部或者第一列的左边时不可拖动 toggleDraggableState(me, target ? !!state : false, target ? state : '', pos, target); } } else { toggleDragButton(false, table, me); } } catch (e) { showError(e); } } var dragButtonTimer; function toggleDragButton(show, table, editor) { if (!show) { if (dragOver)return; dragButtonTimer = setTimeout(function () { !dragOver && dragButton && dragButton.parentNode && dragButton.parentNode.removeChild(dragButton); }, 2000); } else { createDragButton(table, editor); } } function createDragButton(table, editor) { var pos = domUtils.getXY(table), doc = table.ownerDocument; if (dragButton && dragButton.parentNode)return dragButton; dragButton = doc.createElement("div"); dragButton.contentEditable = false; dragButton.innerHTML = ""; dragButton.style.cssText = "width:15px;height:15px;background-image:url(" + editor.options.UEDITOR_HOME_URL + "dialogs/table/dragicon.png);position: absolute;cursor:move;top:" + (pos.y - 15) + "px;left:" + (pos.x) + "px;"; domUtils.unSelectable(dragButton); dragButton.onmouseover = function (evt) { dragOver = true; }; dragButton.onmouseout = function (evt) { dragOver = false; }; domUtils.on(dragButton, 'click', function (type, evt) { doClick(evt, this); }); domUtils.on(dragButton, 'dblclick', function (type, evt) { doDblClick(evt); }); domUtils.on(dragButton, 'dragstart', function (type, evt) { domUtils.preventDefault(evt); }); var timer; function doClick(evt, button) { // 部分浏览器下需要清理 clearTimeout(timer); timer = setTimeout(function () { editor.fireEvent("tableClicked", table, button); }, 300); } function doDblClick(evt) { clearTimeout(timer); var ut = getUETable(table), start = table.rows[0].cells[0], end = ut.getLastCell(), range = ut.getCellsRange(start, end); editor.selection.getRange().setStart(start, 0).setCursor(false, true); ut.setSelected(range); } doc.body.appendChild(dragButton); } // function inPosition(table, pos) { // var tablePos = domUtils.getXY(table), // width = table.offsetWidth, // height = table.offsetHeight; // if (pos.x - tablePos.x < 5 && pos.y - tablePos.y < 5) { // return "topLeft"; // } else if (tablePos.x + width - pos.x < 5 && tablePos.y + height - pos.y < 5) { // return "bottomRight"; // } // } function inTableSide(table, cell, evt, top) { var pos = mouseCoords(evt), state = getRelation(cell, pos); if (top) { var caption = table.getElementsByTagName("caption")[0], capHeight = caption ? caption.offsetHeight : 0; return (state == "v1") && ((pos.y - domUtils.getXY(table).y - capHeight) < 8); } else { return (state == "h1") && ((pos.x - domUtils.getXY(table).x) < 8); } } /** * 获取拖动时允许的X轴坐标 * @param dragTd * @param evt */ function getPermissionX(dragTd, evt) { var ut = getUETable(dragTd); if (ut) { var preTd = ut.getSameEndPosCells(dragTd, "x")[0], nextTd = ut.getSameStartPosXCells(dragTd)[0], mouseX = mouseCoords(evt).x, left = (preTd ? domUtils.getXY(preTd).x : domUtils.getXY(ut.table).x) + 20 , right = nextTd ? domUtils.getXY(nextTd).x + nextTd.offsetWidth - 20 : (me.body.offsetWidth + 5 || parseInt(domUtils.getComputedStyle(me.body, "width"), 10)); left += cellMinWidth; right -= cellMinWidth; return mouseX < left ? left : mouseX > right ? right : mouseX; } } /** * 获取拖动时允许的Y轴坐标 */ function getPermissionY(dragTd, evt) { try { var top = domUtils.getXY(dragTd).y, mousePosY = mouseCoords(evt).y; return mousePosY < top ? top : mousePosY; } catch (e) { showError(e); } } /** * 移动状态切换 */ function toggleDraggableState(editor, draggable, dir, mousePos, cell) { try { editor.body.style.cursor = dir == "h" ? "col-resize" : dir == "v" ? "row-resize" : "text"; if (browser.ie) { if (dir && !mousedown && !getUETableBySelected(editor)) { getDragLine(editor, editor.document); showDragLineAt(dir, cell); } else { hideDragLine(editor) } } onBorder = draggable; } catch (e) { showError(e); } } /** * 获取与UETable相关的resize line * @param uetable UETable对象 */ function getResizeLineByUETable() { var lineId = '_UETableResizeLine', line = this.document.getElementById( lineId ); if( !line ) { line = this.document.createElement("div"); line.id = lineId; line.contnetEditable = false; line.setAttribute("unselectable", "on"); var styles = { width: 2*cellBorderWidth + 1 + 'px', position: 'absolute', 'z-index': 100000, cursor: 'col-resize', background: 'red', display: 'none' }; //切换状态 line.onmouseout = function(){ this.style.display = 'none'; }; utils.extend( line.style, styles ); this.document.body.appendChild( line ); } return line; } /** * 更新resize-line */ function updateResizeLine( cell, uetable ) { var line = getResizeLineByUETable.call( this ), table = uetable.table, styles = { top: domUtils.getXY( table ).y + 'px', left: domUtils.getXY( cell).x + cell.offsetWidth - cellBorderWidth + 'px', display: 'block', height: table.offsetHeight + 'px' }; utils.extend( line.style, styles ); } /** * 显示resize-line */ function showResizeLine( cell ) { var uetable = getUETable( cell ); updateResizeLine.call( this, cell, uetable ); } /** * 获取鼠标与当前单元格的相对位置 * @param ele * @param mousePos */ function getRelation(ele, mousePos) { var elePos = domUtils.getXY(ele); if( !elePos ) { return ''; } if (elePos.x + ele.offsetWidth - mousePos.x < cellBorderWidth) { return "h"; } if (mousePos.x - elePos.x < cellBorderWidth) { return 'h1' } if (elePos.y + ele.offsetHeight - mousePos.y < cellBorderWidth) { return "v"; } if (mousePos.y - elePos.y < cellBorderWidth) { return 'v1' } return ''; } function mouseDownEvent(type, evt) { if( isEditorDisabled() ) { return ; } userActionStatus = { x: evt.clientX, y: evt.clientY }; //右键菜单单独处理 if (evt.button == 2) { var ut = getUETableBySelected(me), flag = false; if (ut) { var td = getTargetTd(me, evt); utils.each(ut.selectedTds, function (ti) { if (ti === td) { flag = true; } }); if (!flag) { removeSelectedClass(domUtils.getElementsByTagName(me.body, "th td")); ut.clearSelected() } else { td = ut.selectedTds[0]; setTimeout(function () { me.selection.getRange().setStart(td, 0).setCursor(false, true); }, 0); } } } else { tableClickHander( evt ); } } //清除表格的计时器 function clearTableTimer() { tabTimer && clearTimeout( tabTimer ); tabTimer = null; } //双击收缩 function tableDbclickHandler(evt) { singleClickState = 0; evt = evt || me.window.event; var target = getParentTdOrTh(evt.target || evt.srcElement); if (target) { var h; if (h = getRelation(target, mouseCoords(evt))) { hideDragLine( me ); if (h == 'h1') { h = 'h'; if (inTableSide(domUtils.findParentByTagName(target, "table"), target, evt)) { me.execCommand('adaptbywindow'); } else { target = getUETable(target).getPreviewCell(target); if (target) { var rng = me.selection.getRange(); rng.selectNodeContents(target).setCursor(true, true) } } } if (h == 'h') { var ut = getUETable(target), table = ut.table, cells = getCellsByMoveBorder( target, table, true ); cells = extractArray( cells, 'left' ); ut.width = ut.offsetWidth; var oldWidth = [], newWidth = []; utils.each( cells, function( cell ){ oldWidth.push( cell.offsetWidth ); } ); utils.each( cells, function( cell ){ cell.removeAttribute("width"); } ); window.setTimeout( function(){ //是否允许改变 var changeable = true; utils.each( cells, function( cell, index ){ var width = cell.offsetWidth; if( width > oldWidth[index] ) { changeable = false; return false; } newWidth.push( width ); } ); var change = changeable ? newWidth : oldWidth; utils.each( cells, function( cell, index ){ cell.width = change[index] - getTabcellSpace(); } ); }, 0 ); // minWidth -= cellMinWidth; // // table.removeAttribute("width"); // utils.each(cells, function (cell) { // cell.style.width = ""; // cell.width -= minWidth; // }); } } } } function tableClickHander( evt ) { removeSelectedClass(domUtils.getElementsByTagName(me.body, "td th")); //trace:3113 //选中单元格,点击table外部,不会清掉table上挂的ueTable,会引起getUETableBySelected方法返回值 utils.each(me.document.getElementsByTagName('table'), function (t) { t.ueTable = null; }); startTd = getTargetTd(me, evt); if( !startTd ) return; var table = domUtils.findParentByTagName(startTd, "table", true); ut = getUETable(table); ut && ut.clearSelected(); //判断当前鼠标状态 if (!onBorder) { me.document.body.style.webkitUserSelect = ''; mousedown = true; me.addListener('mouseover', mouseOverEvent); } else { //边框上的动作处理 borderActionHandler( evt ); } } //处理表格边框上的动作, 这里做延时处理,避免两种动作互相影响 function borderActionHandler( evt ) { if ( browser.ie ) { evt = reconstruct(evt ); } clearTableDragTimer(); //是否正在等待resize的缓冲中 isInResizeBuffer = true; tableDragTimer = setTimeout(function(){ tableBorderDrag( evt ); }, dblclickTime); } function extractArray( originArr, key ) { var result = [], tmp = null; for( var i = 0, len = originArr.length; i 0 && singleClickState--; }, dblclickTime ); if( singleClickState === 2 ) { singleClickState = 0; tableDbclickHandler(evt); return; } } if (evt.button == 2)return; var me = this; //清除表格上原生跨选问题 var range = me.selection.getRange(), start = domUtils.findParentByTagName(range.startContainer, 'table', true), end = domUtils.findParentByTagName(range.endContainer, 'table', true); if (start || end) { if (start === end) { start = domUtils.findParentByTagName(range.startContainer, ['td', 'th', 'caption'], true); end = domUtils.findParentByTagName(range.endContainer, ['td', 'th', 'caption'], true); if (start !== end) { me.selection.clearRange() } } else { me.selection.clearRange() } } mousedown = false; me.document.body.style.webkitUserSelect = ''; //拖拽状态下的mouseUP if ( onDrag && dragTd ) { me.selection.getNative()[browser.ie9below ? 'empty' : 'removeAllRanges'](); singleClickState = 0; dragLine = me.document.getElementById('ue_tableDragLine'); // trace 3973 if (dragLine) { var dragTdPos = domUtils.getXY(dragTd), dragLinePos = domUtils.getXY(dragLine); switch (onDrag) { case "h": changeColWidth(dragTd, dragLinePos.x - dragTdPos.x); break; case "v": changeRowHeight(dragTd, dragLinePos.y - dragTdPos.y - dragTd.offsetHeight); break; default: } onDrag = ""; dragTd = null; hideDragLine(me); me.fireEvent('saveScene'); return; } } //正常状态下的mouseup if (!startTd) { var target = domUtils.findParentByTagName(evt.target || evt.srcElement, "td", true); if (!target) target = domUtils.findParentByTagName(evt.target || evt.srcElement, "th", true); if (target && (target.tagName == "TD" || target.tagName == "TH")) { if (me.fireEvent("excludetable", target) === true) return; range = new dom.Range(me.document); range.setStart(target, 0).setCursor(false, true); } } else { var ut = getUETable(startTd), cell = ut ? ut.selectedTds[0] : null; if (cell) { range = new dom.Range(me.document); if (domUtils.isEmptyBlock(cell)) { range.setStart(cell, 0).setCursor(false, true); } else { range.selectNodeContents(cell).shrinkBoundary().setCursor(false, true); } } else { range = me.selection.getRange().shrinkBoundary(); if (!range.collapsed) { var start = domUtils.findParentByTagName(range.startContainer, ['td', 'th'], true), end = domUtils.findParentByTagName(range.endContainer, ['td', 'th'], true); //在table里边的不能清除 if (start && !end || !start && end || start && end && start !== end) { range.setCursor(false, true); } } } startTd = null; me.removeListener('mouseover', mouseOverEvent); } me._selectionChange(250, evt); } function mouseOverEvent(type, evt) { if( isEditorDisabled() ) { return; } var me = this, tar = evt.target || evt.srcElement; currentTd = domUtils.findParentByTagName(tar, "td", true) || domUtils.findParentByTagName(tar, "th", true); //需要判断两个TD是否位于同一个表格内 if (startTd && currentTd && ((startTd.tagName == "TD" && currentTd.tagName == "TD") || (startTd.tagName == "TH" && currentTd.tagName == "TH")) && domUtils.findParentByTagName(startTd, 'table') == domUtils.findParentByTagName(currentTd, 'table')) { var ut = getUETable(currentTd); if (startTd != currentTd) { me.document.body.style.webkitUserSelect = 'none'; me.selection.getNative()[browser.ie9below ? 'empty' : 'removeAllRanges'](); var range = ut.getCellsRange(startTd, currentTd); ut.setSelected(range); } else { me.document.body.style.webkitUserSelect = ''; ut.clearSelected(); } } evt.preventDefault ? evt.preventDefault() : (evt.returnValue = false); } function setCellHeight(cell, height, backHeight) { var lineHight = parseInt(domUtils.getComputedStyle(cell, "line-height"), 10), tmpHeight = backHeight + height; height = tmpHeight < lineHight ? lineHight : tmpHeight; if (cell.style.height) cell.style.height = ""; cell.rowSpan == 1 ? cell.setAttribute("height", height) : (cell.removeAttribute && cell.removeAttribute("height")); } function getWidth(cell) { if (!cell)return 0; return parseInt(domUtils.getComputedStyle(cell, "width"), 10); } function changeColWidth(cell, changeValue) { var ut = getUETable(cell); if (ut) { //根据当前移动的边框获取相关的单元格 var table = ut.table, cells = getCellsByMoveBorder( cell, table ); table.style.width = ""; table.removeAttribute("width"); //修正改变量 changeValue = correctChangeValue( changeValue, cell, cells ); if (cell.nextSibling) { var i=0; utils.each( cells, function( cellGroup ){ cellGroup.left.width = (+cellGroup.left.width)+changeValue; cellGroup.right && ( cellGroup.right.width = (+cellGroup.right.width)-changeValue ); } ); } else { utils.each( cells, function( cellGroup ){ cellGroup.left.width -= -changeValue; } ); } } } function isEditorDisabled() { return me.body.contentEditable === "false"; } function changeRowHeight(td, changeValue) { if (Math.abs(changeValue) < 10) return; var ut = getUETable(td); if (ut) { var cells = ut.getSameEndPosCells(td, "y"), //备份需要连带变化的td的原始高度,否则后期无法获取正确的值 backHeight = cells[0] ? cells[0].offsetHeight : 0; for (var i = 0, cell; cell = cells[i++];) { setCellHeight(cell, changeValue, backHeight); } } } /** * 获取调整单元格大小的相关单元格 * @isContainMergeCell 返回的结果中是否包含发生合并后的单元格 */ function getCellsByMoveBorder( cell, table, isContainMergeCell ) { if( !table ) { table = domUtils.findParentByTagName( cell, 'table' ); } if( !table ) { return null; } //获取到该单元格所在行的序列号 var index = domUtils.getNodeIndex( cell ), temp = cell, rows = table.rows, colIndex = 0; while( temp ) { //获取到当前单元格在未发生单元格合并时的序列 if( temp.nodeType === 1 ) { colIndex += (temp.colSpan || 1); } temp = temp.previousSibling; } temp = null; //记录想关的单元格 var borderCells = []; utils.each(rows, function( tabRow ){ var cells = tabRow.cells, currIndex = 0; utils.each( cells, function( tabCell ){ currIndex += (tabCell.colSpan || 1); if( currIndex === colIndex ) { borderCells.push({ left: tabCell, right: tabCell.nextSibling || null }); return false; } else if( currIndex > colIndex ) { if( isContainMergeCell ) { borderCells.push({ left: tabCell }); } return false; } } ); }); return borderCells; } /** * 通过给定的单元格集合获取最小的单元格width */ function getMinWidthByTableCells( cells ) { var minWidth = Number.MAX_VALUE; for( var i = 0, curCell; curCell = cells[ i ] ; i++ ) { minWidth = Math.min( minWidth, curCell.width || getTableCellWidth( curCell ) ); } return minWidth; } function correctChangeValue( changeValue, relatedCell, cells ) { //为单元格的paading预留空间 changeValue -= getTabcellSpace(); if( changeValue < 0 ) { return 0; } changeValue -= getTableCellWidth( relatedCell ); //确定方向 var direction = changeValue < 0 ? 'left':'right'; changeValue = Math.abs(changeValue); //只关心非最后一个单元格就可以 utils.each( cells, function( cellGroup ){ var curCell = cellGroup[direction]; //为单元格保留最小空间 if( curCell ) { changeValue = Math.min( changeValue, getTableCellWidth( curCell )-cellMinWidth ); } } ); //修正越界 changeValue = changeValue < 0 ? 0 : changeValue; return direction === 'left' ? -changeValue : changeValue; } function getTableCellWidth( cell ) { var width = 0, //偏移纠正量 offset = 0, width = cell.offsetWidth - getTabcellSpace(); //最后一个节点纠正一下 if( !cell.nextSibling ) { width -= getTableCellOffset( cell ); } width = width < 0 ? 0 : width; try { cell.width = width; } catch(e) { } return width; } /** * 获取单元格所在表格的最末单元格的偏移量 */ function getTableCellOffset( cell ) { tab = domUtils.findParentByTagName( cell, "table", false); if( tab.offsetVal === undefined ) { var prev = cell.previousSibling; if( prev ) { //最后一个单元格和前一个单元格的width diff结果 如果恰好为一个border width, 则条件成立 tab.offsetVal = cell.offsetWidth - prev.offsetWidth === UT.borderWidth ? UT.borderWidth : 0; } else { tab.offsetVal = 0; } } return tab.offsetVal; } function getTabcellSpace() { if( UT.tabcellSpace === undefined ) { var cell = null, tab = me.document.createElement("table"), tbody = me.document.createElement("tbody"), trow = me.document.createElement("tr"), tabcell = me.document.createElement("td"), mirror = null; tabcell.style.cssText = 'border: 0;'; tabcell.width = 1; trow.appendChild( tabcell ); trow.appendChild( mirror = tabcell.cloneNode( false ) ); tbody.appendChild( trow ); tab.appendChild( tbody ); tab.style.cssText = "visibility: hidden;"; me.body.appendChild( tab ); UT.paddingSpace = tabcell.offsetWidth - 1; var tmpTabWidth = tab.offsetWidth; tabcell.style.cssText = ''; mirror.style.cssText = ''; UT.borderWidth = ( tab.offsetWidth - tmpTabWidth ) / 3; UT.tabcellSpace = UT.paddingSpace + UT.borderWidth; me.body.removeChild( tab ); } getTabcellSpace = function(){ return UT.tabcellSpace; }; return UT.tabcellSpace; } function getDragLine(editor, doc) { if (mousedown)return; dragLine = editor.document.createElement("div"); domUtils.setAttributes(dragLine, { id:"ue_tableDragLine", unselectable:'on', contenteditable:false, 'onresizestart':'return false', 'ondragstart':'return false', 'onselectstart':'return false', style:"background-color:blue;position:absolute;padding:0;margin:0;background-image:none;border:0px none;opacity:0;filter:alpha(opacity=0)" }); editor.body.appendChild(dragLine); } function hideDragLine(editor) { if (mousedown)return; var line; while (line = editor.document.getElementById('ue_tableDragLine')) { domUtils.remove(line) } } /** * 依据state(v|h)在cell位置显示横线 * @param state * @param cell */ function showDragLineAt(state, cell) { if (!cell) return; var table = domUtils.findParentByTagName(cell, "table"), caption = table.getElementsByTagName('caption'), width = table.offsetWidth, height = table.offsetHeight - (caption.length > 0 ? caption[0].offsetHeight : 0), tablePos = domUtils.getXY(table), cellPos = domUtils.getXY(cell), css; switch (state) { case "h": css = 'height:' + height + 'px;top:' + (tablePos.y + (caption.length > 0 ? caption[0].offsetHeight : 0)) + 'px;left:' + (cellPos.x + cell.offsetWidth); dragLine.style.cssText = css + 'px;position: absolute;display:block;background-color:blue;width:1px;border:0; color:blue;opacity:.3;filter:alpha(opacity=30)'; break; case "v": css = 'width:' + width + 'px;left:' + tablePos.x + 'px;top:' + (cellPos.y + cell.offsetHeight ); //必须加上border:0和color:blue,否则低版ie不支持背景色显示 dragLine.style.cssText = css + 'px;overflow:hidden;position: absolute;display:block;background-color:blue;height:1px;border:0;color:blue;opacity:.2;filter:alpha(opacity=20)'; break; default: } } /** * 当表格边框颜色为白色时设置为虚线,true为添加虚线 * @param editor * @param flag */ function switchBorderColor(editor, flag) { var tableArr = domUtils.getElementsByTagName(editor.body, "table"), color; for (var i = 0, node; node = tableArr[i++];) { var td = domUtils.getElementsByTagName(node, "td"); if (td[0]) { if (flag) { color = (td[0].style.borderColor).replace(/\s/g, ""); if (/(#ffffff)|(rgb\(255,255,255\))/ig.test(color)) domUtils.addClass(node, "noBorderTable") } else { domUtils.removeClasses(node, "noBorderTable") } } } } function getTableWidth(editor, needIEHack, defaultValue) { var body = editor.body; return body.offsetWidth - (needIEHack ? parseInt(domUtils.getComputedStyle(body, 'margin-left'), 10) * 2 : 0) - defaultValue.tableBorder * 2 - (editor.options.offsetWidth || 0); } /** * 获取当前拖动的单元格 */ function getTargetTd(editor, evt) { var target = domUtils.findParentByTagName(evt.target || evt.srcElement, ["td", "th"], true), dir = null; if( !target ) { return null; } dir = getRelation( target, mouseCoords( evt ) ); //如果有前一个节点, 需要做一个修正, 否则可能会得到一个错误的td if( !target ) { return null; } if( dir === 'h1' && target.previousSibling ) { var position = domUtils.getXY( target), cellWidth = target.offsetWidth; if( Math.abs( position.x + cellWidth - evt.clientX ) > cellWidth / 3 ) { target = target.previousSibling; } } else if( dir === 'v1' && target.parentNode.previousSibling ) { var position = domUtils.getXY( target), cellHeight = target.offsetHeight; if( Math.abs( position.y + cellHeight - evt.clientY ) > cellHeight / 3 ) { target = target.parentNode.previousSibling.firstChild; } } //排除了非td内部以及用于代码高亮部分的td return target && !(editor.fireEvent("excludetable", target) === true) ? target : null; } }; // plugins/table.sort.js /** * Created with JetBrains PhpStorm. * User: Jinqn * Date: 13-10-12 * Time: 上午10:20 * To change this template use File | Settings | File Templates. */ UE.UETable.prototype.sortTable = function (sortByCellIndex, compareFn) { var table = this.table, rows = table.rows, trArray = [], flag = rows[0].cells[0].tagName === "TH", lastRowIndex = 0; if(this.selectedTds.length){ var range = this.cellsRange, len = range.endRowIndex + 1; for (var i = range.beginRowIndex; i < len; i++) { trArray[i] = rows[i]; } trArray.splice(0,range.beginRowIndex); lastRowIndex = (range.endRowIndex +1) === this.rowsNum ? 0 : range.endRowIndex +1; }else{ for (var i = 0,len = rows.length; i < len; i++) { trArray[i] = rows[i]; } } var Fn = { 'reversecurrent': function(td1,td2){ return 1; }, 'orderbyasc': function(td1,td2){ var value1 = td1.innerText||td1.textContent, value2 = td2.innerText||td2.textContent; return value1.localeCompare(value2); }, 'reversebyasc': function(td1,td2){ var value1 = td1.innerHTML, value2 = td2.innerHTML; return value2.localeCompare(value1); }, 'orderbynum': function(td1,td2){ var value1 = td1[browser.ie ? 'innerText':'textContent'].match(/\d+/), value2 = td2[browser.ie ? 'innerText':'textContent'].match(/\d+/); if(value1) value1 = +value1[0]; if(value2) value2 = +value2[0]; return (value1||0) - (value2||0); }, 'reversebynum': function(td1,td2){ var value1 = td1[browser.ie ? 'innerText':'textContent'].match(/\d+/), value2 = td2[browser.ie ? 'innerText':'textContent'].match(/\d+/); if(value1) value1 = +value1[0]; if(value2) value2 = +value2[0]; return (value2||0) - (value1||0); } }; //对表格设置排序的标记data-sort-type table.setAttribute('data-sort-type', compareFn && typeof compareFn === "string" && Fn[compareFn] ? compareFn:''); //th不参与排序 flag && trArray.splice(0, 1); trArray = utils.sort(trArray,function (tr1, tr2) { var result; if (compareFn && typeof compareFn === "function") { result = compareFn.call(this, tr1.cells[sortByCellIndex], tr2.cells[sortByCellIndex]); } else if (compareFn && typeof compareFn === "number") { result = 1; } else if (compareFn && typeof compareFn === "string" && Fn[compareFn]) { result = Fn[compareFn].call(this, tr1.cells[sortByCellIndex], tr2.cells[sortByCellIndex]); } else { result = Fn['orderbyasc'].call(this, tr1.cells[sortByCellIndex], tr2.cells[sortByCellIndex]); } return result; }); var fragment = table.ownerDocument.createDocumentFragment(); for (var j = 0, len = trArray.length; j < len; j++) { fragment.appendChild(trArray[j]); } var tbody = table.getElementsByTagName("tbody")[0]; if(!lastRowIndex){ tbody.appendChild(fragment); }else{ tbody.insertBefore(fragment,rows[lastRowIndex- range.endRowIndex + range.beginRowIndex - 1]) } }; UE.plugins['tablesort'] = function () { var me = this, UT = UE.UETable, getUETable = function (tdOrTable) { return UT.getUETable(tdOrTable); }, getTableItemsByRange = function (editor) { return UT.getTableItemsByRange(editor); }; me.ready(function () { //添加表格可排序的样式 utils.cssRule('tablesort', 'table.sortEnabled tr.firstRow th,table.sortEnabled tr.firstRow td{padding-right:20px;background-repeat: no-repeat;background-position: center right;' + ' background-image:url(' + me.options.themePath + me.options.theme + '/images/sortable.png);}', me.document); //做单元格合并操作时,清除可排序标识 me.addListener("afterexeccommand", function (type, cmd) { if( cmd == 'mergeright' || cmd == 'mergedown' || cmd == 'mergecells') { this.execCommand('disablesort'); } }); }); //表格排序 UE.commands['sorttable'] = { queryCommandState: function () { var me = this, tableItems = getTableItemsByRange(me); if (!tableItems.cell) return -1; var table = tableItems.table, cells = table.getElementsByTagName("td"); for (var i = 0, cell; cell = cells[i++];) { if (cell.rowSpan != 1 || cell.colSpan != 1) return -1; } return 0; }, execCommand: function (cmd, fn) { var me = this, range = me.selection.getRange(), bk = range.createBookmark(true), tableItems = getTableItemsByRange(me), cell = tableItems.cell, ut = getUETable(tableItems.table), cellInfo = ut.getCellInfo(cell); ut.sortTable(cellInfo.cellIndex, fn); range.moveToBookmark(bk); try{ range.select(); }catch(e){} } }; //设置表格可排序,清除表格可排序 UE.commands["enablesort"] = UE.commands["disablesort"] = { queryCommandState: function (cmd) { var table = getTableItemsByRange(this).table; if(table && cmd=='enablesort') { var cells = domUtils.getElementsByTagName(table, 'th td'); for(var i = 0; i1 || cells[i].getAttribute('rowspan')>1) return -1; } } return !table ? -1: cmd=='enablesort' ^ table.getAttribute('data-sort')!='sortEnabled' ? -1:0; }, execCommand: function (cmd) { var table = getTableItemsByRange(this).table; table.setAttribute("data-sort", cmd == "enablesort" ? "sortEnabled" : "sortDisabled"); cmd == "enablesort" ? domUtils.addClass(table,"sortEnabled"):domUtils.removeClasses(table,"sortEnabled"); } }; }; // plugins/contextmenu.js ///import core ///commands 右键菜单 ///commandsName ContextMenu ///commandsTitle 右键菜单 /** * 右键菜单 * @function * @name baidu.editor.plugins.contextmenu * @author zhanyi */ UE.plugins['contextmenu'] = function () { var me = this; me.setOpt('enableContextMenu',true); if(me.getOpt('enableContextMenu') === false){ return; } var lang = me.getLang( "contextMenu" ), menu, items = me.options.contextMenu || [ {label:lang['selectall'], cmdName:'selectall'}, { label:lang.cleardoc, cmdName:'cleardoc', exec:function () { if ( confirm( lang.confirmclear ) ) { this.execCommand( 'cleardoc' ); } } }, '-', { label:lang.unlink, cmdName:'unlink' }, '-', { group:lang.paragraph, icon:'justifyjustify', subMenu:[ { label:lang.justifyleft, cmdName:'justify', value:'left' }, { label:lang.justifyright, cmdName:'justify', value:'right' }, { label:lang.justifycenter, cmdName:'justify', value:'center' }, { label:lang.justifyjustify, cmdName:'justify', value:'justify' } ] }, '-', { group:lang.table, icon:'table', subMenu:[ { label:lang.inserttable, cmdName:'inserttable' }, { label:lang.deletetable, cmdName:'deletetable' }, '-', { label:lang.deleterow, cmdName:'deleterow' }, { label:lang.deletecol, cmdName:'deletecol' }, { label:lang.insertcol, cmdName:'insertcol' }, { label:lang.insertcolnext, cmdName:'insertcolnext' }, { label:lang.insertrow, cmdName:'insertrow' }, { label:lang.insertrownext, cmdName:'insertrownext' }, '-', { label:lang.insertcaption, cmdName:'insertcaption' }, { label:lang.deletecaption, cmdName:'deletecaption' }, { label:lang.inserttitle, cmdName:'inserttitle' }, { label:lang.deletetitle, cmdName:'deletetitle' }, { label:lang.inserttitlecol, cmdName:'inserttitlecol' }, { label:lang.deletetitlecol, cmdName:'deletetitlecol' }, '-', { label:lang.mergecells, cmdName:'mergecells' }, { label:lang.mergeright, cmdName:'mergeright' }, { label:lang.mergedown, cmdName:'mergedown' }, '-', { label:lang.splittorows, cmdName:'splittorows' }, { label:lang.splittocols, cmdName:'splittocols' }, { label:lang.splittocells, cmdName:'splittocells' }, '-', { label:lang.averageDiseRow, cmdName:'averagedistributerow' }, { label:lang.averageDisCol, cmdName:'averagedistributecol' }, '-', { label:lang.edittd, cmdName:'edittd', exec:function () { if ( UE.ui['edittd'] ) { new UE.ui['edittd']( this ); } this.getDialog('edittd').open(); } }, { label:lang.edittable, cmdName:'edittable', exec:function () { if ( UE.ui['edittable'] ) { new UE.ui['edittable']( this ); } this.getDialog('edittable').open(); } }, { label:lang.setbordervisible, cmdName:'setbordervisible' } ] }, { group:lang.tablesort, icon:'tablesort', subMenu:[ { label:lang.enablesort, cmdName:'enablesort' }, { label:lang.disablesort, cmdName:'disablesort' }, '-', { label:lang.reversecurrent, cmdName:'sorttable', value:'reversecurrent' }, { label:lang.orderbyasc, cmdName:'sorttable', value:'orderbyasc' }, { label:lang.reversebyasc, cmdName:'sorttable', value:'reversebyasc' }, { label:lang.orderbynum, cmdName:'sorttable', value:'orderbynum' }, { label:lang.reversebynum, cmdName:'sorttable', value:'reversebynum' } ] }, { group:lang.borderbk, icon:'borderBack', subMenu:[ { label:lang.setcolor, cmdName:"interlacetable", exec:function(){ this.execCommand("interlacetable"); } }, { label:lang.unsetcolor, cmdName:"uninterlacetable", exec:function(){ this.execCommand("uninterlacetable"); } }, { label:lang.setbackground, cmdName:"settablebackground", exec:function(){ this.execCommand("settablebackground",{repeat:true,colorList:["#bbb","#ccc"]}); } }, { label:lang.unsetbackground, cmdName:"cleartablebackground", exec:function(){ this.execCommand("cleartablebackground"); } }, { label:lang.redandblue, cmdName:"settablebackground", exec:function(){ this.execCommand("settablebackground",{repeat:true,colorList:["red","blue"]}); } }, { label:lang.threecolorgradient, cmdName:"settablebackground", exec:function(){ this.execCommand("settablebackground",{repeat:true,colorList:["#aaa","#bbb","#ccc"]}); } } ] }, { group:lang.aligntd, icon:'aligntd', subMenu:[ { cmdName:'cellalignment', value:{align:'left',vAlign:'top'} }, { cmdName:'cellalignment', value:{align:'center',vAlign:'top'} }, { cmdName:'cellalignment', value:{align:'right',vAlign:'top'} }, { cmdName:'cellalignment', value:{align:'left',vAlign:'middle'} }, { cmdName:'cellalignment', value:{align:'center',vAlign:'middle'} }, { cmdName:'cellalignment', value:{align:'right',vAlign:'middle'} }, { cmdName:'cellalignment', value:{align:'left',vAlign:'bottom'} }, { cmdName:'cellalignment', value:{align:'center',vAlign:'bottom'} }, { cmdName:'cellalignment', value:{align:'right',vAlign:'bottom'} } ] }, { group:lang.aligntable, icon:'aligntable', subMenu:[ { cmdName:'tablealignment', className: 'left', label:lang.tableleft, value:"left" }, { cmdName:'tablealignment', className: 'center', label:lang.tablecenter, value:"center" }, { cmdName:'tablealignment', className: 'right', label:lang.tableright, value:"right" } ] }, '-', { label:lang.insertparagraphbefore, cmdName:'insertparagraph', value:true }, { label:lang.insertparagraphafter, cmdName:'insertparagraph' }, { label:lang['copy'], cmdName:'copy' }, { label:lang['paste'], cmdName:'paste' } ]; if ( !items.length ) { return; } var uiUtils = UE.ui.uiUtils; me.addListener( 'contextmenu', function ( type, evt ) { var offset = uiUtils.getViewportOffsetByEvent( evt ); me.fireEvent( 'beforeselectionchange' ); if ( menu ) { menu.destroy(); } for ( var i = 0, ti, contextItems = []; ti = items[i]; i++ ) { var last; (function ( item ) { if ( item == '-' ) { if ( (last = contextItems[contextItems.length - 1 ] ) && last !== '-' ) { contextItems.push( '-' ); } } else if ( item.hasOwnProperty( "group" ) ) { for ( var j = 0, cj, subMenu = []; cj = item.subMenu[j]; j++ ) { (function ( subItem ) { if ( subItem == '-' ) { if ( (last = subMenu[subMenu.length - 1 ] ) && last !== '-' ) { subMenu.push( '-' ); }else{ subMenu.splice(subMenu.length-1); } } else { if ( (me.commands[subItem.cmdName] || UE.commands[subItem.cmdName] || subItem.query) && (subItem.query ? subItem.query() : me.queryCommandState( subItem.cmdName )) > -1 ) { subMenu.push( { 'label':subItem.label || me.getLang( "contextMenu." + subItem.cmdName + (subItem.value || '') )||"", 'className':'edui-for-' +subItem.cmdName + ( subItem.className ? ( ' edui-for-' + subItem.cmdName + '-' + subItem.className ) : '' ), onclick:subItem.exec ? function () { subItem.exec.call( me ); } : function () { me.execCommand( subItem.cmdName, subItem.value ); } } ); } } })( cj ); } if ( subMenu.length ) { function getLabel(){ switch (item.icon){ case "table": return me.getLang( "contextMenu.table" ); case "justifyjustify": return me.getLang( "contextMenu.paragraph" ); case "aligntd": return me.getLang("contextMenu.aligntd"); case "aligntable": return me.getLang("contextMenu.aligntable"); case "tablesort": return lang.tablesort; case "borderBack": return lang.borderbk; default : return ''; } } contextItems.push( { //todo 修正成自动获取方式 'label':getLabel(), className:'edui-for-' + item.icon, 'subMenu':{ items:subMenu, editor:me } } ); } } else { //有可能commmand没有加载右键不能出来,或者没有command也想能展示出来添加query方法 if ( (me.commands[item.cmdName] || UE.commands[item.cmdName] || item.query) && (item.query ? item.query.call(me) : me.queryCommandState( item.cmdName )) > -1 ) { contextItems.push( { 'label':item.label || me.getLang( "contextMenu." + item.cmdName ), className:'edui-for-' + (item.icon ? item.icon : item.cmdName + (item.value || '')), onclick:item.exec ? function () { item.exec.call( me ); } : function () { me.execCommand( item.cmdName, item.value ); } } ); } } })( ti ); } if ( contextItems[contextItems.length - 1] == '-' ) { contextItems.pop(); } menu = new UE.ui.Menu( { items:contextItems, className:"edui-contextmenu", editor:me } ); menu.render(); menu.showAt( offset ); me.fireEvent("aftershowcontextmenu",menu); domUtils.preventDefault( evt ); if ( browser.ie ) { var ieRange; try { ieRange = me.selection.getNative().createRange(); } catch ( e ) { return; } if ( ieRange.item ) { var range = new dom.Range( me.document ); range.selectNode( ieRange.item( 0 ) ).select( true, true ); } } }); // 添加复制的flash按钮 me.addListener('aftershowcontextmenu', function(type, menu) { if (me.zeroclipboard) { var items = menu.items; for (var key in items) { if (items[key].className == 'edui-for-copy') { me.zeroclipboard.clip(items[key].getDom()); } } } }); }; // plugins/shortcutmenu.js ///import core ///commands 弹出菜单 // commandsName popupmenu ///commandsTitle 弹出菜单 /** * 弹出菜单 * @function * @name baidu.editor.plugins.popupmenu * @author xuheng */ UE.plugins['shortcutmenu'] = function () { var me = this, menu, items = me.options.shortcutMenu || []; if (!items.length) { return; } me.addListener ('contextmenu mouseup' , function (type , e) { var me = this, customEvt = { type : type , target : e.target || e.srcElement , screenX : e.screenX , screenY : e.screenY , clientX : e.clientX , clientY : e.clientY }; setTimeout (function () { var rng = me.selection.getRange (); if (rng.collapsed === false || type == "contextmenu") { if (!menu) { menu = new baidu.editor.ui.ShortCutMenu ({ editor : me , items : items , theme : me.options.theme , className : 'edui-shortcutmenu' }); menu.render (); me.fireEvent ("afterrendershortcutmenu" , menu); } menu.show (customEvt , !!UE.plugins['contextmenu']); } }); if (type == 'contextmenu') { domUtils.preventDefault (e); if (browser.ie9below) { var ieRange; try { ieRange = me.selection.getNative().createRange(); } catch (e) { return; } if (ieRange.item) { var range = new dom.Range (me.document); range.selectNode (ieRange.item (0)).select (true , true); } } } }); me.addListener ('keydown' , function (type) { if (type == "keydown") { menu && !menu.isHidden && menu.hide (); } }); }; // plugins/basestyle.js /** * B、I、sub、super命令支持 * @file * @since 1.2.6.1 */ UE.plugins['basestyle'] = function(){ /** * 字体加粗 * @command bold * @param { String } cmd 命令字符串 * @remind 对已加粗的文本内容执行该命令, 将取消加粗 * @method execCommand * @example * ```javascript * //editor是编辑器实例 * //对当前选中的文本内容执行加粗操作 * //第一次执行, 文本内容加粗 * editor.execCommand( 'bold' ); * * //第二次执行, 文本内容取消加粗 * editor.execCommand( 'bold' ); * ``` */ /** * 字体倾斜 * @command italic * @method execCommand * @param { String } cmd 命令字符串 * @remind 对已倾斜的文本内容执行该命令, 将取消倾斜 * @example * ```javascript * //editor是编辑器实例 * //对当前选中的文本内容执行斜体操作 * //第一次操作, 文本内容将变成斜体 * editor.execCommand( 'italic' ); * * //再次对同一文本内容执行, 则文本内容将恢复正常 * editor.execCommand( 'italic' ); * ``` */ /** * 下标文本,与“superscript”命令互斥 * @command subscript * @method execCommand * @remind 把选中的文本内容切换成下标文本, 如果当前选中的文本已经是下标, 则该操作会把文本内容还原成正常文本 * @param { String } cmd 命令字符串 * @example * ```javascript * //editor是编辑器实例 * //对当前选中的文本内容执行下标操作 * //第一次操作, 文本内容将变成下标文本 * editor.execCommand( 'subscript' ); * * //再次对同一文本内容执行, 则文本内容将恢复正常 * editor.execCommand( 'subscript' ); * ``` */ /** * 上标文本,与“subscript”命令互斥 * @command superscript * @method execCommand * @remind 把选中的文本内容切换成上标文本, 如果当前选中的文本已经是上标, 则该操作会把文本内容还原成正常文本 * @param { String } cmd 命令字符串 * @example * ```javascript * //editor是编辑器实例 * //对当前选中的文本内容执行上标操作 * //第一次操作, 文本内容将变成上标文本 * editor.execCommand( 'superscript' ); * * //再次对同一文本内容执行, 则文本内容将恢复正常 * editor.execCommand( 'superscript' ); * ``` */ var basestyles = { 'bold':['strong','b'], 'italic':['em','i'], 'subscript':['sub'], 'superscript':['sup'] }, getObj = function(editor,tagNames){ return domUtils.filterNodeList(editor.selection.getStartElementPath(),tagNames); }, me = this; //添加快捷键 me.addshortcutkey({ "Bold" : "ctrl+66",//^B "Italic" : "ctrl+73", //^I "Underline" : "ctrl+85"//^U }); me.addInputRule(function(root){ utils.each(root.getNodesByTagName('b i'),function(node){ switch (node.tagName){ case 'b': node.tagName = 'strong'; break; case 'i': node.tagName = 'em'; } }); }); for ( var style in basestyles ) { (function( cmd, tagNames ) { me.commands[cmd] = { execCommand : function( cmdName ) { var range = me.selection.getRange(),obj = getObj(this,tagNames); if ( range.collapsed ) { if ( obj ) { var tmpText = me.document.createTextNode(''); range.insertNode( tmpText ).removeInlineStyle( tagNames ); range.setStartBefore(tmpText); domUtils.remove(tmpText); } else { var tmpNode = range.document.createElement( tagNames[0] ); if(cmdName == 'superscript' || cmdName == 'subscript'){ tmpText = me.document.createTextNode(''); range.insertNode(tmpText) .removeInlineStyle(['sub','sup']) .setStartBefore(tmpText) .collapse(true); } range.insertNode( tmpNode ).setStart( tmpNode, 0 ); } range.collapse( true ); } else { if(cmdName == 'superscript' || cmdName == 'subscript'){ if(!obj || obj.tagName.toLowerCase() != cmdName){ range.removeInlineStyle(['sub','sup']); } } obj ? range.removeInlineStyle( tagNames ) : range.applyInlineStyle( tagNames[0] ); } range.select(); }, queryCommandState : function() { return getObj(this,tagNames) ? 1 : 0; } }; })( style, basestyles[style] ); } }; // plugins/elementpath.js /** * 选取路径命令 * @file */ UE.plugins['elementpath'] = function(){ var currentLevel, tagNames, me = this; me.setOpt('elementPathEnabled',true); if(!me.options.elementPathEnabled){ return; } me.commands['elementpath'] = { execCommand : function( cmdName, level ) { var start = tagNames[level], range = me.selection.getRange(); currentLevel = level*1; range.selectNode(start).select(); }, queryCommandValue : function() { //产生一个副本,不能修改原来的startElementPath; var parents = [].concat(this.selection.getStartElementPath()).reverse(), names = []; tagNames = parents; for(var i=0,ci;ci=parents[i];i++){ if(ci.nodeType == 3) { continue; } var name = ci.tagName.toLowerCase(); if(name == 'img' && ci.getAttribute('anchorname')){ name = 'anchor'; } names[i] = name; if(currentLevel == i){ currentLevel = -1; break; } } return names; } }; }; // plugins/formatmatch.js /** * 格式刷,只格式inline的 * @file * @since 1.2.6.1 */ /** * 格式刷 * @command formatmatch * @method execCommand * @remind 该操作不能复制段落格式 * @param { String } cmd 命令字符串 * @example * ```javascript * //editor是编辑器实例 * //获取格式刷 * editor.execCommand( 'formatmatch' ); * ``` */ UE.plugins['formatmatch'] = function(){ var me = this, list = [],img, flag = 0; me.addListener('reset',function(){ list = []; flag = 0; }); function addList(type,evt){ if(browser.webkit){ var target = evt.target.tagName == 'IMG' ? evt.target : null; } function addFormat(range){ if(text){ range.selectNode(text); } return range.applyInlineStyle(list[list.length-1].tagName,null,list); } me.undoManger && me.undoManger.save(); var range = me.selection.getRange(), imgT = target || range.getClosedNode(); if(img && imgT && imgT.tagName == 'IMG'){ //trace:964 imgT.style.cssText += ';float:' + (img.style.cssFloat || img.style.styleFloat ||'none') + ';display:' + (img.style.display||'inline'); img = null; }else{ if(!img){ var collapsed = range.collapsed; if(collapsed){ var text = me.document.createTextNode('match'); range.insertNode(text).select(); } me.__hasEnterExecCommand = true; //不能把block上的属性干掉 //trace:1553 var removeFormatAttributes = me.options.removeFormatAttributes; me.options.removeFormatAttributes = ''; me.execCommand('removeformat'); me.options.removeFormatAttributes = removeFormatAttributes; me.__hasEnterExecCommand = false; //trace:969 range = me.selection.getRange(); if(list.length){ addFormat(range); } if(text){ range.setStartBefore(text).collapse(true); } range.select(); text && domUtils.remove(text); } } me.undoManger && me.undoManger.save(); me.removeListener('mouseup',addList); flag = 0; } me.commands['formatmatch'] = { execCommand : function( cmdName ) { if(flag){ flag = 0; list = []; me.removeListener('mouseup',addList); return; } var range = me.selection.getRange(); img = range.getClosedNode(); if(!img || img.tagName != 'IMG'){ range.collapse(true).shrinkBoundary(); var start = range.startContainer; list = domUtils.findParents(start,true,function(node){ return !domUtils.isBlockElm(node) && node.nodeType == 1; }); //a不能加入格式刷, 并且克隆节点 for(var i=0,ci;ci=list[i];i++){ if(ci.tagName == 'A'){ list.splice(i,1); break; } } } me.addListener('mouseup',addList); flag = 1; }, queryCommandState : function() { return flag; }, notNeedUndo : 1 }; }; // plugins/searchreplace.js ///import core ///commands 查找替换 ///commandsName SearchReplace ///commandsTitle 查询替换 ///commandsDialog dialogs\searchreplace /** * @description 查找替换 * @author zhanyi */ UE.plugin.register('searchreplace',function(){ var me = this; var _blockElm = {'table':1,'tbody':1,'tr':1,'ol':1,'ul':1}; function findTextInString(textContent,opt,currentIndex){ var str = opt.searchStr; if(opt.dir == -1){ textContent = textContent.split('').reverse().join(''); str = str.split('').reverse().join(''); currentIndex = textContent.length - currentIndex; } var reg = new RegExp(str,'g' + (opt.casesensitive ? '' : 'i')),match; while(match = reg.exec(textContent)){ if(match.index >= currentIndex){ return opt.dir == -1 ? textContent.length - match.index - opt.searchStr.length : match.index; } } return -1 } function findTextBlockElm(node,currentIndex,opt){ var textContent,index,methodName = opt.all || opt.dir == 1 ? 'getNextDomNode' : 'getPreDomNode'; if(domUtils.isBody(node)){ node = node.firstChild; } var first = 1; while(node){ textContent = node.nodeType == 3 ? node.nodeValue : node[browser.ie ? 'innerText' : 'textContent']; index = findTextInString(textContent,opt,currentIndex ); first = 0; if(index!=-1){ return { 'node':node, 'index':index } } node = domUtils[methodName](node); while(node && _blockElm[node.nodeName.toLowerCase()]){ node = domUtils[methodName](node,true); } if(node){ currentIndex = opt.dir == -1 ? (node.nodeType == 3 ? node.nodeValue : node[browser.ie ? 'innerText' : 'textContent']).length : 0; } } } function findNTextInBlockElm(node,index,str){ var currentIndex = 0, currentNode = node.firstChild, currentNodeLength = 0, result; while(currentNode){ if(currentNode.nodeType == 3){ currentNodeLength = currentNode.nodeValue.replace(/(^[\t\r\n]+)|([\t\r\n]+$)/,'').length; currentIndex += currentNodeLength; if(currentIndex >= index){ return { 'node':currentNode, 'index': currentNodeLength - (currentIndex - index) } } }else if(!dtd.$empty[currentNode.tagName]){ currentNodeLength = currentNode[browser.ie ? 'innerText' : 'textContent'].replace(/(^[\t\r\n]+)|([\t\r\n]+$)/,'').length currentIndex += currentNodeLength; if(currentIndex >= index){ result = findNTextInBlockElm(currentNode,currentNodeLength - (currentIndex - index),str); if(result){ return result; } } } currentNode = domUtils.getNextDomNode(currentNode); } } function searchReplace(me,opt){ var rng = me.selection.getRange(), startBlockNode, searchStr = opt.searchStr, span = me.document.createElement('span'); span.innerHTML = '$$ueditor_searchreplace_key$$'; rng.shrinkBoundary(true); //判断是不是第一次选中 if(!rng.collapsed){ rng.select(); var rngText = me.selection.getText(); if(new RegExp('^' + opt.searchStr + '$',(opt.casesensitive ? '' : 'i')).test(rngText)){ if(opt.replaceStr != undefined){ replaceText(rng,opt.replaceStr); rng.select(); return true; }else{ rng.collapse(opt.dir == -1) } } } rng.insertNode(span); rng.enlargeToBlockElm(true); startBlockNode = rng.startContainer; var currentIndex = startBlockNode[browser.ie ? 'innerText' : 'textContent'].indexOf('$$ueditor_searchreplace_key$$'); rng.setStartBefore(span); domUtils.remove(span); var result = findTextBlockElm(startBlockNode,currentIndex,opt); if(result){ var rngStart = findNTextInBlockElm(result.node,result.index,searchStr); var rngEnd = findNTextInBlockElm(result.node,result.index + searchStr.length,searchStr); rng.setStart(rngStart.node,rngStart.index).setEnd(rngEnd.node,rngEnd.index); if(opt.replaceStr !== undefined){ replaceText(rng,opt.replaceStr) } rng.select(); return true; }else{ rng.setCursor() } } function replaceText(rng,str){ str = me.document.createTextNode(str); rng.deleteContents().insertNode(str); } return { commands:{ 'searchreplace':{ execCommand:function(cmdName,opt){ utils.extend(opt,{ all : false, casesensitive : false, dir : 1 },true); var num = 0; if(opt.all){ var rng = me.selection.getRange(), first = me.body.firstChild; if(first && first.nodeType == 1){ rng.setStart(first,0); rng.shrinkBoundary(true); }else if(first.nodeType == 3){ rng.setStartBefore(first) } rng.collapse(true).select(true); if(opt.replaceStr !== undefined){ me.fireEvent('saveScene'); } while(searchReplace(this,opt)){ num++; } if(num){ me.fireEvent('saveScene'); } }else{ if(opt.replaceStr !== undefined){ me.fireEvent('saveScene'); } if(searchReplace(this,opt)){ num++ } if(num){ me.fireEvent('saveScene'); } } return num; }, notNeedUndo:1 } } } }); // plugins/customstyle.js /** * 自定义样式 * @file * @since 1.2.6.1 */ /** * 根据config配置文件里“customstyle”选项的值对匹配的标签执行样式替换。 * @command customstyle * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'customstyle' ); * ``` */ UE.plugins['customstyle'] = function() { var me = this; me.setOpt({ 'customstyle':[ {tag:'h1',name:'tc', style:'font-size:32px;font-weight:bold;border-bottom:#ccc 2px solid;padding:0 4px 0 0;text-align:center;margin:0 0 20px 0;'}, {tag:'h1',name:'tl', style:'font-size:32px;font-weight:bold;border-bottom:#ccc 2px solid;padding:0 4px 0 0;text-align:left;margin:0 0 10px 0;'}, {tag:'span',name:'im', style:'font-size:16px;font-style:italic;font-weight:bold;line-height:18px;'}, {tag:'span',name:'hi', style:'font-size:16px;font-style:italic;font-weight:bold;color:rgb(51, 153, 204);line-height:18px;'} ]}); me.commands['customstyle'] = { execCommand : function(cmdName, obj) { var me = this, tagName = obj.tag, node = domUtils.findParent(me.selection.getStart(), function(node) { return node.getAttribute('label'); }, true), range,bk,tmpObj = {}; for (var p in obj) { if(obj[p]!==undefined) tmpObj[p] = obj[p]; } delete tmpObj.tag; if (node && node.getAttribute('label') == obj.label) { range = this.selection.getRange(); bk = range.createBookmark(); if (range.collapsed) { //trace:1732 删掉自定义标签,要有p来回填站位 if(dtd.$block[node.tagName]){ var fillNode = me.document.createElement('p'); domUtils.moveChild(node, fillNode); node.parentNode.insertBefore(fillNode, node); domUtils.remove(node); }else{ domUtils.remove(node,true); } } else { var common = domUtils.getCommonAncestor(bk.start, bk.end), nodes = domUtils.getElementsByTagName(common, tagName); if(new RegExp(tagName,'i').test(common.tagName)){ nodes.push(common); } for (var i = 0,ni; ni = nodes[i++];) { if (ni.getAttribute('label') == obj.label) { var ps = domUtils.getPosition(ni, bk.start),pe = domUtils.getPosition(ni, bk.end); if ((ps & domUtils.POSITION_FOLLOWING || ps & domUtils.POSITION_CONTAINS) && (pe & domUtils.POSITION_PRECEDING || pe & domUtils.POSITION_CONTAINS) ) if (dtd.$block[tagName]) { var fillNode = me.document.createElement('p'); domUtils.moveChild(ni, fillNode); ni.parentNode.insertBefore(fillNode, ni); } domUtils.remove(ni, true); } } node = domUtils.findParent(common, function(node) { return node.getAttribute('label') == obj.label; }, true); if (node) { domUtils.remove(node, true); } } range.moveToBookmark(bk).select(); } else { if (dtd.$block[tagName]) { this.execCommand('paragraph', tagName, tmpObj,'customstyle'); range = me.selection.getRange(); if (!range.collapsed) { range.collapse(); node = domUtils.findParent(me.selection.getStart(), function(node) { return node.getAttribute('label') == obj.label; }, true); var pNode = me.document.createElement('p'); domUtils.insertAfter(node, pNode); domUtils.fillNode(me.document, pNode); range.setStart(pNode, 0).setCursor(); } } else { range = me.selection.getRange(); if (range.collapsed) { node = me.document.createElement(tagName); domUtils.setAttributes(node, tmpObj); range.insertNode(node).setStart(node, 0).setCursor(); return; } bk = range.createBookmark(); range.applyInlineStyle(tagName, tmpObj).moveToBookmark(bk).select(); } } }, queryCommandValue : function() { var parent = domUtils.filterNodeList( this.selection.getStartElementPath(), function(node){return node.getAttribute('label')} ); return parent ? parent.getAttribute('label') : ''; } }; //当去掉customstyle是,如果是块元素,用p代替 me.addListener('keyup', function(type, evt) { var keyCode = evt.keyCode || evt.which; if (keyCode == 32 || keyCode == 13) { var range = me.selection.getRange(); if (range.collapsed) { var node = domUtils.findParent(me.selection.getStart(), function(node) { return node.getAttribute('label'); }, true); if (node && dtd.$block[node.tagName] && domUtils.isEmptyNode(node)) { var p = me.document.createElement('p'); domUtils.insertAfter(node, p); domUtils.fillNode(me.document, p); domUtils.remove(node); range.setStart(p, 0).setCursor(); } } } }); }; // plugins/catchremoteimage.js ///import core ///commands 远程图片抓取 ///commandsName catchRemoteImage,catchremoteimageenable ///commandsTitle 远程图片抓取 /** * 远程图片抓取,当开启本插件时所有不符合本地域名的图片都将被抓取成为本地服务器上的图片 */ UE.plugins['catchremoteimage'] = function () { var me = this, ajax = UE.ajax; /* 设置默认值 */ if (me.options.catchRemoteImageEnable === false) return; me.setOpt({ catchRemoteImageEnable: false }); me.addListener("afterpaste", function () { me.fireEvent("catchRemoteImage"); }); me.addListener("catchRemoteImage", function () { var catcherLocalDomain = me.getOpt('catcherLocalDomain'), catcherActionUrl = me.getActionUrl(me.getOpt('catcherActionName')), catcherUrlPrefix = me.getOpt('catcherUrlPrefix'), catcherFieldName = me.getOpt('catcherFieldName'); var remoteImages = [], imgs = domUtils.getElementsByTagName(me.document, "img"), test = function (src, urls) { if (src.indexOf(location.host) != -1 || /(^\.)|(^\/)/.test(src)) { return true; } if (urls) { for (var j = 0, url; url = urls[j++];) { if (src.indexOf(url) !== -1) { return true; } } } return false; }; for (var i = 0, ci; ci = imgs[i++];) { if (ci.getAttribute("word_img")) { continue; } var src = ci.getAttribute("_src") || ci.src || ""; if (/^(https?|ftp):/i.test(src) && !test(src, catcherLocalDomain)) { remoteImages.push(src); } } if (remoteImages.length) { catchremoteimage(remoteImages, { //成功抓取 success: function (r) { try { var info = r.state !== undefined ? r:eval("(" + r.responseText + ")"); } catch (e) { return; } /* 获取源路径和新路径 */ var i, j, ci, cj, oldSrc, newSrc, list = info.list; for (i = 0; ci = imgs[i++];) { oldSrc = ci.getAttribute("_src") || ci.src || ""; for (j = 0; cj = list[j++];) { if (oldSrc == cj.source && cj.state == "SUCCESS") { //抓取失败时不做替换处理 newSrc = catcherUrlPrefix + cj.url; domUtils.setAttributes(ci, { "src": newSrc, "_src": newSrc }); break; } } } me.fireEvent('catchremotesuccess') }, //回调失败,本次请求超时 error: function () { me.fireEvent("catchremoteerror"); } }); } function catchremoteimage(imgs, callbacks) { var params = utils.serializeParam(me.queryCommandValue('serverparam')) || '', url = utils.formatUrl(catcherActionUrl + (catcherActionUrl.indexOf('?') == -1 ? '?':'&') + params), isJsonp = utils.isCrossDomainUrl(url), opt = { 'method': 'POST', 'dataType': isJsonp ? 'jsonp':'', 'timeout': 60000, //单位:毫秒,回调请求超时设置。目标用户如果网速不是很快的话此处建议设置一个较大的数值 'onsuccess': callbacks["success"], 'onerror': callbacks["error"] }; opt[catcherFieldName] = imgs; ajax.request(url, opt); } }); }; // plugins/snapscreen.js /** * 截屏插件,为UEditor提供插入支持 * @file * @since 1.4.2 */ UE.plugin.register('snapscreen', function (){ var me = this; var snapplugin; function getLocation(url){ var search, a = document.createElement('a'), params = utils.serializeParam(me.queryCommandValue('serverparam')) || ''; a.href = url; if (browser.ie) { a.href = a.href; } search = a.search; if (params) { search = search + (search.indexOf('?') == -1 ? '?':'&')+ params; search = search.replace(/[&]+/ig, '&'); } return { 'port': a.port, 'hostname': a.hostname, 'path': a.pathname + search || + a.hash } } return { commands:{ /** * 字体背景颜色 * @command snapscreen * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand('snapscreen'); * ``` */ 'snapscreen':{ execCommand:function (cmd) { var url, local, res; var lang = me.getLang("snapScreen_plugin"); if(!snapplugin){ var container = me.container; var doc = me.container.ownerDocument || me.container.document; snapplugin = doc.createElement("object"); try{snapplugin.type = "application/x-pluginbaidusnap";}catch(e){ return; } snapplugin.style.cssText = "position:absolute;left:-9999px;width:0;height:0;"; snapplugin.setAttribute("width","0"); snapplugin.setAttribute("height","0"); container.appendChild(snapplugin); } function onSuccess(rs){ try{ rs = eval("("+ rs +")"); if(rs.state == 'SUCCESS'){ var opt = me.options; me.execCommand('insertimage', { src: opt.snapscreenUrlPrefix + rs.url, _src: opt.snapscreenUrlPrefix + rs.url, alt: rs.title || '', floatStyle: opt.snapscreenImgAlign }); } else { alert(rs.state); } }catch(e){ alert(lang.callBackErrorMsg); } } url = me.getActionUrl(me.getOpt('snapscreenActionName')); local = getLocation(url); setTimeout(function () { try{ res =snapplugin.saveSnapshot(local.hostname, local.path, local.port); }catch(e){ me.ui._dialogs['snapscreenDialog'].open(); return; } onSuccess(res); }, 50); }, queryCommandState: function(){ return (navigator.userAgent.indexOf("Windows",0) != -1) ? 0:-1; } } } } }); // plugins/insertparagraph.js /** * 插入段落 * @file * @since 1.2.6.1 */ /** * 插入段落 * @command insertparagraph * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * //editor是编辑器实例 * editor.execCommand( 'insertparagraph' ); * ``` */ UE.commands['insertparagraph'] = { execCommand : function( cmdName,front) { var me = this, range = me.selection.getRange(), start = range.startContainer,tmpNode; while(start ){ if(domUtils.isBody(start)){ break; } tmpNode = start; start = start.parentNode; } if(tmpNode){ var p = me.document.createElement('p'); if(front){ tmpNode.parentNode.insertBefore(p,tmpNode) }else{ tmpNode.parentNode.insertBefore(p,tmpNode.nextSibling) } domUtils.fillNode(me.document,p); range.setStart(p,0).setCursor(false,true); } } }; // plugins/webapp.js /** * 百度应用 * @file * @since 1.2.6.1 */ /** * 插入百度应用 * @command webapp * @method execCommand * @remind 需要百度APPKey * @remind 百度应用主页: http://app.baidu.com/ * @param { Object } appOptions 应用所需的参数项, 支持的key有: title=>应用标题, width=>应用容器宽度, * height=>应用容器高度,logo=>应用logo,url=>应用地址 * @example * ```javascript * //editor是编辑器实例 * //在编辑器里插入一个“植物大战僵尸”的APP * editor.execCommand( 'webapp' , { * title: '植物大战僵尸', * width: 560, * height: 465, * logo: '应用展示的图片', * url: '百度应用的地址' * } ); * ``` */ //UE.plugins['webapp'] = function () { // var me = this; // function createInsertStr( obj, toIframe, addParagraph ) { // return !toIframe ? // (addParagraph ? '

    ' : '') + '' + // (addParagraph ? '

    ' : '') // : // ''; // } // // function switchImgAndIframe( img2frame ) { // var tmpdiv, // nodes = domUtils.getElementsByTagName( me.document, !img2frame ? "iframe" : "img" ); // for ( var i = 0, node; node = nodes[i++]; ) { // if ( node.className != "edui-faked-webapp" ){ // continue; // } // tmpdiv = me.document.createElement( "div" ); // tmpdiv.innerHTML = createInsertStr( img2frame ? {url:node.getAttribute( "_url" ), width:node.width, height:node.height,title:node.title,logo:node.style.backgroundImage.replace("url(","").replace(")","")} : {url:node.getAttribute( "src", 2 ),title:node.title, width:node.width, height:node.height,logo:node.getAttribute("logo_url")}, img2frame ? true : false,false ); // node.parentNode.replaceChild( tmpdiv.firstChild, node ); // } // } // // me.addListener( "beforegetcontent", function () { // switchImgAndIframe( true ); // } ); // me.addListener( 'aftersetcontent', function () { // switchImgAndIframe( false ); // } ); // me.addListener( 'aftergetcontent', function ( cmdName ) { // if ( cmdName == 'aftergetcontent' && me.queryCommandState( 'source' ) ){ // return; // } // switchImgAndIframe( false ); // } ); // // me.commands['webapp'] = { // execCommand:function ( cmd, obj ) { // me.execCommand( "inserthtml", createInsertStr( obj, false,true ) ); // } // }; //}; UE.plugin.register('webapp', function (){ var me = this; function createInsertStr(obj,toEmbed){ return !toEmbed ? '' : '' } return { outputRule: function(root){ utils.each(root.getNodesByTagName('img'),function(node){ var html; if(node.getAttr('class') == 'edui-faked-webapp'){ html = createInsertStr({ title:node.getAttr('title'), 'width':node.getAttr('width'), 'height':node.getAttr('height'), 'align':node.getAttr('align'), 'cssfloat':node.getStyle('float'), 'url':node.getAttr("_url"), 'logo':node.getAttr('_logo_url') },true); var embed = UE.uNode.createElement(html); node.parentNode.replaceChild(embed,node); } }) }, inputRule:function(root){ utils.each(root.getNodesByTagName('iframe'),function(node){ if(node.getAttr('class') == 'edui-faked-webapp'){ var img = UE.uNode.createElement(createInsertStr({ title:node.getAttr('title'), 'width':node.getAttr('width'), 'height':node.getAttr('height'), 'align':node.getAttr('align'), 'cssfloat':node.getStyle('float'), 'url':node.getAttr("src"), 'logo':node.getAttr('logo_url') })); node.parentNode.replaceChild(img,node); } }) }, commands:{ /** * 插入百度应用 * @command webapp * @method execCommand * @remind 需要百度APPKey * @remind 百度应用主页: http://app.baidu.com/ * @param { Object } appOptions 应用所需的参数项, 支持的key有: title=>应用标题, width=>应用容器宽度, * height=>应用容器高度,logo=>应用logo,url=>应用地址 * @example * ```javascript * //editor是编辑器实例 * //在编辑器里插入一个“植物大战僵尸”的APP * editor.execCommand( 'webapp' , { * title: '植物大战僵尸', * width: 560, * height: 465, * logo: '应用展示的图片', * url: '百度应用的地址' * } ); * ``` */ 'webapp':{ execCommand:function (cmd, obj) { var me = this, str = createInsertStr(utils.extend(obj,{ align:'none' }), false); me.execCommand("inserthtml",str); }, queryCommandState:function () { var me = this, img = me.selection.getRange().getClosedNode(), flag = img && (img.className == "edui-faked-webapp"); return flag ? 1 : 0; } } } } }); // plugins/template.js ///import core ///import plugins\inserthtml.js ///import plugins\cleardoc.js ///commands 模板 ///commandsName template ///commandsTitle 模板 ///commandsDialog dialogs\template UE.plugins['template'] = function () { UE.commands['template'] = { execCommand:function (cmd, obj) { obj.html && this.execCommand("inserthtml", obj.html); } }; this.addListener("click", function (type, evt) { var el = evt.target || evt.srcElement, range = this.selection.getRange(); var tnode = domUtils.findParent(el, function (node) { if (node.className && domUtils.hasClass(node, "ue_t")) { return node; } }, true); tnode && range.selectNode(tnode).shrinkBoundary().select(); }); this.addListener("keydown", function (type, evt) { var range = this.selection.getRange(); if (!range.collapsed) { if (!evt.ctrlKey && !evt.metaKey && !evt.shiftKey && !evt.altKey) { var tnode = domUtils.findParent(range.startContainer, function (node) { if (node.className && domUtils.hasClass(node, "ue_t")) { return node; } }, true); if (tnode) { domUtils.removeClasses(tnode, ["ue_t"]); } } } }); }; // plugins/music.js /** * 插入音乐命令 * @file */ UE.plugin.register('music', function (){ var me = this; function creatInsertStr(url,width,height,align,cssfloat,toEmbed){ return !toEmbed ? '' : ''; } return { outputRule: function(root){ utils.each(root.getNodesByTagName('img'),function(node){ var html; if(node.getAttr('class') == 'edui-faked-music'){ var cssfloat = node.getStyle('float'); var align = node.getAttr('align'); html = creatInsertStr(node.getAttr("_url"), node.getAttr('width'), node.getAttr('height'), align, cssfloat, true); var embed = UE.uNode.createElement(html); node.parentNode.replaceChild(embed,node); } }) }, inputRule:function(root){ utils.each(root.getNodesByTagName('embed'),function(node){ if(node.getAttr('class') == 'edui-faked-music'){ var cssfloat = node.getStyle('float'); var align = node.getAttr('align'); html = creatInsertStr(node.getAttr("src"), node.getAttr('width'), node.getAttr('height'), align, cssfloat,false); var img = UE.uNode.createElement(html); node.parentNode.replaceChild(img,node); } }) }, commands:{ /** * 插入音乐 * @command music * @method execCommand * @param { Object } musicOptions 插入音乐的参数项, 支持的key有: url=>音乐地址; * width=>音乐容器宽度;height=>音乐容器高度;align=>音乐文件的对齐方式, 可选值有: left, center, right, none * @example * ```javascript * //editor是编辑器实例 * //在编辑器里插入一个“植物大战僵尸”的APP * editor.execCommand( 'music' , { * width: 400, * height: 95, * align: "center", * url: "音乐地址" * } ); * ``` */ 'music':{ execCommand:function (cmd, musicObj) { var me = this, str = creatInsertStr(musicObj.url, musicObj.width || 400, musicObj.height || 95, "none", false); me.execCommand("inserthtml",str); }, queryCommandState:function () { var me = this, img = me.selection.getRange().getClosedNode(), flag = img && (img.className == "edui-faked-music"); return flag ? 1 : 0; } } } } }); // plugins/autoupload.js /** * @description * 1.拖放文件到编辑区域,自动上传并插入到选区 * 2.插入粘贴板的图片,自动上传并插入到选区 * @author Jinqn * @date 2013-10-14 */ UE.plugin.register('autoupload', function (){ function sendAndInsertFile(file, editor) { var me = editor; //模拟数据 var fieldName, urlPrefix, maxSize, allowFiles, actionUrl, loadingHtml, errorHandler, successHandler, filetype = /image\/\w+/i.test(file.type) ? 'image':'file', loadingId = 'loading_' + (+new Date()).toString(36); fieldName = me.getOpt(filetype + 'FieldName'); urlPrefix = me.getOpt(filetype + 'UrlPrefix'); maxSize = me.getOpt(filetype + 'MaxSize'); allowFiles = me.getOpt(filetype + 'AllowFiles'); actionUrl = me.getActionUrl(me.getOpt(filetype + 'ActionName')); errorHandler = function(title) { var loader = me.document.getElementById(loadingId); loader && domUtils.remove(loader); me.fireEvent('showmessage', { 'id': loadingId, 'content': title, 'type': 'error', 'timeout': 4000 }); }; if (filetype == 'image') { loadingHtml = ''; successHandler = function(data) { var link = urlPrefix + data.url, loader = me.document.getElementById(loadingId); if (loader) { loader.setAttribute('src', link); loader.setAttribute('_src', link); loader.setAttribute('title', data.title || ''); loader.setAttribute('alt', data.original || ''); loader.removeAttribute('id'); domUtils.removeClasses(loader, 'loadingclass'); } }; } else { loadingHtml = '

    ' + '' + '

    '; successHandler = function(data) { var link = urlPrefix + data.url, loader = me.document.getElementById(loadingId); var rng = me.selection.getRange(), bk = rng.createBookmark(); rng.selectNode(loader).select(); me.execCommand('insertfile', {'url': link}); rng.moveToBookmark(bk).select(); }; } /* 插入loading的占位符 */ me.execCommand('inserthtml', loadingHtml); /* 判断后端配置是否没有加载成功 */ if (!me.getOpt(filetype + 'ActionName')) { errorHandler(me.getLang('autoupload.errorLoadConfig')); return; } /* 判断文件大小是否超出限制 */ if(file.size > maxSize) { errorHandler(me.getLang('autoupload.exceedSizeError')); return; } /* 判断文件格式是否超出允许 */ var fileext = file.name ? file.name.substr(file.name.lastIndexOf('.')):''; if ((fileext && filetype != 'image') || (allowFiles && (allowFiles.join('') + '.').indexOf(fileext.toLowerCase() + '.') == -1)) { errorHandler(me.getLang('autoupload.exceedTypeError')); return; } /* 创建Ajax并提交 */ var xhr = new XMLHttpRequest(), fd = new FormData(), params = utils.serializeParam(me.queryCommandValue('serverparam')) || '', url = utils.formatUrl(actionUrl + (actionUrl.indexOf('?') == -1 ? '?':'&') + params); fd.append(fieldName, file, file.name || ('blob.' + file.type.substr('image/'.length))); fd.append('type', 'ajax'); xhr.open("post", url, true); xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); xhr.addEventListener('load', function (e) { try{ var json = (new Function("return " + utils.trim(e.target.response)))(); if (json.state == 'SUCCESS' && json.url) { successHandler(json); } else { errorHandler(json.state); } }catch(er){ errorHandler(me.getLang('autoupload.loadError')); } }); xhr.send(fd); } function getPasteImage(e){ return e.clipboardData && e.clipboardData.items && e.clipboardData.items.length == 1 && /^image\//.test(e.clipboardData.items[0].type) ? e.clipboardData.items:null; } function getDropImage(e){ return e.dataTransfer && e.dataTransfer.files ? e.dataTransfer.files:null; } return { outputRule: function(root){ utils.each(root.getNodesByTagName('img'),function(n){ if (/\b(loaderrorclass)|(bloaderrorclass)\b/.test(n.getAttr('class'))) { n.parentNode.removeChild(n); } }); utils.each(root.getNodesByTagName('p'),function(n){ if (/\bloadpara\b/.test(n.getAttr('class'))) { n.parentNode.removeChild(n); } }); }, bindEvents:{ //插入粘贴板的图片,拖放插入图片 'ready':function(e){ var me = this; if(window.FormData && window.FileReader) { domUtils.on(me.body, 'paste drop', function(e){ var hasImg = false, items; //获取粘贴板文件列表或者拖放文件列表 items = e.type == 'paste' ? getPasteImage(e):getDropImage(e); if(items){ var len = items.length, file; while (len--){ file = items[len]; if(file.getAsFile) file = file.getAsFile(); if(file && file.size > 0) { sendAndInsertFile(file, me); hasImg = true; } } hasImg && e.preventDefault(); } }); //取消拖放图片时出现的文字光标位置提示 domUtils.on(me.body, 'dragover', function (e) { if(e.dataTransfer.types[0] == 'Files') { e.preventDefault(); } }); //设置loading的样式 utils.cssRule('loading', '.loadingclass{display:inline-block;cursor:default;background: url(\'' + this.options.themePath + this.options.theme +'/images/loading.gif\') no-repeat center center transparent;border:1px solid #cccccc;margin-left:1px;height: 22px;width: 22px;}\n' + '.loaderrorclass{display:inline-block;cursor:default;background: url(\'' + this.options.themePath + this.options.theme +'/images/loaderror.png\') no-repeat center center transparent;border:1px solid #cccccc;margin-right:1px;height: 22px;width: 22px;' + '}', this.document); } } } } }); // plugins/autosave.js UE.plugin.register('autosave', function (){ var me = this, //无限循环保护 lastSaveTime = new Date(), //最小保存间隔时间 MIN_TIME = 20, //auto save key saveKey = null; function save ( editor ) { var saveData; if ( new Date() - lastSaveTime < MIN_TIME ) { return; } if ( !editor.hasContents() ) { //这里不能调用命令来删除, 会造成事件死循环 saveKey && me.removePreferences( saveKey ); return; } lastSaveTime = new Date(); editor._saveFlag = null; saveData = me.body.innerHTML; if ( editor.fireEvent( "beforeautosave", { content: saveData } ) === false ) { return; } me.setPreferences( saveKey, saveData ); editor.fireEvent( "afterautosave", { content: saveData } ); } return { defaultOptions: { //默认间隔时间 saveInterval: 500, enableAutoSave: true // HaoChuan9421 }, bindEvents:{ 'ready':function(){ var _suffix = "-drafts-data", key = null; if ( me.key ) { key = me.key + _suffix; } else { key = ( me.container.parentNode.id || 'ue-common' ) + _suffix; } //页面地址+编辑器ID 保持唯一 saveKey = ( location.protocol + location.host + location.pathname ).replace( /[.:\/]/g, '_' ) + key; }, 'contentchange': function () { // HaoChuan9421 if (!me.getOpt('enableAutoSave')) { return; } if ( !saveKey ) { return; } if ( me._saveFlag ) { window.clearTimeout( me._saveFlag ); } if ( me.options.saveInterval > 0 ) { me._saveFlag = window.setTimeout( function () { save( me ); }, me.options.saveInterval ); } else { save(me); } } }, commands:{ 'clearlocaldata':{ execCommand:function (cmd, name) { if ( saveKey && me.getPreferences( saveKey ) ) { me.removePreferences( saveKey ) } }, notNeedUndo: true, ignoreContentChange:true }, 'getlocaldata':{ execCommand:function (cmd, name) { return saveKey ? me.getPreferences( saveKey ) || '' : ''; }, notNeedUndo: true, ignoreContentChange:true }, 'drafts':{ execCommand:function (cmd, name) { if ( saveKey ) { me.body.innerHTML = me.getPreferences( saveKey ) || '

    '+domUtils.fillHtml+'

    '; me.focus(true); } }, queryCommandState: function () { return saveKey ? ( me.getPreferences( saveKey ) === null ? -1 : 0 ) : -1; }, notNeedUndo: true, ignoreContentChange:true } } } }); // plugins/charts.js UE.plugin.register('charts', function (){ var me = this; return { bindEvents: { 'chartserror': function () { } }, commands:{ 'charts': { execCommand: function ( cmd, data ) { var tableNode = domUtils.findParentByTagName(this.selection.getRange().startContainer, 'table', true), flagText = [], config = {}; if ( !tableNode ) { return false; } if ( !validData( tableNode ) ) { me.fireEvent( "chartserror" ); return false; } config.title = data.title || ''; config.subTitle = data.subTitle || ''; config.xTitle = data.xTitle || ''; config.yTitle = data.yTitle || ''; config.suffix = data.suffix || ''; config.tip = data.tip || ''; //数据对齐方式 config.dataFormat = data.tableDataFormat || ''; //图表类型 config.chartType = data.chartType || 0; for ( var key in config ) { if ( !config.hasOwnProperty( key ) ) { continue; } flagText.push( key+":"+config[ key ] ); } tableNode.setAttribute( "data-chart", flagText.join( ";" ) ); domUtils.addClass( tableNode, "edui-charts-table" ); }, queryCommandState: function ( cmd, name ) { var tableNode = domUtils.findParentByTagName(this.selection.getRange().startContainer, 'table', true); return tableNode && validData( tableNode ) ? 0 : -1; } } }, inputRule:function(root){ utils.each(root.getNodesByTagName('table'),function( tableNode ){ if ( tableNode.getAttr("data-chart") !== undefined ) { tableNode.setAttr("style"); } }) }, outputRule:function(root){ utils.each(root.getNodesByTagName('table'),function( tableNode ){ if ( tableNode.getAttr("data-chart") !== undefined ) { tableNode.setAttr("style", "display: none;"); } }) } } function validData ( table ) { var firstRows = null, cellCount = 0; //行数不够 if ( table.rows.length < 2 ) { return false; } //列数不够 if ( table.rows[0].cells.length < 2 ) { return false; } //第一行所有cell必须是th firstRows = table.rows[ 0 ].cells; cellCount = firstRows.length; for ( var i = 0, cell; cell = firstRows[ i ]; i++ ) { if ( cell.tagName.toLowerCase() !== 'th' ) { return false; } } for ( var i = 1, row; row = table.rows[ i ]; i++ ) { //每行单元格数不匹配, 返回false if ( row.cells.length != cellCount ) { return false; } //第一列不是th也返回false if ( row.cells[0].tagName.toLowerCase() !== 'th' ) { return false; } for ( var j = 1, cell; cell = row.cells[ j ]; j++ ) { var value = utils.trim( ( cell.innerText || cell.textContent || '' ) ); value = value.replace( new RegExp( UE.dom.domUtils.fillChar, 'g' ), '' ).replace( /^\s+|\s+$/g, '' ); //必须是数字 if ( !/^\d*\.?\d+$/.test( value ) ) { return false; } } } return true; } }); // plugins/section.js /** * 目录大纲支持插件 * @file * @since 1.3.0 */ UE.plugin.register('section', function (){ /* 目录节点对象 */ function Section(option){ this.tag = ''; this.level = -1, this.dom = null; this.nextSection = null; this.previousSection = null; this.parentSection = null; this.startAddress = []; this.endAddress = []; this.children = []; } function getSection(option) { var section = new Section(); return utils.extend(section, option); } function getNodeFromAddress(startAddress, root) { var current = root; for(var i = 0;i < startAddress.length; i++) { if(!current.childNodes) return null; current = current.childNodes[startAddress[i]]; } return current; } var me = this; return { bindMultiEvents:{ type: 'aftersetcontent afterscencerestore', handler: function(){ me.fireEvent('updateSections'); } }, bindEvents:{ /* 初始化、拖拽、粘贴、执行setcontent之后 */ 'ready': function (){ me.fireEvent('updateSections'); domUtils.on(me.body, 'drop paste', function(){ me.fireEvent('updateSections'); }); }, /* 执行paragraph命令之后 */ 'afterexeccommand': function (type, cmd) { if(cmd == 'paragraph') { me.fireEvent('updateSections'); } }, /* 部分键盘操作,触发updateSections事件 */ 'keyup': function (type, e) { var me = this, range = me.selection.getRange(); if(range.collapsed != true) { me.fireEvent('updateSections'); } else { var keyCode = e.keyCode || e.which; if(keyCode == 13 || keyCode == 8 || keyCode == 46) { me.fireEvent('updateSections'); } } } }, commands:{ 'getsections': { execCommand: function (cmd, levels) { var levelFn = levels || ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; for (var i = 0; i < levelFn.length; i++) { if (typeof levelFn[i] == 'string') { levelFn[i] = function(fn){ return function(node){ return node.tagName == fn.toUpperCase() }; }(levelFn[i]); } else if (typeof levelFn[i] != 'function') { levelFn[i] = function (node) { return null; } } } function getSectionLevel(node) { for (var i = 0; i < levelFn.length; i++) { if (levelFn[i](node)) return i; } return -1; } var me = this, Directory = getSection({'level':-1, 'title':'root'}), previous = Directory; function traversal(node, Directory) { var level, tmpSection = null, parent, child, children = node.childNodes; for (var i = 0, len = children.length; i < len; i++) { child = children[i]; level = getSectionLevel(child); if (level >= 0) { var address = me.selection.getRange().selectNode(child).createAddress(true).startAddress, current = getSection({ 'tag': child.tagName, 'title': child.innerText || child.textContent || '', 'level': level, 'dom': child, 'startAddress': utils.clone(address, []), 'endAddress': utils.clone(address, []), 'children': [] }); previous.nextSection = current; current.previousSection = previous; parent = previous; while(level <= parent.level){ parent = parent.parentSection; } current.parentSection = parent; parent.children.push(current); tmpSection = previous = current; } else { child.nodeType === 1 && traversal(child, Directory); tmpSection && tmpSection.endAddress[tmpSection.endAddress.length - 1] ++; } } } traversal(me.body, Directory); return Directory; }, notNeedUndo: true }, 'movesection': { execCommand: function (cmd, sourceSection, targetSection, isAfter) { var me = this, targetAddress, target; if(!sourceSection || !targetSection || targetSection.level == -1) return; targetAddress = isAfter ? targetSection.endAddress:targetSection.startAddress; target = getNodeFromAddress(targetAddress, me.body); /* 判断目标地址是否被源章节包含 */ if(!targetAddress || !target || isContainsAddress(sourceSection.startAddress, sourceSection.endAddress, targetAddress)) return; var startNode = getNodeFromAddress(sourceSection.startAddress, me.body), endNode = getNodeFromAddress(sourceSection.endAddress, me.body), current, nextNode; if(isAfter) { current = endNode; while ( current && !(domUtils.getPosition( startNode, current ) & domUtils.POSITION_FOLLOWING) ) { nextNode = current.previousSibling; domUtils.insertAfter(target, current); if(current == startNode) break; current = nextNode; } } else { current = startNode; while ( current && !(domUtils.getPosition( current, endNode ) & domUtils.POSITION_FOLLOWING) ) { nextNode = current.nextSibling; target.parentNode.insertBefore(current, target); if(current == endNode) break; current = nextNode; } } me.fireEvent('updateSections'); /* 获取地址的包含关系 */ function isContainsAddress(startAddress, endAddress, addressTarget){ var isAfterStartAddress = false, isBeforeEndAddress = false; for(var i = 0; i< startAddress.length; i++){ if(i >= addressTarget.length) break; if(addressTarget[i] > startAddress[i]) { isAfterStartAddress = true; break; } else if(addressTarget[i] < startAddress[i]) { break; } } for(var i = 0; i< endAddress.length; i++){ if(i >= addressTarget.length) break; if(addressTarget[i] < startAddress[i]) { isBeforeEndAddress = true; break; } else if(addressTarget[i] > startAddress[i]) { break; } } return isAfterStartAddress && isBeforeEndAddress; } } }, 'deletesection': { execCommand: function (cmd, section, keepChildren) { var me = this; if(!section) return; function getNodeFromAddress(startAddress) { var current = me.body; for(var i = 0;i < startAddress.length; i++) { if(!current.childNodes) return null; current = current.childNodes[startAddress[i]]; } return current; } var startNode = getNodeFromAddress(section.startAddress), endNode = getNodeFromAddress(section.endAddress), current = startNode, nextNode; if(!keepChildren) { while ( current && domUtils.inDoc(endNode, me.document) && !(domUtils.getPosition( current, endNode ) & domUtils.POSITION_FOLLOWING) ) { nextNode = current.nextSibling; domUtils.remove(current); current = nextNode; } } else { domUtils.remove(current); } me.fireEvent('updateSections'); } }, 'selectsection': { execCommand: function (cmd, section) { if(!section && !section.dom) return false; var me = this, range = me.selection.getRange(), address = { 'startAddress':utils.clone(section.startAddress, []), 'endAddress':utils.clone(section.endAddress, []) }; address.endAddress[address.endAddress.length - 1]++; range.moveToAddress(address).select().scrollToView(); return true; }, notNeedUndo: true }, 'scrolltosection': { execCommand: function (cmd, section) { if(!section && !section.dom) return false; var me = this, range = me.selection.getRange(), address = { 'startAddress':section.startAddress, 'endAddress':section.endAddress }; address.endAddress[address.endAddress.length - 1]++; range.moveToAddress(address).scrollToView(); return true; }, notNeedUndo: true } } } }); // plugins/simpleupload.js /** * @description * 简单上传:点击按钮,直接选择文件上传。 * 原 UEditor 作者使用了 form 表单 + iframe 的方式上传 * 但由于同源策略的限制,父页面无法访问跨域的 iframe 内容 * 导致无法获取接口返回的数据,使得单图上传无法在跨域的情况下使用 * 这里改为普通的XHR上传,兼容到IE10+ * @author HaoChuan9421 * @date 2018-12-20 */ UE.plugin.register('simpleupload', function() { var me = this, containerBtn, timestrap = (+new Date()).toString(36); function initUploadBtn() { var w = containerBtn.offsetWidth || 20, h = containerBtn.offsetHeight || 20, btnStyle = 'display:block;width:' + w + 'px;height:' + h + 'px;overflow:hidden;border:0;margin:0;padding:0;position:absolute;top:0;left:0;filter:alpha(opacity=0);-moz-opacity:0;-khtml-opacity: 0;opacity: 0;cursor:pointer;'; var form = document.createElement('form'); var input = document.createElement('input'); form.id = 'edui_form_' + timestrap; form.enctype = 'multipart/form-data'; form.style = btnStyle; input.id = 'edui_input_' + timestrap; input.type = 'file' input.accept = 'image/*'; input.name = me.options.imageFieldName; input.style = btnStyle; form.appendChild(input); containerBtn.appendChild(form); input.addEventListener('change', function(event) { if (!input.value) return; var loadingId = 'loading_' + (+new Date()).toString(36); var imageActionUrl = me.getActionUrl(me.getOpt('imageActionName')); var params = utils.serializeParam(me.queryCommandValue('serverparam')) || ''; var action = utils.formatUrl(imageActionUrl + (imageActionUrl.indexOf('?') == -1 ? '?' : '&') + params); var allowFiles = me.getOpt('imageAllowFiles'); me.focus(); me.execCommand('inserthtml', ''); function showErrorLoader(title) { if (loadingId) { var loader = me.document.getElementById(loadingId); loader && domUtils.remove(loader); me.fireEvent('showmessage', { 'id': loadingId, 'content': title, 'type': 'error', 'timeout': 4000 }); } } /* 判断后端配置是否没有加载成功 */ if (!me.getOpt('imageActionName')) { showErrorLoader(me.getLang('autoupload.errorLoadConfig')); return; } // 判断文件格式是否错误 var filename = input.value, fileext = filename ? filename.substr(filename.lastIndexOf('.')) : ''; if (!fileext || (allowFiles && (allowFiles.join('') + '.').indexOf(fileext.toLowerCase() + '.') == -1)) { showErrorLoader(me.getLang('simpleupload.exceedTypeError')); return; } var xhr = new XMLHttpRequest() xhr.open('post', action, true) if (me.options.headers && Object.prototype.toString.apply(me.options.headers) === "[object Object]") { for (var key in me.options.headers) { xhr.setRequestHeader(key, me.options.headers[key]) } } xhr.onload = function() { if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) { var res = JSON.parse(xhr.responseText) var link = me.options.imageUrlPrefix + res.url; if (res.state == 'SUCCESS' && res.url) { loader = me.document.getElementById(loadingId); loader.setAttribute('src', link); loader.setAttribute('_src', link); loader.setAttribute('title', res.title || ''); loader.setAttribute('alt', res.original || ''); loader.removeAttribute('id'); domUtils.removeClasses(loader, 'loadingclass'); me.fireEvent("contentchange"); } else { showErrorLoader(res.state); } } else { showErrorLoader(me.getLang('simpleupload.loadError')); } }; xhr.onerror = function() { showErrorLoader(me.getLang('simpleupload.loadError')); }; xhr.send(new FormData(form)); form.reset(); }) } return { bindEvents: { 'ready': function() { //设置loading的样式 utils.cssRule('loading', '.loadingclass{display:inline-block;cursor:default;background: url(\'' + this.options.themePath + this.options.theme + '/images/loading.gif\') no-repeat center center transparent;border:1px solid #cccccc;margin-right:1px;height: 22px;width: 22px;}\n' + '.loaderrorclass{display:inline-block;cursor:default;background: url(\'' + this.options.themePath + this.options.theme + '/images/loaderror.png\') no-repeat center center transparent;border:1px solid #cccccc;margin-right:1px;height: 22px;width: 22px;' + '}', this.document); }, /* 初始化简单上传按钮 */ 'simpleuploadbtnready': function(type, container) { containerBtn = container; me.afterConfigReady(initUploadBtn); } }, outputRule: function(root) { utils.each(root.getNodesByTagName('img'), function(n) { if (/\b(loaderrorclass)|(bloaderrorclass)\b/.test(n.getAttr('class'))) { n.parentNode.removeChild(n); } }); } } }); // plugins/serverparam.js /** * 服务器提交的额外参数列表设置插件 * @file * @since 1.2.6.1 */ UE.plugin.register('serverparam', function (){ var me = this, serverParam = {}; return { commands:{ /** * 修改服务器提交的额外参数列表,清除所有项 * @command serverparam * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand('serverparam'); * editor.queryCommandValue('serverparam'); //返回空 * ``` */ /** * 修改服务器提交的额外参数列表,删除指定项 * @command serverparam * @method execCommand * @param { String } cmd 命令字符串 * @param { String } key 要清除的属性 * @example * ```javascript * editor.execCommand('serverparam', 'name'); //删除属性name * ``` */ /** * 修改服务器提交的额外参数列表,使用键值添加项 * @command serverparam * @method execCommand * @param { String } cmd 命令字符串 * @param { String } key 要添加的属性 * @param { String } value 要添加属性的值 * @example * ```javascript * editor.execCommand('serverparam', 'name', 'hello'); * editor.queryCommandValue('serverparam'); //返回对象 {'name': 'hello'} * ``` */ /** * 修改服务器提交的额外参数列表,传入键值对对象添加多项 * @command serverparam * @method execCommand * @param { String } cmd 命令字符串 * @param { Object } key 传入的键值对对象 * @example * ```javascript * editor.execCommand('serverparam', {'name': 'hello'}); * editor.queryCommandValue('serverparam'); //返回对象 {'name': 'hello'} * ``` */ /** * 修改服务器提交的额外参数列表,使用自定义函数添加多项 * @command serverparam * @method execCommand * @param { String } cmd 命令字符串 * @param { Function } key 自定义获取参数的函数 * @example * ```javascript * editor.execCommand('serverparam', function(editor){ * return {'key': 'value'}; * }); * editor.queryCommandValue('serverparam'); //返回对象 {'key': 'value'} * ``` */ /** * 获取服务器提交的额外参数列表 * @command serverparam * @method queryCommandValue * @param { String } cmd 命令字符串 * @example * ```javascript * editor.queryCommandValue( 'serverparam' ); //返回对象 {'key': 'value'} * ``` */ 'serverparam':{ execCommand:function (cmd, key, value) { if (key === undefined || key === null) { //不传参数,清空列表 serverParam = {}; } else if (utils.isString(key)) { //传入键值 if(value === undefined || value === null) { delete serverParam[key]; } else { serverParam[key] = value; } } else if (utils.isObject(key)) { //传入对象,覆盖列表项 utils.extend(serverParam, key, true); } else if (utils.isFunction(key)){ //传入函数,添加列表项 utils.extend(serverParam, key(), true); } }, queryCommandValue: function(){ return serverParam || {}; } } } } }); // plugins/insertfile.js /** * 插入附件 */ UE.plugin.register('insertfile', function (){ var me = this; function getFileIcon(url){ var ext = url.substr(url.lastIndexOf('.') + 1).toLowerCase(), maps = { "rar":"icon_rar.gif", "zip":"icon_rar.gif", "tar":"icon_rar.gif", "gz":"icon_rar.gif", "bz2":"icon_rar.gif", "doc":"icon_doc.gif", "docx":"icon_doc.gif", "pdf":"icon_pdf.gif", "mp3":"icon_mp3.gif", "xls":"icon_xls.gif", "chm":"icon_chm.gif", "ppt":"icon_ppt.gif", "pptx":"icon_ppt.gif", "avi":"icon_mv.gif", "rmvb":"icon_mv.gif", "wmv":"icon_mv.gif", "flv":"icon_mv.gif", "swf":"icon_mv.gif", "rm":"icon_mv.gif", "exe":"icon_exe.gif", "psd":"icon_psd.gif", "txt":"icon_txt.gif", "jpg":"icon_jpg.gif", "png":"icon_jpg.gif", "jpeg":"icon_jpg.gif", "gif":"icon_jpg.gif", "ico":"icon_jpg.gif", "bmp":"icon_jpg.gif" }; return maps[ext] ? maps[ext]:maps['txt']; } return { commands:{ 'insertfile': { execCommand: function (command, filelist){ filelist = utils.isArray(filelist) ? filelist : [filelist]; var i, item, icon, title, html = '', URL = me.getOpt('UEDITOR_HOME_URL'), iconDir = URL + (URL.substr(URL.length - 1) == '/' ? '':'/') + 'dialogs/attachment/fileTypeImages/'; for (i = 0; i < filelist.length; i++) { item = filelist[i]; icon = iconDir + getFileIcon(item.url); title = item.title || item.url.substr(item.url.lastIndexOf('/') + 1); html += '

    ' + '' + '' + title + '' + '

    '; } me.execCommand('insertHtml', html); } } } } }); // plugins/xssFilter.js /** * @file xssFilter.js * @desc xss过滤器 * @author robbenmu */ UE.plugins.xssFilter = function() { var config = UEDITOR_CONFIG; var whitList = config.whitList; function filter(node) { var tagName = node.tagName; var attrs = node.attrs; if (!whitList.hasOwnProperty(tagName)) { node.parentNode.removeChild(node); return false; } UE.utils.each(attrs, function (val, key) { if (whitList[tagName].indexOf(key) === -1) { node.setAttr(key); } }); } // 添加inserthtml\paste等操作用的过滤规则 if (whitList && config.xssFilterRules) { this.options.filterRules = function () { var result = {}; UE.utils.each(whitList, function(val, key) { result[key] = function (node) { return filter(node); }; }); return result; }(); } var tagList = []; UE.utils.each(whitList, function (val, key) { tagList.push(key); }); // 添加input过滤规则 // if (whitList && config.inputXssFilter) { this.addInputRule(function (root) { root.traversal(function(node) { if (node.type !== 'element') { return false; } filter(node); }); }); } // 添加output过滤规则 // if (whitList && config.outputXssFilter) { this.addOutputRule(function (root) { root.traversal(function(node) { if (node.type !== 'element') { return false; } filter(node); }); }); } }; // ui/ui.js var baidu = baidu || {}; baidu.editor = baidu.editor || {}; UE.ui = baidu.editor.ui = {}; // ui/uiutils.js (function (){ var browser = baidu.editor.browser, domUtils = baidu.editor.dom.domUtils; var magic = '$EDITORUI'; var root = window[magic] = {}; var uidMagic = 'ID' + magic; var uidCount = 0; var uiUtils = baidu.editor.ui.uiUtils = { uid: function (obj){ return (obj ? obj[uidMagic] || (obj[uidMagic] = ++ uidCount) : ++ uidCount); }, hook: function ( fn, callback ) { var dg; if (fn && fn._callbacks) { dg = fn; } else { dg = function (){ var q; if (fn) { q = fn.apply(this, arguments); } var callbacks = dg._callbacks; var k = callbacks.length; while (k --) { var r = callbacks[k].apply(this, arguments); if (q === undefined) { q = r; } } return q; }; dg._callbacks = []; } dg._callbacks.push(callback); return dg; }, createElementByHtml: function (html){ var el = document.createElement('div'); el.innerHTML = html; el = el.firstChild; el.parentNode.removeChild(el); return el; }, getViewportElement: function (){ return (browser.ie && browser.quirks) ? document.body : document.documentElement; }, getClientRect: function (element){ var bcr; //trace IE6下在控制编辑器显隐时可能会报错,catch一下 try{ bcr = element.getBoundingClientRect(); }catch(e){ bcr={left:0,top:0,height:0,width:0} } var rect = { left: Math.round(bcr.left), top: Math.round(bcr.top), height: Math.round(bcr.bottom - bcr.top), width: Math.round(bcr.right - bcr.left) }; var doc; while ((doc = element.ownerDocument) !== document && (element = domUtils.getWindow(doc).frameElement)) { bcr = element.getBoundingClientRect(); rect.left += bcr.left; rect.top += bcr.top; } rect.bottom = rect.top + rect.height; rect.right = rect.left + rect.width; return rect; }, getViewportRect: function (){ var viewportEl = uiUtils.getViewportElement(); var width = (window.innerWidth || viewportEl.clientWidth) | 0; var height = (window.innerHeight ||viewportEl.clientHeight) | 0; return { left: 0, top: 0, height: height, width: width, bottom: height, right: width }; }, setViewportOffset: function (element, offset){ var rect; var fixedLayer = uiUtils.getFixedLayer(); if (element.parentNode === fixedLayer) { element.style.left = offset.left + 'px'; element.style.top = offset.top + 'px'; } else { domUtils.setViewportOffset(element, offset); } }, getEventOffset: function (evt){ var el = evt.target || evt.srcElement; var rect = uiUtils.getClientRect(el); var offset = uiUtils.getViewportOffsetByEvent(evt); return { left: offset.left - rect.left, top: offset.top - rect.top }; }, getViewportOffsetByEvent: function (evt){ var el = evt.target || evt.srcElement; var frameEl = domUtils.getWindow(el).frameElement; var offset = { left: evt.clientX, top: evt.clientY }; if (frameEl && el.ownerDocument !== document) { var rect = uiUtils.getClientRect(frameEl); offset.left += rect.left; offset.top += rect.top; } return offset; }, setGlobal: function (id, obj){ root[id] = obj; return magic + '["' + id + '"]'; }, unsetGlobal: function (id){ delete root[id]; }, copyAttributes: function (tgt, src){ var attributes = src.attributes; var k = attributes.length; while (k --) { var attrNode = attributes[k]; if ( attrNode.nodeName != 'style' && attrNode.nodeName != 'class' && (!browser.ie || attrNode.specified) ) { tgt.setAttribute(attrNode.nodeName, attrNode.nodeValue); } } if (src.className) { domUtils.addClass(tgt,src.className); } if (src.style.cssText) { tgt.style.cssText += ';' + src.style.cssText; } }, removeStyle: function (el, styleName){ if (el.style.removeProperty) { el.style.removeProperty(styleName); } else if (el.style.removeAttribute) { el.style.removeAttribute(styleName); } else throw ''; }, contains: function (elA, elB){ return elA && elB && (elA === elB ? false : ( elA.contains ? elA.contains(elB) : elA.compareDocumentPosition(elB) & 16 )); }, startDrag: function (evt, callbacks,doc){ var doc = doc || document; var startX = evt.clientX; var startY = evt.clientY; function handleMouseMove(evt){ var x = evt.clientX - startX; var y = evt.clientY - startY; callbacks.ondragmove(x, y,evt); if (evt.stopPropagation) { evt.stopPropagation(); } else { evt.cancelBubble = true; } } if (doc.addEventListener) { function handleMouseUp(evt){ doc.removeEventListener('mousemove', handleMouseMove, true); doc.removeEventListener('mouseup', handleMouseUp, true); window.removeEventListener('mouseup', handleMouseUp, true); callbacks.ondragstop(); } doc.addEventListener('mousemove', handleMouseMove, true); doc.addEventListener('mouseup', handleMouseUp, true); window.addEventListener('mouseup', handleMouseUp, true); evt.preventDefault(); } else { var elm = evt.srcElement; elm.setCapture(); function releaseCaptrue(){ elm.releaseCapture(); elm.detachEvent('onmousemove', handleMouseMove); elm.detachEvent('onmouseup', releaseCaptrue); elm.detachEvent('onlosecaptrue', releaseCaptrue); callbacks.ondragstop(); } elm.attachEvent('onmousemove', handleMouseMove); elm.attachEvent('onmouseup', releaseCaptrue); elm.attachEvent('onlosecaptrue', releaseCaptrue); evt.returnValue = false; } callbacks.ondragstart(); }, getFixedLayer: function (){ var layer = document.getElementById('edui_fixedlayer'); if (layer == null) { layer = document.createElement('div'); layer.id = 'edui_fixedlayer'; document.body.appendChild(layer); if (browser.ie && browser.version <= 8) { layer.style.position = 'absolute'; bindFixedLayer(); setTimeout(updateFixedOffset); } else { layer.style.position = 'fixed'; } layer.style.left = '0'; layer.style.top = '0'; layer.style.width = '0'; layer.style.height = '0'; } return layer; }, makeUnselectable: function (element){ if (browser.opera || (browser.ie && browser.version < 9)) { element.unselectable = 'on'; if (element.hasChildNodes()) { for (var i=0; i
    '; } }; utils.inherits(Separator, UIBase); })(); // ui/mask.js ///import core ///import uicore (function (){ var utils = baidu.editor.utils, domUtils = baidu.editor.dom.domUtils, UIBase = baidu.editor.ui.UIBase, uiUtils = baidu.editor.ui.uiUtils; var Mask = baidu.editor.ui.Mask = function (options){ this.initOptions(options); this.initUIBase(); }; Mask.prototype = { getHtmlTpl: function (){ return '
    '; }, postRender: function (){ var me = this; domUtils.on(window, 'resize', function (){ setTimeout(function (){ if (!me.isHidden()) { me._fill(); } }); }); }, show: function (zIndex){ this._fill(); this.getDom().style.display = ''; this.getDom().style.zIndex = zIndex; }, hide: function (){ this.getDom().style.display = 'none'; this.getDom().style.zIndex = ''; }, isHidden: function (){ return this.getDom().style.display == 'none'; }, _onMouseDown: function (){ return false; }, _onClick: function (e, target){ this.fireEvent('click', e, target); }, _fill: function (){ var el = this.getDom(); var vpRect = uiUtils.getViewportRect(); el.style.width = vpRect.width + 'px'; el.style.height = vpRect.height + 'px'; } }; utils.inherits(Mask, UIBase); })(); // ui/popup.js ///import core ///import uicore (function () { var utils = baidu.editor.utils, uiUtils = baidu.editor.ui.uiUtils, domUtils = baidu.editor.dom.domUtils, UIBase = baidu.editor.ui.UIBase, Popup = baidu.editor.ui.Popup = function (options){ this.initOptions(options); this.initPopup(); }; var allPopups = []; function closeAllPopup( evt,el ){ for ( var i = 0; i < allPopups.length; i++ ) { var pop = allPopups[i]; if (!pop.isHidden()) { if (pop.queryAutoHide(el) !== false) { if(evt&&/scroll/ig.test(evt.type)&&pop.className=="edui-wordpastepop") return; pop.hide(); } } } if(allPopups.length) pop.editor.fireEvent("afterhidepop"); } Popup.postHide = closeAllPopup; var ANCHOR_CLASSES = ['edui-anchor-topleft','edui-anchor-topright', 'edui-anchor-bottomleft','edui-anchor-bottomright']; Popup.prototype = { SHADOW_RADIUS: 5, content: null, _hidden: false, autoRender: true, canSideLeft: true, canSideUp: true, initPopup: function (){ this.initUIBase(); allPopups.push( this ); }, getHtmlTpl: function (){ return '
    ' + '
    ' + ' ' + '
    ' + '
    ' + this.getContentHtmlTpl() + '
    ' + '
    ' + '
    '; }, getContentHtmlTpl: function (){ if(this.content){ if (typeof this.content == 'string') { return this.content; } return this.content.renderHtml(); }else{ return '' } }, _UIBase_postRender: UIBase.prototype.postRender, postRender: function (){ if (this.content instanceof UIBase) { this.content.postRender(); } //捕获鼠标滚轮 if( this.captureWheel && !this.captured ) { this.captured = true; var winHeight = ( document.documentElement.clientHeight || document.body.clientHeight ) - 80, _height = this.getDom().offsetHeight, _top = uiUtils.getClientRect( this.combox.getDom() ).top, content = this.getDom('content'), ifr = this.getDom('body').getElementsByTagName('iframe'), me = this; ifr.length && ( ifr = ifr[0] ); while( _top + _height > winHeight ) { _height -= 30; } content.style.height = _height + 'px'; //同步更改iframe高度 ifr && ( ifr.style.height = _height + 'px' ); //阻止在combox上的鼠标滚轮事件, 防止用户的正常操作被误解 if( window.XMLHttpRequest ) { domUtils.on( content, ( 'onmousewheel' in document.body ) ? 'mousewheel' :'DOMMouseScroll' , function(e){ if(e.preventDefault) { e.preventDefault(); } else { e.returnValue = false; } if( e.wheelDelta ) { content.scrollTop -= ( e.wheelDelta / 120 )*60; } else { content.scrollTop -= ( e.detail / -3 )*60; } }); } else { //ie6 domUtils.on( this.getDom(), 'mousewheel' , function(e){ e.returnValue = false; me.getDom('content').scrollTop -= ( e.wheelDelta / 120 )*60; }); } } this.fireEvent('postRenderAfter'); this.hide(true); this._UIBase_postRender(); }, _doAutoRender: function (){ if (!this.getDom() && this.autoRender) { this.render(); } }, mesureSize: function (){ var box = this.getDom('content'); return uiUtils.getClientRect(box); }, fitSize: function (){ if( this.captureWheel && this.sized ) { return this.__size; } this.sized = true; var popBodyEl = this.getDom('body'); popBodyEl.style.width = ''; popBodyEl.style.height = ''; var size = this.mesureSize(); if( this.captureWheel ) { popBodyEl.style.width = -(-20 -size.width) + 'px'; var height = parseInt( this.getDom('content').style.height, 10 ); !window.isNaN( height ) && ( size.height = height ); } else { popBodyEl.style.width = size.width + 'px'; } popBodyEl.style.height = size.height + 'px'; this.__size = size; this.captureWheel && (this.getDom('content').style.overflow = 'auto'); return size; }, showAnchor: function ( element, hoz ){ this.showAnchorRect( uiUtils.getClientRect( element ), hoz ); }, showAnchorRect: function ( rect, hoz, adj ){ this._doAutoRender(); var vpRect = uiUtils.getViewportRect(); this.getDom().style.visibility = 'hidden'; this._show(); var popSize = this.fitSize(); var sideLeft, sideUp, left, top; if (hoz) { sideLeft = this.canSideLeft && (rect.right + popSize.width > vpRect.right && rect.left > popSize.width); sideUp = this.canSideUp && (rect.top + popSize.height > vpRect.bottom && rect.bottom > popSize.height); left = (sideLeft ? rect.left - popSize.width : rect.right); top = (sideUp ? rect.bottom - popSize.height : rect.top); } else { sideLeft = this.canSideLeft && (rect.right + popSize.width > vpRect.right && rect.left > popSize.width); sideUp = this.canSideUp && (rect.top + popSize.height > vpRect.bottom && rect.bottom > popSize.height); left = (sideLeft ? rect.right - popSize.width : rect.left); top = (sideUp ? rect.top - popSize.height : rect.bottom); } var popEl = this.getDom(); uiUtils.setViewportOffset(popEl, { left: left, top: top }); domUtils.removeClasses(popEl, ANCHOR_CLASSES); popEl.className += ' ' + ANCHOR_CLASSES[(sideUp ? 1 : 0) * 2 + (sideLeft ? 1 : 0)]; if(this.editor){ popEl.style.zIndex = this.editor.container.style.zIndex * 1 + 10; baidu.editor.ui.uiUtils.getFixedLayer().style.zIndex = popEl.style.zIndex - 1; } this.getDom().style.visibility = 'visible'; }, showAt: function (offset) { var left = offset.left; var top = offset.top; var rect = { left: left, top: top, right: left, bottom: top, height: 0, width: 0 }; this.showAnchorRect(rect, false, true); }, _show: function (){ if (this._hidden) { var box = this.getDom(); box.style.display = ''; this._hidden = false; // if (box.setActive) { // box.setActive(); // } this.fireEvent('show'); } }, isHidden: function (){ return this._hidden; }, show: function (){ this._doAutoRender(); this._show(); }, hide: function (notNofity){ if (!this._hidden && this.getDom()) { this.getDom().style.display = 'none'; this._hidden = true; if (!notNofity) { this.fireEvent('hide'); } } }, queryAutoHide: function (el){ return !el || !uiUtils.contains(this.getDom(), el); } }; utils.inherits(Popup, UIBase); domUtils.on( document, 'mousedown', function ( evt ) { var el = evt.target || evt.srcElement; closeAllPopup( evt,el ); } ); domUtils.on( window, 'scroll', function (evt,el) { closeAllPopup( evt,el ); } ); })(); // ui/colorpicker.js ///import core ///import uicore (function (){ var utils = baidu.editor.utils, UIBase = baidu.editor.ui.UIBase, ColorPicker = baidu.editor.ui.ColorPicker = function (options){ this.initOptions(options); this.noColorText = this.noColorText || this.editor.getLang("clearColor"); this.initUIBase(); }; ColorPicker.prototype = { getHtmlTpl: function (){ return genColorPicker(this.noColorText,this.editor); }, _onTableClick: function (evt){ var tgt = evt.target || evt.srcElement; var color = tgt.getAttribute('data-color'); if (color) { this.fireEvent('pickcolor', color); } }, _onTableOver: function (evt){ var tgt = evt.target || evt.srcElement; var color = tgt.getAttribute('data-color'); if (color) { this.getDom('preview').style.backgroundColor = color; } }, _onTableOut: function (){ this.getDom('preview').style.backgroundColor = ''; }, _onPickNoColor: function (){ this.fireEvent('picknocolor'); } }; utils.inherits(ColorPicker, UIBase); var COLORS = ( 'ffffff,000000,eeece1,1f497d,4f81bd,c0504d,9bbb59,8064a2,4bacc6,f79646,' + 'f2f2f2,7f7f7f,ddd9c3,c6d9f0,dbe5f1,f2dcdb,ebf1dd,e5e0ec,dbeef3,fdeada,' + 'd8d8d8,595959,c4bd97,8db3e2,b8cce4,e5b9b7,d7e3bc,ccc1d9,b7dde8,fbd5b5,' + 'bfbfbf,3f3f3f,938953,548dd4,95b3d7,d99694,c3d69b,b2a2c7,92cddc,fac08f,' + 'a5a5a5,262626,494429,17365d,366092,953734,76923c,5f497a,31859b,e36c09,' + '7f7f7f,0c0c0c,1d1b10,0f243e,244061,632423,4f6128,3f3151,205867,974806,' + 'c00000,ff0000,ffc000,ffff00,92d050,00b050,00b0f0,0070c0,002060,7030a0,').split(','); function genColorPicker(noColorText,editor){ var html = '
    ' + '
    ' + '
    ' + '
    '+ noColorText +'
    ' + '
    ' + '' + ''+ ''; for (var i=0; i':'')+''; } html += i<70 ? '':''; } html += '
    '+editor.getLang("themeColor")+'
    '+editor.getLang("standardColor")+'
    '; return html; } })(); // ui/tablepicker.js ///import core ///import uicore (function (){ var utils = baidu.editor.utils, uiUtils = baidu.editor.ui.uiUtils, UIBase = baidu.editor.ui.UIBase; var TablePicker = baidu.editor.ui.TablePicker = function (options){ this.initOptions(options); this.initTablePicker(); }; TablePicker.prototype = { defaultNumRows: 10, defaultNumCols: 10, maxNumRows: 20, maxNumCols: 20, numRows: 10, numCols: 10, lengthOfCellSide: 22, initTablePicker: function (){ this.initUIBase(); }, getHtmlTpl: function (){ var me = this; return '
    ' + '
    ' + '
    ' + '' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    '; }, _UIBase_render: UIBase.prototype.render, render: function (holder){ this._UIBase_render(holder); this.getDom('label').innerHTML = '0'+this.editor.getLang("t_row")+' x 0'+this.editor.getLang("t_col"); }, _track: function (numCols, numRows){ var style = this.getDom('overlay').style; var sideLen = this.lengthOfCellSide; style.width = numCols * sideLen + 'px'; style.height = numRows * sideLen + 'px'; var label = this.getDom('label'); label.innerHTML = numCols +this.editor.getLang("t_col")+' x ' + numRows + this.editor.getLang("t_row"); this.numCols = numCols; this.numRows = numRows; }, _onMouseOver: function (evt, el){ var rel = evt.relatedTarget || evt.fromElement; if (!uiUtils.contains(el, rel) && el !== rel) { this.getDom('label').innerHTML = '0'+this.editor.getLang("t_col")+' x 0'+this.editor.getLang("t_row"); this.getDom('overlay').style.visibility = ''; } }, _onMouseOut: function (evt, el){ var rel = evt.relatedTarget || evt.toElement; if (!uiUtils.contains(el, rel) && el !== rel) { this.getDom('label').innerHTML = '0'+this.editor.getLang("t_col")+' x 0'+this.editor.getLang("t_row"); this.getDom('overlay').style.visibility = 'hidden'; } }, _onMouseMove: function (evt, el){ var style = this.getDom('overlay').style; var offset = uiUtils.getEventOffset(evt); var sideLen = this.lengthOfCellSide; var numCols = Math.ceil(offset.left / sideLen); var numRows = Math.ceil(offset.top / sideLen); this._track(numCols, numRows); }, _onClick: function (){ this.fireEvent('picktable', this.numCols, this.numRows); } }; utils.inherits(TablePicker, UIBase); })(); // ui/stateful.js (function (){ var browser = baidu.editor.browser, domUtils = baidu.editor.dom.domUtils, uiUtils = baidu.editor.ui.uiUtils; var TPL_STATEFUL = 'onmousedown="$$.Stateful_onMouseDown(event, this);"' + ' onmouseup="$$.Stateful_onMouseUp(event, this);"' + ( browser.ie ? ( ' onmouseenter="$$.Stateful_onMouseEnter(event, this);"' + ' onmouseleave="$$.Stateful_onMouseLeave(event, this);"' ) : ( ' onmouseover="$$.Stateful_onMouseOver(event, this);"' + ' onmouseout="$$.Stateful_onMouseOut(event, this);"' )); baidu.editor.ui.Stateful = { alwalysHoverable: false, target:null,//目标元素和this指向dom不一样 Stateful_init: function (){ this._Stateful_dGetHtmlTpl = this.getHtmlTpl; this.getHtmlTpl = this.Stateful_getHtmlTpl; }, Stateful_getHtmlTpl: function (){ var tpl = this._Stateful_dGetHtmlTpl(); // 使用function避免$转义 return tpl.replace(/stateful/g, function (){ return TPL_STATEFUL; }); }, Stateful_onMouseEnter: function (evt, el){ this.target=el; if (!this.isDisabled() || this.alwalysHoverable) { this.addState('hover'); this.fireEvent('over'); } }, Stateful_onMouseLeave: function (evt, el){ if (!this.isDisabled() || this.alwalysHoverable) { this.removeState('hover'); this.removeState('active'); this.fireEvent('out'); } }, Stateful_onMouseOver: function (evt, el){ var rel = evt.relatedTarget; if (!uiUtils.contains(el, rel) && el !== rel) { this.Stateful_onMouseEnter(evt, el); } }, Stateful_onMouseOut: function (evt, el){ var rel = evt.relatedTarget; if (!uiUtils.contains(el, rel) && el !== rel) { this.Stateful_onMouseLeave(evt, el); } }, Stateful_onMouseDown: function (evt, el){ if (!this.isDisabled()) { this.addState('active'); } }, Stateful_onMouseUp: function (evt, el){ if (!this.isDisabled()) { this.removeState('active'); } }, Stateful_postRender: function (){ if (this.disabled && !this.hasState('disabled')) { this.addState('disabled'); } }, hasState: function (state){ return domUtils.hasClass(this.getStateDom(), 'edui-state-' + state); }, addState: function (state){ if (!this.hasState(state)) { this.getStateDom().className += ' edui-state-' + state; } }, removeState: function (state){ if (this.hasState(state)) { domUtils.removeClasses(this.getStateDom(), ['edui-state-' + state]); } }, getStateDom: function (){ return this.getDom('state'); }, isChecked: function (){ return this.hasState('checked'); }, setChecked: function (checked){ if (!this.isDisabled() && checked) { this.addState('checked'); } else { this.removeState('checked'); } }, isDisabled: function (){ return this.hasState('disabled'); }, setDisabled: function (disabled){ if (disabled) { this.removeState('hover'); this.removeState('checked'); this.removeState('active'); this.addState('disabled'); } else { this.removeState('disabled'); } } }; })(); // ui/button.js ///import core ///import uicore ///import ui/stateful.js (function (){ var utils = baidu.editor.utils, UIBase = baidu.editor.ui.UIBase, Stateful = baidu.editor.ui.Stateful, Button = baidu.editor.ui.Button = function (options){ if(options.name){ var btnName = options.name; var cssRules = options.cssRules; if(!options.className){ options.className = 'edui-for-' + btnName; } options.cssRules = '.edui-default .edui-for-'+ btnName +' .edui-icon {'+ cssRules +'}' } this.initOptions(options); this.initButton(); }; Button.prototype = { uiName: 'button', label: '', title: '', showIcon: true, showText: true, cssRules:'', initButton: function (){ this.initUIBase(); this.Stateful_init(); if(this.cssRules){ utils.cssRule('edui-customize-'+this.name+'-style',this.cssRules); } }, getHtmlTpl: function (){ return '
    ' + '
    ' + '
    ' + (this.showIcon ? '
    ' : '') + (this.showText ? '
    ' + this.label + '
    ' : '') + '
    ' + '
    ' + '
    '; }, postRender: function (){ this.Stateful_postRender(); this.setDisabled(this.disabled) }, _onMouseDown: function (e){ var target = e.target || e.srcElement, tagName = target && target.tagName && target.tagName.toLowerCase(); if (tagName == 'input' || tagName == 'object' || tagName == 'object') { return false; } }, _onClick: function (){ if (!this.isDisabled()) { this.fireEvent('click'); } }, setTitle: function(text){ var label = this.getDom('label'); label.innerHTML = text; } }; utils.inherits(Button, UIBase); utils.extend(Button.prototype, Stateful); })(); // ui/splitbutton.js ///import core ///import uicore ///import ui/stateful.js (function (){ var utils = baidu.editor.utils, uiUtils = baidu.editor.ui.uiUtils, domUtils = baidu.editor.dom.domUtils, UIBase = baidu.editor.ui.UIBase, Stateful = baidu.editor.ui.Stateful, SplitButton = baidu.editor.ui.SplitButton = function (options){ this.initOptions(options); this.initSplitButton(); }; SplitButton.prototype = { popup: null, uiName: 'splitbutton', title: '', initSplitButton: function (){ this.initUIBase(); this.Stateful_init(); var me = this; if (this.popup != null) { var popup = this.popup; this.popup = null; this.setPopup(popup); } }, _UIBase_postRender: UIBase.prototype.postRender, postRender: function (){ this.Stateful_postRender(); this._UIBase_postRender(); }, setPopup: function (popup){ if (this.popup === popup) return; if (this.popup != null) { this.popup.dispose(); } popup.addListener('show', utils.bind(this._onPopupShow, this)); popup.addListener('hide', utils.bind(this._onPopupHide, this)); popup.addListener('postrender', utils.bind(function (){ popup.getDom('body').appendChild( uiUtils.createElementByHtml('
    ') ); popup.getDom().className += ' ' + this.className; }, this)); this.popup = popup; }, _onPopupShow: function (){ this.addState('opened'); }, _onPopupHide: function (){ this.removeState('opened'); }, getHtmlTpl: function (){ return '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    '; }, showPopup: function (){ // 当popup往上弹出的时候,做特殊处理 var rect = uiUtils.getClientRect(this.getDom()); rect.top -= this.popup.SHADOW_RADIUS; rect.height += this.popup.SHADOW_RADIUS; this.popup.showAnchorRect(rect); }, _onArrowClick: function (event, el){ if (!this.isDisabled()) { this.showPopup(); } }, _onButtonClick: function (){ if (!this.isDisabled()) { this.fireEvent('buttonclick'); } } }; utils.inherits(SplitButton, UIBase); utils.extend(SplitButton.prototype, Stateful, true); })(); // ui/colorbutton.js ///import core ///import uicore ///import ui/colorpicker.js ///import ui/popup.js ///import ui/splitbutton.js (function (){ var utils = baidu.editor.utils, uiUtils = baidu.editor.ui.uiUtils, ColorPicker = baidu.editor.ui.ColorPicker, Popup = baidu.editor.ui.Popup, SplitButton = baidu.editor.ui.SplitButton, ColorButton = baidu.editor.ui.ColorButton = function (options){ this.initOptions(options); this.initColorButton(); }; ColorButton.prototype = { initColorButton: function (){ var me = this; this.popup = new Popup({ content: new ColorPicker({ noColorText: me.editor.getLang("clearColor"), editor:me.editor, onpickcolor: function (t, color){ me._onPickColor(color); }, onpicknocolor: function (t, color){ me._onPickNoColor(color); } }), editor:me.editor }); this.initSplitButton(); }, _SplitButton_postRender: SplitButton.prototype.postRender, postRender: function (){ this._SplitButton_postRender(); this.getDom('button_body').appendChild( uiUtils.createElementByHtml('
    ') ); this.getDom().className += ' edui-colorbutton'; }, setColor: function (color){ this.getDom('colorlump').style.backgroundColor = color; this.color = color; }, _onPickColor: function (color){ if (this.fireEvent('pickcolor', color) !== false) { this.setColor(color); this.popup.hide(); } }, _onPickNoColor: function (color){ if (this.fireEvent('picknocolor') !== false) { this.popup.hide(); } } }; utils.inherits(ColorButton, SplitButton); })(); // ui/tablebutton.js ///import core ///import uicore ///import ui/popup.js ///import ui/tablepicker.js ///import ui/splitbutton.js (function (){ var utils = baidu.editor.utils, Popup = baidu.editor.ui.Popup, TablePicker = baidu.editor.ui.TablePicker, SplitButton = baidu.editor.ui.SplitButton, TableButton = baidu.editor.ui.TableButton = function (options){ this.initOptions(options); this.initTableButton(); }; TableButton.prototype = { initTableButton: function (){ var me = this; this.popup = new Popup({ content: new TablePicker({ editor:me.editor, onpicktable: function (t, numCols, numRows){ me._onPickTable(numCols, numRows); } }), 'editor':me.editor }); this.initSplitButton(); }, _onPickTable: function (numCols, numRows){ if (this.fireEvent('picktable', numCols, numRows) !== false) { this.popup.hide(); } } }; utils.inherits(TableButton, SplitButton); })(); // ui/autotypesetpicker.js ///import core ///import uicore (function () { var utils = baidu.editor.utils, UIBase = baidu.editor.ui.UIBase; var AutoTypeSetPicker = baidu.editor.ui.AutoTypeSetPicker = function (options) { this.initOptions(options); this.initAutoTypeSetPicker(); }; AutoTypeSetPicker.prototype = { initAutoTypeSetPicker:function () { this.initUIBase(); }, getHtmlTpl:function () { var me = this.editor, opt = me.options.autotypeset, lang = me.getLang("autoTypeSet"); var textAlignInputName = 'textAlignValue' + me.uid, imageBlockInputName = 'imageBlockLineValue' + me.uid, symbolConverInputName = 'symbolConverValue' + me.uid; return '
    ' + '
    ' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
    ' + lang.mergeLine + '' + lang.delLine + '
    ' + lang.removeFormat + '' + lang.indent + '
    ' + lang.alignment + '' + '' + me.getLang("justifyleft") + '' + me.getLang("justifycenter") + '' + me.getLang("justifyright") + '
    ' + lang.imageFloat + '' + '' + me.getLang("default") + '' + me.getLang("justifyleft") + '' + me.getLang("justifycenter") + '' + me.getLang("justifyright") + '
    ' + lang.removeFontsize + '' + lang.removeFontFamily + '
    ' + lang.removeHtml + '
    ' + lang.pasteFilter + '
    ' + lang.symbol + '' + '' + lang.bdc2sb + '' + lang.tobdc + '' + '
    ' + '
    ' + '
    '; }, _UIBase_render:UIBase.prototype.render }; utils.inherits(AutoTypeSetPicker, UIBase); })(); // ui/autotypesetbutton.js ///import core ///import uicore ///import ui/popup.js ///import ui/autotypesetpicker.js ///import ui/splitbutton.js (function (){ var utils = baidu.editor.utils, Popup = baidu.editor.ui.Popup, AutoTypeSetPicker = baidu.editor.ui.AutoTypeSetPicker, SplitButton = baidu.editor.ui.SplitButton, AutoTypeSetButton = baidu.editor.ui.AutoTypeSetButton = function (options){ this.initOptions(options); this.initAutoTypeSetButton(); }; function getPara(me){ var opt = {}, cont = me.getDom(), editorId = me.editor.uid, inputType = null, attrName = null, ipts = domUtils.getElementsByTagName(cont,"input"); for(var i=ipts.length-1,ipt;ipt=ipts[i--];){ inputType = ipt.getAttribute("type"); if(inputType=="checkbox"){ attrName = ipt.getAttribute("name"); opt[attrName] && delete opt[attrName]; if(ipt.checked){ var attrValue = document.getElementById( attrName + "Value" + editorId ); if(attrValue){ if(/input/ig.test(attrValue.tagName)){ opt[attrName] = attrValue.value; } else { var iptChilds = attrValue.getElementsByTagName("input"); for(var j=iptChilds.length-1,iptchild;iptchild=iptChilds[j--];){ if(iptchild.checked){ opt[attrName] = iptchild.value; break; } } } } else { opt[attrName] = true; } } else { opt[attrName] = false; } } else { opt[ipt.getAttribute("value")] = ipt.checked; } } var selects = domUtils.getElementsByTagName(cont,"select"); for(var i=0,si;si=selects[i++];){ var attr = si.getAttribute('name'); opt[attr] = opt[attr] ? si.value : ''; } utils.extend(me.editor.options.autotypeset,opt); me.editor.setPreferences('autotypeset', opt); } AutoTypeSetButton.prototype = { initAutoTypeSetButton: function (){ var me = this; this.popup = new Popup({ //传入配置参数 content: new AutoTypeSetPicker({editor:me.editor}), 'editor':me.editor, hide : function(){ if (!this._hidden && this.getDom()) { getPara(this); this.getDom().style.display = 'none'; this._hidden = true; this.fireEvent('hide'); } } }); var flag = 0; this.popup.addListener('postRenderAfter',function(){ var popupUI = this; if(flag)return; var cont = this.getDom(), btn = cont.getElementsByTagName('button')[0]; btn.onclick = function(){ getPara(popupUI); me.editor.execCommand('autotypeset'); popupUI.hide() }; domUtils.on(cont, 'click', function(e) { var target = e.target || e.srcElement, editorId = me.editor.uid; if (target && target.tagName == 'INPUT') { // 点击图片浮动的checkbox,去除对应的radio if (target.name == 'imageBlockLine' || target.name == 'textAlign' || target.name == 'symbolConver') { var checked = target.checked, radioTd = document.getElementById( target.name + 'Value' + editorId), radios = radioTd.getElementsByTagName('input'), defalutSelect = { 'imageBlockLine': 'none', 'textAlign': 'left', 'symbolConver': 'tobdc' }; for (var i = 0; i < radios.length; i++) { if (checked) { if (radios[i].value == defalutSelect[target.name]) { radios[i].checked = 'checked'; } } else { radios[i].checked = false; } } } // 点击radio,选中对应的checkbox if (target.name == ('imageBlockLineValue' + editorId) || target.name == ('textAlignValue' + editorId) || target.name == 'bdc') { var checkboxs = target.parentNode.previousSibling.getElementsByTagName('input'); checkboxs && (checkboxs[0].checked = true); } getPara(popupUI); } }); flag = 1; }); this.initSplitButton(); } }; utils.inherits(AutoTypeSetButton, SplitButton); })(); // ui/cellalignpicker.js ///import core ///import uicore (function () { var utils = baidu.editor.utils, Popup = baidu.editor.ui.Popup, Stateful = baidu.editor.ui.Stateful, UIBase = baidu.editor.ui.UIBase; /** * 该参数将新增一个参数: selected, 参数类型为一个Object, 形如{ 'align': 'center', 'valign': 'top' }, 表示单元格的初始 * 对齐状态为: 竖直居上,水平居中; 其中 align的取值为:'center', 'left', 'right'; valign的取值为: 'top', 'middle', 'bottom' * @update 2013/4/2 hancong03@baidu.com */ var CellAlignPicker = baidu.editor.ui.CellAlignPicker = function (options) { this.initOptions(options); this.initSelected(); this.initCellAlignPicker(); }; CellAlignPicker.prototype = { //初始化选中状态, 该方法将根据传递进来的参数获取到应该选中的对齐方式图标的索引 initSelected: function(){ var status = { valign: { top: 0, middle: 1, bottom: 2 }, align: { left: 0, center: 1, right: 2 }, count: 3 }, result = -1; if( this.selected ) { this.selectedIndex = status.valign[ this.selected.valign ] * status.count + status.align[ this.selected.align ]; } }, initCellAlignPicker:function () { this.initUIBase(); this.Stateful_init(); }, getHtmlTpl:function () { var alignType = [ 'left', 'center', 'right' ], COUNT = 9, tempClassName = null, tempIndex = -1, tmpl = []; for( var i= 0; i'); tmpl.push( '
    ' ); tempIndex === 2 && tmpl.push(''); } return '
    ' + '
    ' + '' + tmpl.join('') + '
    ' + '
    ' + '
    '; }, getStateDom: function (){ return this.target; }, _onClick: function (evt){ var target= evt.target || evt.srcElement; if(/icon/.test(target.className)){ this.items[target.parentNode.getAttribute("index")].onclick(); Popup.postHide(evt); } }, _UIBase_render:UIBase.prototype.render }; utils.inherits(CellAlignPicker, UIBase); utils.extend(CellAlignPicker.prototype, Stateful,true); })(); // ui/pastepicker.js ///import core ///import uicore (function () { var utils = baidu.editor.utils, Stateful = baidu.editor.ui.Stateful, uiUtils = baidu.editor.ui.uiUtils, UIBase = baidu.editor.ui.UIBase; var PastePicker = baidu.editor.ui.PastePicker = function (options) { this.initOptions(options); this.initPastePicker(); }; PastePicker.prototype = { initPastePicker:function () { this.initUIBase(); this.Stateful_init(); }, getHtmlTpl:function () { return '
    ' + '
    ' + '
    ' + this.editor.getLang("pasteOpt") + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' }, getStateDom:function () { return this.target; }, format:function (param) { this.editor.ui._isTransfer = true; this.editor.fireEvent('pasteTransfer', param); }, _onClick:function (cur) { var node = domUtils.getNextDomNode(cur), screenHt = uiUtils.getViewportRect().height, subPop = uiUtils.getClientRect(node); if ((subPop.top + subPop.height) > screenHt) node.style.top = (-subPop.height - cur.offsetHeight) + "px"; else node.style.top = ""; if (/hidden/ig.test(domUtils.getComputedStyle(node, "visibility"))) { node.style.visibility = "visible"; domUtils.addClass(cur, "edui-state-opened"); } else { node.style.visibility = "hidden"; domUtils.removeClasses(cur, "edui-state-opened") } }, _UIBase_render:UIBase.prototype.render }; utils.inherits(PastePicker, UIBase); utils.extend(PastePicker.prototype, Stateful, true); })(); // ui/toolbar.js (function (){ var utils = baidu.editor.utils, uiUtils = baidu.editor.ui.uiUtils, UIBase = baidu.editor.ui.UIBase, Toolbar = baidu.editor.ui.Toolbar = function (options){ this.initOptions(options); this.initToolbar(); }; Toolbar.prototype = { items: null, initToolbar: function (){ this.items = this.items || []; this.initUIBase(); }, add: function (item,index){ if(index === undefined){ this.items.push(item); }else{ this.items.splice(index,0,item) } }, getHtmlTpl: function (){ var buff = []; for (var i=0; i' + buff.join('') + '
    ' }, postRender: function (){ var box = this.getDom(); for (var i=0; i
    '; }, postRender:function () { }, queryAutoHide:function () { return true; } }; Menu.prototype = { items:null, uiName:'menu', initMenu:function () { this.items = this.items || []; this.initPopup(); this.initItems(); }, initItems:function () { for (var i = 0; i < this.items.length; i++) { var item = this.items[i]; if (item == '-') { this.items[i] = this.getSeparator(); } else if (!(item instanceof MenuItem)) { item.editor = this.editor; item.theme = this.editor.options.theme; this.items[i] = this.createItem(item); } } }, getSeparator:function () { return menuSeparator; }, createItem:function (item) { //新增一个参数menu, 该参数存储了menuItem所对应的menu引用 item.menu = this; return new MenuItem(item); }, _Popup_getContentHtmlTpl:Popup.prototype.getContentHtmlTpl, getContentHtmlTpl:function () { if (this.items.length == 0) { return this._Popup_getContentHtmlTpl(); } var buff = []; for (var i = 0; i < this.items.length; i++) { var item = this.items[i]; buff[i] = item.renderHtml(); } return ('
    ' + buff.join('') + '
    '); }, _Popup_postRender:Popup.prototype.postRender, postRender:function () { var me = this; for (var i = 0; i < this.items.length; i++) { var item = this.items[i]; item.ownerMenu = this; item.postRender(); } domUtils.on(this.getDom(), 'mouseover', function (evt) { evt = evt || event; var rel = evt.relatedTarget || evt.fromElement; var el = me.getDom(); if (!uiUtils.contains(el, rel) && el !== rel) { me.fireEvent('over'); } }); this._Popup_postRender(); }, queryAutoHide:function (el) { if (el) { if (uiUtils.contains(this.getDom(), el)) { return false; } for (var i = 0; i < this.items.length; i++) { var item = this.items[i]; if (item.queryAutoHide(el) === false) { return false; } } } }, clearItems:function () { for (var i = 0; i < this.items.length; i++) { var item = this.items[i]; clearTimeout(item._showingTimer); clearTimeout(item._closingTimer); if (item.subMenu) { item.subMenu.destroy(); } } this.items = []; }, destroy:function () { if (this.getDom()) { domUtils.remove(this.getDom()); } this.clearItems(); }, dispose:function () { this.destroy(); } }; utils.inherits(Menu, Popup); /** * @update 2013/04/03 hancong03 新增一个参数menu, 该参数存储了menuItem所对应的menu引用 * @type {Function} */ var MenuItem = baidu.editor.ui.MenuItem = function (options) { this.initOptions(options); this.initUIBase(); this.Stateful_init(); if (this.subMenu && !(this.subMenu instanceof Menu)) { if (options.className && options.className.indexOf("aligntd") != -1) { var me = this; //获取单元格对齐初始状态 this.subMenu.selected = this.editor.queryCommandValue( 'cellalignment' ); this.subMenu = new Popup({ content:new CellAlignPicker(this.subMenu), parentMenu:me, editor:me.editor, destroy:function () { if (this.getDom()) { domUtils.remove(this.getDom()); } } }); this.subMenu.addListener("postRenderAfter", function () { domUtils.on(this.getDom(), "mouseover", function () { me.addState('opened'); }); }); } else { this.subMenu = new Menu(this.subMenu); } } }; MenuItem.prototype = { label:'', subMenu:null, ownerMenu:null, uiName:'menuitem', alwalysHoverable:true, getHtmlTpl:function () { return '
    ' + '
    ' + this.renderLabelHtml() + '
    ' + '
    '; }, postRender:function () { var me = this; this.addListener('over', function () { me.ownerMenu.fireEvent('submenuover', me); if (me.subMenu) { me.delayShowSubMenu(); } }); if (this.subMenu) { this.getDom().className += ' edui-hassubmenu'; this.subMenu.render(); this.addListener('out', function () { me.delayHideSubMenu(); }); this.subMenu.addListener('over', function () { clearTimeout(me._closingTimer); me._closingTimer = null; me.addState('opened'); }); this.ownerMenu.addListener('hide', function () { me.hideSubMenu(); }); this.ownerMenu.addListener('submenuover', function (t, subMenu) { if (subMenu !== me) { me.delayHideSubMenu(); } }); this.subMenu._bakQueryAutoHide = this.subMenu.queryAutoHide; this.subMenu.queryAutoHide = function (el) { if (el && uiUtils.contains(me.getDom(), el)) { return false; } return this._bakQueryAutoHide(el); }; } this.getDom().style.tabIndex = '-1'; uiUtils.makeUnselectable(this.getDom()); this.Stateful_postRender(); }, delayShowSubMenu:function () { var me = this; if (!me.isDisabled()) { me.addState('opened'); clearTimeout(me._showingTimer); clearTimeout(me._closingTimer); me._closingTimer = null; me._showingTimer = setTimeout(function () { me.showSubMenu(); }, 250); } }, delayHideSubMenu:function () { var me = this; if (!me.isDisabled()) { me.removeState('opened'); clearTimeout(me._showingTimer); if (!me._closingTimer) { me._closingTimer = setTimeout(function () { if (!me.hasState('opened')) { me.hideSubMenu(); } me._closingTimer = null; }, 400); } } }, renderLabelHtml:function () { return '
    ' + '
    ' + '
    ' + (this.label || '') + '
    '; }, getStateDom:function () { return this.getDom(); }, queryAutoHide:function (el) { if (this.subMenu && this.hasState('opened')) { return this.subMenu.queryAutoHide(el); } }, _onClick:function (event, this_) { if (this.hasState('disabled')) return; if (this.fireEvent('click', event, this_) !== false) { if (this.subMenu) { this.showSubMenu(); } else { Popup.postHide(event); } } }, showSubMenu:function () { var rect = uiUtils.getClientRect(this.getDom()); rect.right -= 5; rect.left += 2; rect.width -= 7; rect.top -= 4; rect.bottom += 4; rect.height += 8; this.subMenu.showAnchorRect(rect, true, true); }, hideSubMenu:function () { this.subMenu.hide(); } }; utils.inherits(MenuItem, UIBase); utils.extend(MenuItem.prototype, Stateful, true); })(); // ui/combox.js ///import core ///import uicore ///import ui/menu.js ///import ui/splitbutton.js (function (){ // todo: menu和item提成通用list var utils = baidu.editor.utils, uiUtils = baidu.editor.ui.uiUtils, Menu = baidu.editor.ui.Menu, SplitButton = baidu.editor.ui.SplitButton, Combox = baidu.editor.ui.Combox = function (options){ this.initOptions(options); this.initCombox(); }; Combox.prototype = { uiName: 'combox', onbuttonclick:function () { this.showPopup(); }, initCombox: function (){ var me = this; this.items = this.items || []; for (var i=0; i vpRect.right) { left = vpRect.right - rect.width; } var top = offset.top; if (top + rect.height > vpRect.bottom) { top = vpRect.bottom - rect.height; } el.style.left = Math.max(left, 0) + 'px'; el.style.top = Math.max(top, 0) + 'px'; }, showAtCenter: function (){ var vpRect = uiUtils.getViewportRect(); if ( !this.fullscreen ) { this.getDom().style.display = ''; var popSize = this.fitSize(); var titleHeight = this.getDom('titlebar').offsetHeight | 0; var left = vpRect.width / 2 - popSize.width / 2; var top = vpRect.height / 2 - (popSize.height - titleHeight) / 2 - titleHeight; var popEl = this.getDom(); this.safeSetOffset({ left: Math.max(left | 0, 0), top: Math.max(top | 0, 0) }); if (!domUtils.hasClass(popEl, 'edui-state-centered')) { popEl.className += ' edui-state-centered'; } } else { var dialogWrapNode = this.getDom(), contentNode = this.getDom('content'); dialogWrapNode.style.display = "block"; var wrapRect = UE.ui.uiUtils.getClientRect( dialogWrapNode ), contentRect = UE.ui.uiUtils.getClientRect( contentNode ); dialogWrapNode.style.left = "-100000px"; contentNode.style.width = ( vpRect.width - wrapRect.width + contentRect.width ) + "px"; contentNode.style.height = ( vpRect.height - wrapRect.height + contentRect.height ) + "px"; dialogWrapNode.style.width = vpRect.width + "px"; dialogWrapNode.style.height = vpRect.height + "px"; dialogWrapNode.style.left = 0; //保存环境的overflow值 this._originalContext = { html: { overflowX: document.documentElement.style.overflowX, overflowY: document.documentElement.style.overflowY }, body: { overflowX: document.body.style.overflowX, overflowY: document.body.style.overflowY } }; document.documentElement.style.overflowX = 'hidden'; document.documentElement.style.overflowY = 'hidden'; document.body.style.overflowX = 'hidden'; document.body.style.overflowY = 'hidden'; } this._show(); }, getContentHtml: function (){ var contentHtml = ''; if (typeof this.content == 'string') { contentHtml = this.content; } else if (this.iframeUrl) { contentHtml = ''; } return contentHtml; }, getHtmlTpl: function (){ var footHtml = ''; if (this.buttons) { var buff = []; for (var i=0; i' + buff.join('') + '
    ' + ''; } return '
    ' + '
    ' + '
    ' + '
    ' + '' + (this.title || '') + '' + '
    ' + this.closeButton.renderHtml() + '
    ' + '
    '+ ( this.autoReset ? '' : this.getContentHtml()) +'
    ' + footHtml + '
    '; }, postRender: function (){ // todo: 保持居中/记住上次关闭位置选项 if (!this.modalMask.getDom()) { this.modalMask.render(); this.modalMask.hide(); } if (!this.dragMask.getDom()) { this.dragMask.render(); this.dragMask.hide(); } var me = this; this.addListener('show', function (){ me.modalMask.show(this.getDom().style.zIndex - 2); }); this.addListener('hide', function (){ me.modalMask.hide(); }); if (this.buttons) { for (var i=0; i'; me.editor.container.style.zIndex && (this.getDom().style.zIndex = me.editor.container.style.zIndex * 1 + 1); } } // canSideUp:false, // canSideLeft:false }); this.onbuttonclick = function(){ this.showPopup(); }; this.initSplitButton(); } }; utils.inherits(MultiMenuPop, SplitButton); })(); // ui/shortcutmenu.js (function () { var UI = baidu.editor.ui, UIBase = UI.UIBase, uiUtils = UI.uiUtils, utils = baidu.editor.utils, domUtils = baidu.editor.dom.domUtils; var allMenus = [],//存储所有快捷菜单 timeID, isSubMenuShow = false;//是否有子pop显示 var ShortCutMenu = UI.ShortCutMenu = function (options) { this.initOptions (options); this.initShortCutMenu (); }; ShortCutMenu.postHide = hideAllMenu; ShortCutMenu.prototype = { isHidden : true , SPACE : 5 , initShortCutMenu : function () { this.items = this.items || []; this.initUIBase (); this.initItems (); this.initEvent (); allMenus.push (this); } , initEvent : function () { var me = this, doc = me.editor.document; domUtils.on (doc , "mousemove" , function (e) { if (me.isHidden === false) { //有pop显示就不隐藏快捷菜单 if (me.getSubMenuMark () || me.eventType == "contextmenu") return; var flag = true, el = me.getDom (), wt = el.offsetWidth, ht = el.offsetHeight, distanceX = wt / 2 + me.SPACE,//距离中心X标准 distanceY = ht / 2,//距离中心Y标准 x = Math.abs (e.screenX - me.left),//离中心距离横坐标 y = Math.abs (e.screenY - me.top);//离中心距离纵坐标 clearTimeout (timeID); timeID = setTimeout (function () { if (y > 0 && y < distanceY) { me.setOpacity (el , "1"); } else if (y > distanceY && y < distanceY + 70) { me.setOpacity (el , "0.5"); flag = false; } else if (y > distanceY + 70 && y < distanceY + 140) { me.hide (); } if (flag && x > 0 && x < distanceX) { me.setOpacity (el , "1") } else if (x > distanceX && x < distanceX + 70) { me.setOpacity (el , "0.5") } else if (x > distanceX + 70 && x < distanceX + 140) { me.hide (); } }); } }); //ie\ff下 mouseout不准 if (browser.chrome) { domUtils.on (doc , "mouseout" , function (e) { var relatedTgt = e.relatedTarget || e.toElement; if (relatedTgt == null || relatedTgt.tagName == "HTML") { me.hide (); } }); } me.editor.addListener ("afterhidepop" , function () { if (!me.isHidden) { isSubMenuShow = true; } }); } , initItems : function () { if (utils.isArray (this.items)) { for (var i = 0, len = this.items.length ; i < len ; i++) { var item = this.items[i].toLowerCase (); if (UI[item]) { this.items[i] = new UI[item] (this.editor); this.items[i].className += " edui-shortcutsubmenu "; } } } } , setOpacity : function (el , value) { if (browser.ie && browser.version < 9) { el.style.filter = "alpha(opacity = " + parseFloat (value) * 100 + ");" } else { el.style.opacity = value; } } , getSubMenuMark : function () { isSubMenuShow = false; var layerEle = uiUtils.getFixedLayer (); var list = domUtils.getElementsByTagName (layerEle , "div" , function (node) { return domUtils.hasClass (node , "edui-shortcutsubmenu edui-popup") }); for (var i = 0, node ; node = list[i++] ;) { if (node.style.display != "none") { isSubMenuShow = true; } } return isSubMenuShow; } , show : function (e , hasContextmenu) { var me = this, offset = {}, el = this.getDom (), fixedlayer = uiUtils.getFixedLayer (); function setPos (offset) { if (offset.left < 0) { offset.left = 0; } if (offset.top < 0) { offset.top = 0; } el.style.cssText = "position:absolute;left:" + offset.left + "px;top:" + offset.top + "px;"; } function setPosByCxtMenu (menu) { if (!menu.tagName) { menu = menu.getDom (); } offset.left = parseInt (menu.style.left); offset.top = parseInt (menu.style.top); offset.top -= el.offsetHeight + 15; setPos (offset); } me.eventType = e.type; el.style.cssText = "display:block;left:-9999px"; if (e.type == "contextmenu" && hasContextmenu) { var menu = domUtils.getElementsByTagName (fixedlayer , "div" , "edui-contextmenu")[0]; if (menu) { setPosByCxtMenu (menu) } else { me.editor.addListener ("aftershowcontextmenu" , function (type , menu) { setPosByCxtMenu (menu); }); } } else { offset = uiUtils.getViewportOffsetByEvent (e); offset.top -= el.offsetHeight + me.SPACE; offset.left += me.SPACE + 20; setPos (offset); me.setOpacity (el , 0.2); } me.isHidden = false; me.left = e.screenX + el.offsetWidth / 2 - me.SPACE; me.top = e.screenY - (el.offsetHeight / 2) - me.SPACE; if (me.editor) { el.style.zIndex = me.editor.container.style.zIndex * 1 + 10; fixedlayer.style.zIndex = el.style.zIndex - 1; } } , hide : function () { if (this.getDom ()) { this.getDom ().style.display = "none"; } this.isHidden = true; } , postRender : function () { if (utils.isArray (this.items)) { for (var i = 0, item ; item = this.items[i++] ;) { item.postRender (); } } } , getHtmlTpl : function () { var buff; if (utils.isArray (this.items)) { buff = []; for (var i = 0 ; i < this.items.length ; i++) { buff[i] = this.items[i].renderHtml (); } buff = buff.join (""); } else { buff = this.items; } return '
    ' + buff + '
    '; } }; utils.inherits (ShortCutMenu , UIBase); function hideAllMenu (e) { var tgt = e.target || e.srcElement, cur = domUtils.findParent (tgt , function (node) { return domUtils.hasClass (node , "edui-shortcutmenu") || domUtils.hasClass (node , "edui-popup"); } , true); if (!cur) { for (var i = 0, menu ; menu = allMenus[i++] ;) { menu.hide () } } } domUtils.on (document , 'mousedown' , function (e) { hideAllMenu (e); }); domUtils.on (window , 'scroll' , function (e) { hideAllMenu (e); }); }) (); // ui/breakline.js (function (){ var utils = baidu.editor.utils, UIBase = baidu.editor.ui.UIBase, Breakline = baidu.editor.ui.Breakline = function (options){ this.initOptions(options); this.initSeparator(); }; Breakline.prototype = { uiName: 'Breakline', initSeparator: function (){ this.initUIBase(); }, getHtmlTpl: function (){ return '
    '; } }; utils.inherits(Breakline, UIBase); })(); // ui/message.js ///import core ///import uicore (function () { var utils = baidu.editor.utils, domUtils = baidu.editor.dom.domUtils, UIBase = baidu.editor.ui.UIBase, Message = baidu.editor.ui.Message = function (options){ this.initOptions(options); this.initMessage(); }; Message.prototype = { initMessage: function (){ this.initUIBase(); }, getHtmlTpl: function (){ return '
    ' + '
    ×
    ' + '
    ' + ' ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    '; }, reset: function(opt){ var me = this; if (!opt.keepshow) { clearTimeout(this.timer); me.timer = setTimeout(function(){ me.hide(); }, opt.timeout || 4000); } opt.content !== undefined && me.setContent(opt.content); opt.type !== undefined && me.setType(opt.type); me.show(); }, postRender: function(){ var me = this, closer = this.getDom('closer'); closer && domUtils.on(closer, 'click', function(){ me.hide(); }); }, setContent: function(content){ this.getDom('content').innerHTML = content; }, setType: function(type){ type = type || 'info'; var body = this.getDom('body'); body.className = body.className.replace(/edui-message-type-[\w-]+/, 'edui-message-type-' + type); }, getContent: function(){ return this.getDom('content').innerHTML; }, getType: function(){ var arr = this.getDom('body').match(/edui-message-type-([\w-]+)/); return arr ? arr[1]:''; }, show: function (){ this.getDom().style.display = 'block'; }, hide: function (){ var dom = this.getDom(); if (dom) { dom.style.display = 'none'; dom.parentNode && dom.parentNode.removeChild(dom); } } }; utils.inherits(Message, UIBase); })(); // adapter/editorui.js //ui跟编辑器的适配層 //那个按钮弹出是dialog,是下拉筐等都是在这个js中配置 //自己写的ui也要在这里配置,放到baidu.editor.ui下边,当编辑器实例化的时候会根据ueditor.config中的toolbars找到相应的进行实例化 (function () { var utils = baidu.editor.utils; var editorui = baidu.editor.ui; var _Dialog = editorui.Dialog; editorui.buttons = {}; editorui.Dialog = function (options) { var dialog = new _Dialog(options); dialog.addListener('hide', function () { if (dialog.editor) { var editor = dialog.editor; try { if (browser.gecko) { var y = editor.window.scrollY, x = editor.window.scrollX; editor.body.focus(); editor.window.scrollTo(x, y); } else { editor.focus(); } } catch (ex) { } } }); return dialog; }; var iframeUrlMap = { 'anchor':'~/dialogs/anchor/anchor.html', 'insertimage':'~/dialogs/image/image.html', 'link':'~/dialogs/link/link.html', 'spechars':'~/dialogs/spechars/spechars.html', 'searchreplace':'~/dialogs/searchreplace/searchreplace.html', 'map':'~/dialogs/map/map.html', 'gmap':'~/dialogs/gmap/gmap.html', 'insertvideo':'~/dialogs/video/video.html', 'help':'~/dialogs/help/help.html', 'preview':'~/dialogs/preview/preview.html', 'emotion':'~/dialogs/emotion/emotion.html', 'wordimage':'~/dialogs/wordimage/wordimage.html', 'attachment':'~/dialogs/attachment/attachment.html', 'insertframe':'~/dialogs/insertframe/insertframe.html', 'edittip':'~/dialogs/table/edittip.html', 'edittable':'~/dialogs/table/edittable.html', 'edittd':'~/dialogs/table/edittd.html', 'webapp':'~/dialogs/webapp/webapp.html', 'snapscreen':'~/dialogs/snapscreen/snapscreen.html', 'scrawl':'~/dialogs/scrawl/scrawl.html', 'music':'~/dialogs/music/music.html', 'template':'~/dialogs/template/template.html', 'background':'~/dialogs/background/background.html', 'charts': '~/dialogs/charts/charts.html' }; //为工具栏添加按钮,以下都是统一的按钮触发命令,所以写在一起 var btnCmds = ['undo', 'redo', 'formatmatch', 'bold', 'italic', 'underline', 'fontborder', 'touppercase', 'tolowercase', 'strikethrough', 'subscript', 'superscript', 'source', 'indent', 'outdent', 'blockquote', 'pasteplain', 'pagebreak', 'selectall', 'print','horizontal', 'removeformat', 'time', 'date', 'unlink', 'insertparagraphbeforetable', 'insertrow', 'insertcol', 'mergeright', 'mergedown', 'deleterow', 'deletecol', 'splittorows', 'splittocols', 'splittocells', 'mergecells', 'deletetable', 'drafts']; for (var i = 0, ci; ci = btnCmds[i++];) { ci = ci.toLowerCase(); editorui[ci] = function (cmd) { return function (editor) { var ui = new editorui.Button({ className:'edui-for-' + cmd, title:editor.options.labelMap[cmd] || editor.getLang("labelMap." + cmd) || '', onclick:function () { editor.execCommand(cmd); }, theme:editor.options.theme, showText:false }); editorui.buttons[cmd] = ui; editor.addListener('selectionchange', function (type, causeByUi, uiReady) { var state = editor.queryCommandState(cmd); if (state == -1) { ui.setDisabled(true); ui.setChecked(false); } else { if (!uiReady) { ui.setDisabled(false); ui.setChecked(state); } } }); return ui; }; }(ci); } //清除文档 editorui.cleardoc = function (editor) { var ui = new editorui.Button({ className:'edui-for-cleardoc', title:editor.options.labelMap.cleardoc || editor.getLang("labelMap.cleardoc") || '', theme:editor.options.theme, onclick:function () { if (confirm(editor.getLang("confirmClear"))) { editor.execCommand('cleardoc'); } } }); editorui.buttons["cleardoc"] = ui; editor.addListener('selectionchange', function () { ui.setDisabled(editor.queryCommandState('cleardoc') == -1); }); return ui; }; //排版,图片排版,文字方向 var typeset = { 'justify':['left', 'right', 'center', 'justify'], 'imagefloat':['none', 'left', 'center', 'right'], 'directionality':['ltr', 'rtl'] }; for (var p in typeset) { (function (cmd, val) { for (var i = 0, ci; ci = val[i++];) { (function (cmd2) { editorui[cmd.replace('float', '') + cmd2] = function (editor) { var ui = new editorui.Button({ className:'edui-for-' + cmd.replace('float', '') + cmd2, title:editor.options.labelMap[cmd.replace('float', '') + cmd2] || editor.getLang("labelMap." + cmd.replace('float', '') + cmd2) || '', theme:editor.options.theme, onclick:function () { editor.execCommand(cmd, cmd2); } }); editorui.buttons[cmd] = ui; editor.addListener('selectionchange', function (type, causeByUi, uiReady) { ui.setDisabled(editor.queryCommandState(cmd) == -1); ui.setChecked(editor.queryCommandValue(cmd) == cmd2 && !uiReady); }); return ui; }; })(ci) } })(p, typeset[p]) } //字体颜色和背景颜色 for (var i = 0, ci; ci = ['backcolor', 'forecolor'][i++];) { editorui[ci] = function (cmd) { return function (editor) { var ui = new editorui.ColorButton({ className:'edui-for-' + cmd, color:'default', title:editor.options.labelMap[cmd] || editor.getLang("labelMap." + cmd) || '', editor:editor, onpickcolor:function (t, color) { editor.execCommand(cmd, color); }, onpicknocolor:function () { editor.execCommand(cmd, 'default'); this.setColor('transparent'); this.color = 'default'; }, onbuttonclick:function () { editor.execCommand(cmd, this.color); } }); editorui.buttons[cmd] = ui; editor.addListener('selectionchange', function () { ui.setDisabled(editor.queryCommandState(cmd) == -1); }); return ui; }; }(ci); } var dialogBtns = { noOk:['searchreplace', 'help', 'spechars', 'webapp','preview'], ok:['attachment', 'anchor', 'link', 'insertimage', 'map', 'gmap', 'insertframe', 'wordimage', 'insertvideo', 'insertframe', 'edittip', 'edittable', 'edittd', 'scrawl', 'template', 'music', 'background', 'charts'] }; for (var p in dialogBtns) { (function (type, vals) { for (var i = 0, ci; ci = vals[i++];) { //todo opera下存在问题 if (browser.opera && ci === "searchreplace") { continue; } (function (cmd) { editorui[cmd] = function (editor, iframeUrl, title) { iframeUrl = iframeUrl || (editor.options.iframeUrlMap || {})[cmd] || iframeUrlMap[cmd]; title = editor.options.labelMap[cmd] || editor.getLang("labelMap." + cmd) || ''; var dialog; //没有iframeUrl不创建dialog if (iframeUrl) { dialog = new editorui.Dialog(utils.extend({ iframeUrl:editor.ui.mapUrl(iframeUrl), editor:editor, className:'edui-for-' + cmd, title:title, holdScroll: cmd === 'insertimage', fullscreen: /charts|preview/.test(cmd), closeDialog:editor.getLang("closeDialog") }, type == 'ok' ? { buttons:[ { className:'edui-okbutton', label:editor.getLang("ok"), editor:editor, onclick:function () { dialog.close(true); } }, { className:'edui-cancelbutton', label:editor.getLang("cancel"), editor:editor, onclick:function () { dialog.close(false); } } ] } : {})); editor.ui._dialogs[cmd + "Dialog"] = dialog; } var ui = new editorui.Button({ className:'edui-for-' + cmd, title:title, onclick:function () { if (dialog) { switch (cmd) { case "wordimage": var images = editor.execCommand("wordimage"); if (images && images.length) { dialog.render(); dialog.open(); } break; case "scrawl": if (editor.queryCommandState("scrawl") != -1) { dialog.render(); dialog.open(); } break; default: dialog.render(); dialog.open(); } } }, theme:editor.options.theme, disabled:(cmd == 'scrawl' && editor.queryCommandState("scrawl") == -1) || ( cmd == 'charts' ) }); editorui.buttons[cmd] = ui; editor.addListener('selectionchange', function () { //只存在于右键菜单而无工具栏按钮的ui不需要检测状态 var unNeedCheckState = {'edittable':1}; if (cmd in unNeedCheckState)return; var state = editor.queryCommandState(cmd); if (ui.getDom()) { ui.setDisabled(state == -1); ui.setChecked(state); } }); return ui; }; })(ci.toLowerCase()) } })(p, dialogBtns[p]); } editorui.snapscreen = function (editor, iframeUrl, title) { title = editor.options.labelMap['snapscreen'] || editor.getLang("labelMap.snapscreen") || ''; var ui = new editorui.Button({ className:'edui-for-snapscreen', title:title, onclick:function () { editor.execCommand("snapscreen"); }, theme:editor.options.theme }); editorui.buttons['snapscreen'] = ui; iframeUrl = iframeUrl || (editor.options.iframeUrlMap || {})["snapscreen"] || iframeUrlMap["snapscreen"]; if (iframeUrl) { var dialog = new editorui.Dialog({ iframeUrl:editor.ui.mapUrl(iframeUrl), editor:editor, className:'edui-for-snapscreen', title:title, buttons:[ { className:'edui-okbutton', label:editor.getLang("ok"), editor:editor, onclick:function () { dialog.close(true); } }, { className:'edui-cancelbutton', label:editor.getLang("cancel"), editor:editor, onclick:function () { dialog.close(false); } } ] }); dialog.render(); editor.ui._dialogs["snapscreenDialog"] = dialog; } editor.addListener('selectionchange', function () { ui.setDisabled(editor.queryCommandState('snapscreen') == -1); }); return ui; }; editorui.insertcode = function (editor, list, title) { list = editor.options['insertcode'] || []; title = editor.options.labelMap['insertcode'] || editor.getLang("labelMap.insertcode") || ''; // if (!list.length) return; var items = []; utils.each(list,function(key,val){ items.push({ label:key, value:val, theme:editor.options.theme, renderLabelHtml:function () { return '
    ' + (this.label || '') + '
    '; } }); }); var ui = new editorui.Combox({ editor:editor, items:items, onselect:function (t, index) { editor.execCommand('insertcode', this.items[index].value); }, onbuttonclick:function () { this.showPopup(); }, title:title, initValue:title, className:'edui-for-insertcode', indexByValue:function (value) { if (value) { for (var i = 0, ci; ci = this.items[i]; i++) { if (ci.value.indexOf(value) != -1) return i; } } return -1; } }); editorui.buttons['insertcode'] = ui; editor.addListener('selectionchange', function (type, causeByUi, uiReady) { if (!uiReady) { var state = editor.queryCommandState('insertcode'); if (state == -1) { ui.setDisabled(true); } else { ui.setDisabled(false); var value = editor.queryCommandValue('insertcode'); if(!value){ ui.setValue(title); return; } //trace:1871 ie下从源码模式切换回来时,字体会带单引号,而且会有逗号 value && (value = value.replace(/['"]/g, '').split(',')[0]); ui.setValue(value); } } }); return ui; }; editorui.fontfamily = function (editor, list, title) { list = editor.options['fontfamily'] || []; title = editor.options.labelMap['fontfamily'] || editor.getLang("labelMap.fontfamily") || ''; if (!list.length) return; for (var i = 0, ci, items = []; ci = list[i]; i++) { var langLabel = editor.getLang('fontfamily')[ci.name] || ""; (function (key, val) { items.push({ label:key, value:val, theme:editor.options.theme, renderLabelHtml:function () { return '
    ' + (this.label || '') + '
    '; } }); })(ci.label || langLabel, ci.val) } var ui = new editorui.Combox({ editor:editor, items:items, onselect:function (t, index) { editor.execCommand('FontFamily', this.items[index].value); }, onbuttonclick:function () { this.showPopup(); }, title:title, initValue:title, className:'edui-for-fontfamily', indexByValue:function (value) { if (value) { for (var i = 0, ci; ci = this.items[i]; i++) { if (ci.value.indexOf(value) != -1) return i; } } return -1; } }); editorui.buttons['fontfamily'] = ui; editor.addListener('selectionchange', function (type, causeByUi, uiReady) { if (!uiReady) { var state = editor.queryCommandState('FontFamily'); if (state == -1) { ui.setDisabled(true); } else { ui.setDisabled(false); var value = editor.queryCommandValue('FontFamily'); //trace:1871 ie下从源码模式切换回来时,字体会带单引号,而且会有逗号 value && (value = value.replace(/['"]/g, '').split(',')[0]); ui.setValue(value); } } }); return ui; }; editorui.fontsize = function (editor, list, title) { title = editor.options.labelMap['fontsize'] || editor.getLang("labelMap.fontsize") || ''; list = list || editor.options['fontsize'] || []; if (!list.length) return; var items = []; for (var i = 0; i < list.length; i++) { var size = list[i] + 'px'; items.push({ label:size, value:size, theme:editor.options.theme, renderLabelHtml:function () { return '
    ' + (this.label || '') + '
    '; } }); } var ui = new editorui.Combox({ editor:editor, items:items, title:title, initValue:title, onselect:function (t, index) { editor.execCommand('FontSize', this.items[index].value); }, onbuttonclick:function () { this.showPopup(); }, className:'edui-for-fontsize' }); editorui.buttons['fontsize'] = ui; editor.addListener('selectionchange', function (type, causeByUi, uiReady) { if (!uiReady) { var state = editor.queryCommandState('FontSize'); if (state == -1) { ui.setDisabled(true); } else { ui.setDisabled(false); ui.setValue(editor.queryCommandValue('FontSize')); } } }); return ui; }; editorui.paragraph = function (editor, list, title) { title = editor.options.labelMap['paragraph'] || editor.getLang("labelMap.paragraph") || ''; list = editor.options['paragraph'] || []; if (utils.isEmptyObject(list)) return; var items = []; for (var i in list) { items.push({ value:i, label:list[i] || editor.getLang("paragraph")[i], theme:editor.options.theme, renderLabelHtml:function () { return '
    ' + (this.label || '') + '
    '; } }) } var ui = new editorui.Combox({ editor:editor, items:items, title:title, initValue:title, className:'edui-for-paragraph', onselect:function (t, index) { editor.execCommand('Paragraph', this.items[index].value); }, onbuttonclick:function () { this.showPopup(); } }); editorui.buttons['paragraph'] = ui; editor.addListener('selectionchange', function (type, causeByUi, uiReady) { if (!uiReady) { var state = editor.queryCommandState('Paragraph'); if (state == -1) { ui.setDisabled(true); } else { ui.setDisabled(false); var value = editor.queryCommandValue('Paragraph'); var index = ui.indexByValue(value); if (index != -1) { ui.setValue(value); } else { ui.setValue(ui.initValue); } } } }); return ui; }; //自定义标题 editorui.customstyle = function (editor) { var list = editor.options['customstyle'] || [], title = editor.options.labelMap['customstyle'] || editor.getLang("labelMap.customstyle") || ''; if (!list.length)return; var langCs = editor.getLang('customstyle'); for (var i = 0, items = [], t; t = list[i++];) { (function (t) { var ck = {}; ck.label = t.label ? t.label : langCs[t.name]; ck.style = t.style; ck.className = t.className; ck.tag = t.tag; items.push({ label:ck.label, value:ck, theme:editor.options.theme, renderLabelHtml:function () { return '
    ' + '<' + ck.tag + ' ' + (ck.className ? ' class="' + ck.className + '"' : "") + (ck.style ? ' style="' + ck.style + '"' : "") + '>' + ck.label + "<\/" + ck.tag + ">" + '
    '; } }); })(t); } var ui = new editorui.Combox({ editor:editor, items:items, title:title, initValue:title, className:'edui-for-customstyle', onselect:function (t, index) { editor.execCommand('customstyle', this.items[index].value); }, onbuttonclick:function () { this.showPopup(); }, indexByValue:function (value) { for (var i = 0, ti; ti = this.items[i++];) { if (ti.label == value) { return i - 1 } } return -1; } }); editorui.buttons['customstyle'] = ui; editor.addListener('selectionchange', function (type, causeByUi, uiReady) { if (!uiReady) { var state = editor.queryCommandState('customstyle'); if (state == -1) { ui.setDisabled(true); } else { ui.setDisabled(false); var value = editor.queryCommandValue('customstyle'); var index = ui.indexByValue(value); if (index != -1) { ui.setValue(value); } else { ui.setValue(ui.initValue); } } } }); return ui; }; editorui.inserttable = function (editor, iframeUrl, title) { title = editor.options.labelMap['inserttable'] || editor.getLang("labelMap.inserttable") || ''; var ui = new editorui.TableButton({ editor:editor, title:title, className:'edui-for-inserttable', onpicktable:function (t, numCols, numRows) { editor.execCommand('InsertTable', {numRows:numRows, numCols:numCols, border:1}); }, onbuttonclick:function () { this.showPopup(); } }); editorui.buttons['inserttable'] = ui; editor.addListener('selectionchange', function () { ui.setDisabled(editor.queryCommandState('inserttable') == -1); }); return ui; }; editorui.lineheight = function (editor) { var val = editor.options.lineheight || []; if (!val.length)return; for (var i = 0, ci, items = []; ci = val[i++];) { items.push({ //todo:写死了 label:ci, value:ci, theme:editor.options.theme, onclick:function () { editor.execCommand("lineheight", this.value); } }) } var ui = new editorui.MenuButton({ editor:editor, className:'edui-for-lineheight', title:editor.options.labelMap['lineheight'] || editor.getLang("labelMap.lineheight") || '', items:items, onbuttonclick:function () { var value = editor.queryCommandValue('LineHeight') || this.value; editor.execCommand("LineHeight", value); } }); editorui.buttons['lineheight'] = ui; editor.addListener('selectionchange', function () { var state = editor.queryCommandState('LineHeight'); if (state == -1) { ui.setDisabled(true); } else { ui.setDisabled(false); var value = editor.queryCommandValue('LineHeight'); value && ui.setValue((value + '').replace(/cm/, '')); ui.setChecked(state) } }); return ui; }; var rowspacings = ['top', 'bottom']; for (var r = 0, ri; ri = rowspacings[r++];) { (function (cmd) { editorui['rowspacing' + cmd] = function (editor) { var val = editor.options['rowspacing' + cmd] || []; if (!val.length) return null; for (var i = 0, ci, items = []; ci = val[i++];) { items.push({ label:ci, value:ci, theme:editor.options.theme, onclick:function () { editor.execCommand("rowspacing", this.value, cmd); } }) } var ui = new editorui.MenuButton({ editor:editor, className:'edui-for-rowspacing' + cmd, title:editor.options.labelMap['rowspacing' + cmd] || editor.getLang("labelMap.rowspacing" + cmd) || '', items:items, onbuttonclick:function () { var value = editor.queryCommandValue('rowspacing', cmd) || this.value; editor.execCommand("rowspacing", value, cmd); } }); editorui.buttons[cmd] = ui; editor.addListener('selectionchange', function () { var state = editor.queryCommandState('rowspacing', cmd); if (state == -1) { ui.setDisabled(true); } else { ui.setDisabled(false); var value = editor.queryCommandValue('rowspacing', cmd); value && ui.setValue((value + '').replace(/%/, '')); ui.setChecked(state) } }); return ui; } })(ri) } //有序,无序列表 var lists = ['insertorderedlist', 'insertunorderedlist']; for (var l = 0, cl; cl = lists[l++];) { (function (cmd) { editorui[cmd] = function (editor) { var vals = editor.options[cmd], _onMenuClick = function () { editor.execCommand(cmd, this.value); }, items = []; for (var i in vals) { items.push({ label:vals[i] || editor.getLang()[cmd][i] || "", value:i, theme:editor.options.theme, onclick:_onMenuClick }) } var ui = new editorui.MenuButton({ editor:editor, className:'edui-for-' + cmd, title:editor.getLang("labelMap." + cmd) || '', 'items':items, onbuttonclick:function () { var value = editor.queryCommandValue(cmd) || this.value; editor.execCommand(cmd, value); } }); editorui.buttons[cmd] = ui; editor.addListener('selectionchange', function () { var state = editor.queryCommandState(cmd); if (state == -1) { ui.setDisabled(true); } else { ui.setDisabled(false); var value = editor.queryCommandValue(cmd); ui.setValue(value); ui.setChecked(state) } }); return ui; }; })(cl) } editorui.fullscreen = function (editor, title) { title = editor.options.labelMap['fullscreen'] || editor.getLang("labelMap.fullscreen") || ''; var ui = new editorui.Button({ className:'edui-for-fullscreen', title:title, theme:editor.options.theme, onclick:function () { if (editor.ui) { editor.ui.setFullScreen(!editor.ui.isFullScreen()); } this.setChecked(editor.ui.isFullScreen()); } }); editorui.buttons['fullscreen'] = ui; editor.addListener('selectionchange', function () { var state = editor.queryCommandState('fullscreen'); ui.setDisabled(state == -1); ui.setChecked(editor.ui.isFullScreen()); }); return ui; }; // 表情 editorui["emotion"] = function (editor, iframeUrl) { var cmd = "emotion"; var ui = new editorui.MultiMenuPop({ title:editor.options.labelMap[cmd] || editor.getLang("labelMap." + cmd + "") || '', editor:editor, className:'edui-for-' + cmd, iframeUrl:editor.ui.mapUrl(iframeUrl || (editor.options.iframeUrlMap || {})[cmd] || iframeUrlMap[cmd]) }); editorui.buttons[cmd] = ui; editor.addListener('selectionchange', function () { ui.setDisabled(editor.queryCommandState(cmd) == -1) }); return ui; }; editorui.autotypeset = function (editor) { var ui = new editorui.AutoTypeSetButton({ editor:editor, title:editor.options.labelMap['autotypeset'] || editor.getLang("labelMap.autotypeset") || '', className:'edui-for-autotypeset', onbuttonclick:function () { editor.execCommand('autotypeset') } }); editorui.buttons['autotypeset'] = ui; editor.addListener('selectionchange', function () { ui.setDisabled(editor.queryCommandState('autotypeset') == -1); }); return ui; }; /* 简单上传插件 */ editorui["simpleupload"] = function (editor) { var name = 'simpleupload', ui = new editorui.Button({ className:'edui-for-' + name, title:editor.options.labelMap[name] || editor.getLang("labelMap." + name) || '', onclick:function () {}, theme:editor.options.theme, showText:false }); editorui.buttons[name] = ui; editor.addListener('ready', function() { var b = ui.getDom('body'), iconSpan = b.children[0]; editor.fireEvent('simpleuploadbtnready', iconSpan); }); editor.addListener('selectionchange', function (type, causeByUi, uiReady) { var state = editor.queryCommandState(name); if (state == -1) { ui.setDisabled(true); ui.setChecked(false); } else { if (!uiReady) { ui.setDisabled(false); ui.setChecked(state); } } }); return ui; }; })(); // adapter/editor.js ///import core ///commands 全屏 ///commandsName FullScreen ///commandsTitle 全屏 (function () { var utils = baidu.editor.utils, uiUtils = baidu.editor.ui.uiUtils, UIBase = baidu.editor.ui.UIBase, domUtils = baidu.editor.dom.domUtils; var nodeStack = []; function EditorUI(options) { this.initOptions(options); this.initEditorUI(); } EditorUI.prototype = { uiName:'editor', initEditorUI:function () { this.editor.ui = this; this._dialogs = {}; this.initUIBase(); this._initToolbars(); var editor = this.editor, me = this; editor.addListener('ready', function () { //提供getDialog方法 editor.getDialog = function (name) { return editor.ui._dialogs[name + "Dialog"]; }; domUtils.on(editor.window, 'scroll', function (evt) { baidu.editor.ui.Popup.postHide(evt); }); //提供编辑器实时宽高(全屏时宽高不变化) editor.ui._actualFrameWidth = editor.options.initialFrameWidth; UE.browser.ie && UE.browser.version === 6 && editor.container.ownerDocument.execCommand("BackgroundImageCache", false, true); //display bottom-bar label based on config if (editor.options.elementPathEnabled) { editor.ui.getDom('elementpath').innerHTML = '
    ' + editor.getLang("elementPathTip") + ':
    '; } if (editor.options.wordCount) { function countFn() { setCount(editor,me); domUtils.un(editor.document, "click", arguments.callee); } domUtils.on(editor.document, "click", countFn); editor.ui.getDom('wordcount').innerHTML = editor.getLang("wordCountTip"); } editor.ui._scale(); if (editor.options.scaleEnabled) { if (editor.autoHeightEnabled) { editor.disableAutoHeight(); } me.enableScale(); } else { me.disableScale(); } if (!editor.options.elementPathEnabled && !editor.options.wordCount && !editor.options.scaleEnabled) { editor.ui.getDom('elementpath').style.display = "none"; editor.ui.getDom('wordcount').style.display = "none"; editor.ui.getDom('scale').style.display = "none"; } if (!editor.selection.isFocus())return; editor.fireEvent('selectionchange', false, true); }); editor.addListener('mousedown', function (t, evt) { var el = evt.target || evt.srcElement; baidu.editor.ui.Popup.postHide(evt, el); baidu.editor.ui.ShortCutMenu.postHide(evt); }); editor.addListener("delcells", function () { if (UE.ui['edittip']) { new UE.ui['edittip'](editor); } editor.getDialog('edittip').open(); }); var pastePop, isPaste = false, timer; editor.addListener("afterpaste", function () { if(editor.queryCommandState('pasteplain')) return; if(baidu.editor.ui.PastePicker){ pastePop = new baidu.editor.ui.Popup({ content:new baidu.editor.ui.PastePicker({editor:editor}), editor:editor, className:'edui-wordpastepop' }); pastePop.render(); } isPaste = true; }); editor.addListener("afterinserthtml", function () { clearTimeout(timer); timer = setTimeout(function () { if (pastePop && (isPaste || editor.ui._isTransfer)) { if(pastePop.isHidden()){ var span = domUtils.createElement(editor.document, 'span', { 'style':"line-height:0px;", 'innerHTML':'\ufeff' }), range = editor.selection.getRange(); range.insertNode(span); var tmp= getDomNode(span, 'firstChild', 'previousSibling'); tmp && pastePop.showAnchor(tmp.nodeType == 3 ? tmp.parentNode : tmp); domUtils.remove(span); }else{ pastePop.show(); } delete editor.ui._isTransfer; isPaste = false; } }, 200) }); editor.addListener('contextmenu', function (t, evt) { baidu.editor.ui.Popup.postHide(evt); }); editor.addListener('keydown', function (t, evt) { if (pastePop) pastePop.dispose(evt); var keyCode = evt.keyCode || evt.which; if(evt.altKey&&keyCode==90){ UE.ui.buttons['fullscreen'].onclick(); } }); editor.addListener('wordcount', function (type) { setCount(this,me); }); function setCount(editor,ui) { editor.setOpt({ wordCount:true, maximumWords:10000, wordCountMsg:editor.options.wordCountMsg || editor.getLang("wordCountMsg"), wordOverFlowMsg:editor.options.wordOverFlowMsg || editor.getLang("wordOverFlowMsg") }); var opt = editor.options, max = opt.maximumWords, msg = opt.wordCountMsg , errMsg = opt.wordOverFlowMsg, countDom = ui.getDom('wordcount'); if (!opt.wordCount) { return; } var count = editor.getContentLength(true); if (count > max) { countDom.innerHTML = errMsg; editor.fireEvent("wordcountoverflow"); } else { countDom.innerHTML = msg.replace("{#leave}", max - count).replace("{#count}", count); } } editor.addListener('selectionchange', function () { if (editor.options.elementPathEnabled) { me[(editor.queryCommandState('elementpath') == -1 ? 'dis' : 'en') + 'ableElementPath']() } if (editor.options.scaleEnabled) { me[(editor.queryCommandState('scale') == -1 ? 'dis' : 'en') + 'ableScale'](); } }); var popup = new baidu.editor.ui.Popup({ editor:editor, content:'', className:'edui-bubble', _onEditButtonClick:function () { this.hide(); editor.ui._dialogs.linkDialog.open(); }, _onImgEditButtonClick:function (name) { this.hide(); editor.ui._dialogs[name] && editor.ui._dialogs[name].open(); }, _onImgSetFloat:function (value) { this.hide(); editor.execCommand("imagefloat", value); }, _setIframeAlign:function (value) { var frame = popup.anchorEl; var newFrame = frame.cloneNode(true); switch (value) { case -2: newFrame.setAttribute("align", ""); break; case -1: newFrame.setAttribute("align", "left"); break; case 1: newFrame.setAttribute("align", "right"); break; } frame.parentNode.insertBefore(newFrame, frame); domUtils.remove(frame); popup.anchorEl = newFrame; popup.showAnchor(popup.anchorEl); }, _updateIframe:function () { var frame = editor._iframe = popup.anchorEl; if(domUtils.hasClass(frame, 'ueditor_baidumap')) { editor.selection.getRange().selectNode(frame).select(); editor.ui._dialogs.mapDialog.open(); popup.hide(); } else { editor.ui._dialogs.insertframeDialog.open(); popup.hide(); } }, _onRemoveButtonClick:function (cmdName) { editor.execCommand(cmdName); this.hide(); }, queryAutoHide:function (el) { if (el && el.ownerDocument == editor.document) { if (el.tagName.toLowerCase() == 'img' || domUtils.findParentByTagName(el, 'a', true)) { return el !== popup.anchorEl; } } return baidu.editor.ui.Popup.prototype.queryAutoHide.call(this, el); } }); popup.render(); if (editor.options.imagePopup) { editor.addListener('mouseover', function (t, evt) { evt = evt || window.event; var el = evt.target || evt.srcElement; if (editor.ui._dialogs.insertframeDialog && /iframe/ig.test(el.tagName)) { var html = popup.formatHtml( '' + editor.getLang("property") + ': ' + editor.getLang("default") + '  ' + editor.getLang("justifyleft") + '  ' + editor.getLang("justifyright") + '  ' + ' ' + editor.getLang("modify") + ''); if (html) { popup.getDom('content').innerHTML = html; popup.anchorEl = el; popup.showAnchor(popup.anchorEl); } else { popup.hide(); } } }); editor.addListener('selectionchange', function (t, causeByUi) { if (!causeByUi) return; var html = '', str = "", img = editor.selection.getRange().getClosedNode(), dialogs = editor.ui._dialogs; if (img && img.tagName == 'IMG') { var dialogName = 'insertimageDialog'; if (img.className.indexOf("edui-faked-video") != -1 || img.className.indexOf("edui-upload-video") != -1) { dialogName = "insertvideoDialog" } if (img.className.indexOf("edui-faked-webapp") != -1) { dialogName = "webappDialog" } if (img.src.indexOf("http://api.map.baidu.com") != -1) { dialogName = "mapDialog" } if (img.className.indexOf("edui-faked-music") != -1) { dialogName = "musicDialog" } if (img.src.indexOf("http://maps.google.com/maps/api/staticmap") != -1) { dialogName = "gmapDialog" } if (img.getAttribute("anchorname")) { dialogName = "anchorDialog"; html = popup.formatHtml( '' + editor.getLang("property") + ': ' + editor.getLang("modify") + '  ' + '' + editor.getLang("delete") + ''); } if (img.getAttribute("word_img")) { //todo 放到dialog去做查询 editor.word_img = [img.getAttribute("word_img")]; dialogName = "wordimageDialog" } if(domUtils.hasClass(img, 'loadingclass') || domUtils.hasClass(img, 'loaderrorclass')) { dialogName = ""; } if (!dialogs[dialogName]) { return; } str = '' + editor.getLang("property") + ': '+ '' + editor.getLang("default") + '  ' + '' + editor.getLang("justifyleft") + '  ' + '' + editor.getLang("justifyright") + '  ' + '' + editor.getLang("justifycenter") + '  '+ '' + editor.getLang("modify") + ''; !html && (html = popup.formatHtml(str)) } if (editor.ui._dialogs.linkDialog) { var link = editor.queryCommandValue('link'); var url; if (link && (url = (link.getAttribute('_href') || link.getAttribute('href', 2)))) { var txt = url; if (url.length > 30) { txt = url.substring(0, 20) + "..."; } if (html) { html += '
    ' } html += popup.formatHtml( '' + editor.getLang("anthorMsg") + ': ' + txt + '' + ' ' + editor.getLang("modify") + '' + ' ' + editor.getLang("clear") + ''); popup.showAnchor(link); } } if (html) { popup.getDom('content').innerHTML = html; popup.anchorEl = img || link; popup.showAnchor(popup.anchorEl); } else { popup.hide(); } }); } }, _initToolbars:function () { var editor = this.editor; var toolbars = this.toolbars || []; var toolbarUis = []; for (var i = 0; i < toolbars.length; i++) { var toolbar = toolbars[i]; var toolbarUi = new baidu.editor.ui.Toolbar({theme:editor.options.theme}); for (var j = 0; j < toolbar.length; j++) { var toolbarItem = toolbar[j]; var toolbarItemUi = null; if (typeof toolbarItem == 'string') { toolbarItem = toolbarItem.toLowerCase(); if (toolbarItem == '|') { toolbarItem = 'Separator'; } if(toolbarItem == '||'){ toolbarItem = 'Breakline'; } if (baidu.editor.ui[toolbarItem]) { toolbarItemUi = new baidu.editor.ui[toolbarItem](editor); } //fullscreen这里单独处理一下,放到首行去 if (toolbarItem == 'fullscreen') { if (toolbarUis && toolbarUis[0]) { toolbarUis[0].items.splice(0, 0, toolbarItemUi); } else { toolbarItemUi && toolbarUi.items.splice(0, 0, toolbarItemUi); } continue; } } else { toolbarItemUi = toolbarItem; } if (toolbarItemUi && toolbarItemUi.id) { toolbarUi.add(toolbarItemUi); } } toolbarUis[i] = toolbarUi; } //接受外部定制的UI(修复因 utils.each 无法准确的循环出对象的全部元素而导致的自定义 UI 不符合预期的 BUG by HaoChuan9421) // utils.each(UE._customizeUI,function(obj,key){ // var itemUI,index; // if(obj.id && obj.id != editor.key){ // return false; // } // itemUI = obj.execFn.call(editor,editor,key); // if(itemUI){ // index = obj.index; // if(index === undefined){ // index = toolbarUi.items.length; // } // toolbarUi.add(itemUI,index) // } // }); for(var key in UE._customizeUI){ var obj = UE._customizeUI[key] var itemUI,index; if(!obj.id || obj.id == editor.key){ itemUI = obj.execFn.call(editor,editor,key); if(itemUI){ index = obj.index; if(index === undefined){ index = toolbarUi.items.length; } toolbarUi.add(itemUI,index) } } } this.toolbars = toolbarUis; }, getHtmlTpl:function () { return '
    ' + '
    ' + (this.toolbars.length ? '
    ' + this.renderToolbarBoxHtml() + '
    ' : '') + '' + '
    ' + '
    ' + '
    ' + '
    ' + //modify wdcount by matao '
    ' + '' + '' + '' + '
    ' + '
    ' + '
    '; }, showWordImageDialog:function () { this._dialogs['wordimageDialog'].open(); }, renderToolbarBoxHtml:function () { var buff = []; for (var i = 0; i < this.toolbars.length; i++) { buff.push(this.toolbars[i].renderHtml()); } return buff.join(''); }, setFullScreen:function (fullscreen) { var editor = this.editor, container = editor.container.parentNode.parentNode; if (this._fullscreen != fullscreen) { this._fullscreen = fullscreen; this.editor.fireEvent('beforefullscreenchange', fullscreen); if (baidu.editor.browser.gecko) { var bk = editor.selection.getRange().createBookmark(); } if (fullscreen) { while (container.tagName != "BODY") { var position = baidu.editor.dom.domUtils.getComputedStyle(container, "position"); nodeStack.push(position); container.style.position = "static"; container = container.parentNode; } this._bakHtmlOverflow = document.documentElement.style.overflow; this._bakBodyOverflow = document.body.style.overflow; this._bakAutoHeight = this.editor.autoHeightEnabled; this._bakScrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop); this._bakEditorContaninerWidth = editor.iframe.parentNode.offsetWidth; if (this._bakAutoHeight) { //当全屏时不能执行自动长高 editor.autoHeightEnabled = false; this.editor.disableAutoHeight(); } document.documentElement.style.overflow = 'hidden'; //修复,滚动条不收起的问题 window.scrollTo(0,window.scrollY); this._bakCssText = this.getDom().style.cssText; this._bakCssText1 = this.getDom('iframeholder').style.cssText; editor.iframe.parentNode.style.width = ''; this._updateFullScreen(); } else { while (container.tagName != "BODY") { container.style.position = nodeStack.shift(); container = container.parentNode; } this.getDom().style.cssText = this._bakCssText; this.getDom('iframeholder').style.cssText = this._bakCssText1; if (this._bakAutoHeight) { editor.autoHeightEnabled = true; this.editor.enableAutoHeight(); } document.documentElement.style.overflow = this._bakHtmlOverflow; document.body.style.overflow = this._bakBodyOverflow; editor.iframe.parentNode.style.width = this._bakEditorContaninerWidth + 'px'; window.scrollTo(0, this._bakScrollTop); } if (browser.gecko && editor.body.contentEditable === 'true') { var input = document.createElement('input'); document.body.appendChild(input); editor.body.contentEditable = false; setTimeout(function () { input.focus(); setTimeout(function () { editor.body.contentEditable = true; editor.fireEvent('fullscreenchanged', fullscreen); editor.selection.getRange().moveToBookmark(bk).select(true); baidu.editor.dom.domUtils.remove(input); fullscreen && window.scroll(0, 0); }, 0) }, 0) } if(editor.body.contentEditable === 'true'){ this.editor.fireEvent('fullscreenchanged', fullscreen); this.triggerLayout(); } } }, _updateFullScreen:function () { if (this._fullscreen) { var vpRect = uiUtils.getViewportRect(); this.getDom().style.cssText = 'border:0;position:absolute;left:0;top:' + (this.editor.options.topOffset || 0) + 'px;width:' + vpRect.width + 'px;height:' + vpRect.height + 'px;z-index:' + (this.getDom().style.zIndex * 1 + 100); uiUtils.setViewportOffset(this.getDom(), { left:0, top:this.editor.options.topOffset || 0 }); this.editor.setHeight(vpRect.height - this.getDom('toolbarbox').offsetHeight - this.getDom('bottombar').offsetHeight - (this.editor.options.topOffset || 0),true); //不手动调一下,会导致全屏失效 if(browser.gecko){ try{ window.onresize(); }catch(e){ } } } }, _updateElementPath:function () { var bottom = this.getDom('elementpath'), list; if (this.elementPathEnabled && (list = this.editor.queryCommandValue('elementpath'))) { var buff = []; for (var i = 0, ci; ci = list[i]; i++) { buff[i] = this.formatHtml('' + ci + ''); } bottom.innerHTML = '
    ' + this.editor.getLang("elementPathTip") + ': ' + buff.join(' > ') + '
    '; } else { bottom.style.display = 'none' } }, disableElementPath:function () { var bottom = this.getDom('elementpath'); bottom.innerHTML = ''; bottom.style.display = 'none'; this.elementPathEnabled = false; }, enableElementPath:function () { var bottom = this.getDom('elementpath'); bottom.style.display = ''; this.elementPathEnabled = true; this._updateElementPath(); }, _scale:function () { var doc = document, editor = this.editor, editorHolder = editor.container, editorDocument = editor.document, toolbarBox = this.getDom("toolbarbox"), bottombar = this.getDom("bottombar"), scale = this.getDom("scale"), scalelayer = this.getDom("scalelayer"); var isMouseMove = false, position = null, minEditorHeight = 0, minEditorWidth = editor.options.minFrameWidth, pageX = 0, pageY = 0, scaleWidth = 0, scaleHeight = 0; function down() { position = domUtils.getXY(editorHolder); if (!minEditorHeight) { minEditorHeight = editor.options.minFrameHeight + toolbarBox.offsetHeight + bottombar.offsetHeight; } scalelayer.style.cssText = "position:absolute;left:0;display:;top:0;background-color:#41ABFF;opacity:0.4;filter: Alpha(opacity=40);width:" + editorHolder.offsetWidth + "px;height:" + editorHolder.offsetHeight + "px;z-index:" + (editor.options.zIndex + 1); domUtils.on(doc, "mousemove", move); domUtils.on(editorDocument, "mouseup", up); domUtils.on(doc, "mouseup", up); } var me = this; //by xuheng 全屏时关掉缩放 this.editor.addListener('fullscreenchanged', function (e, fullScreen) { if (fullScreen) { me.disableScale(); } else { if (me.editor.options.scaleEnabled) { me.enableScale(); var tmpNode = me.editor.document.createElement('span'); me.editor.body.appendChild(tmpNode); me.editor.body.style.height = Math.max(domUtils.getXY(tmpNode).y, me.editor.iframe.offsetHeight - 20) + 'px'; domUtils.remove(tmpNode) } } }); function move(event) { clearSelection(); var e = event || window.event; pageX = e.pageX || (doc.documentElement.scrollLeft + e.clientX); pageY = e.pageY || (doc.documentElement.scrollTop + e.clientY); scaleWidth = pageX - position.x; scaleHeight = pageY - position.y; if (scaleWidth >= minEditorWidth) { isMouseMove = true; scalelayer.style.width = scaleWidth + 'px'; } if (scaleHeight >= minEditorHeight) { isMouseMove = true; scalelayer.style.height = scaleHeight + "px"; } } function up() { if (isMouseMove) { isMouseMove = false; editor.ui._actualFrameWidth = scalelayer.offsetWidth - 2; editorHolder.style.width = editor.ui._actualFrameWidth + 'px'; editor.setHeight(scalelayer.offsetHeight - bottombar.offsetHeight - toolbarBox.offsetHeight - 2,true); } if (scalelayer) { scalelayer.style.display = "none"; } clearSelection(); domUtils.un(doc, "mousemove", move); domUtils.un(editorDocument, "mouseup", up); domUtils.un(doc, "mouseup", up); } function clearSelection() { if (browser.ie) doc.selection.clear(); else window.getSelection().removeAllRanges(); } this.enableScale = function () { //trace:2868 if (editor.queryCommandState("source") == 1) return; scale.style.display = ""; this.scaleEnabled = true; domUtils.on(scale, "mousedown", down); }; this.disableScale = function () { scale.style.display = "none"; this.scaleEnabled = false; domUtils.un(scale, "mousedown", down); }; }, isFullScreen:function () { return this._fullscreen; }, postRender:function () { UIBase.prototype.postRender.call(this); for (var i = 0; i < this.toolbars.length; i++) { this.toolbars[i].postRender(); } var me = this; var timerId, domUtils = baidu.editor.dom.domUtils, updateFullScreenTime = function () { clearTimeout(timerId); timerId = setTimeout(function () { me._updateFullScreen(); }); }; domUtils.on(window, 'resize', updateFullScreenTime); me.addListener('destroy', function () { domUtils.un(window, 'resize', updateFullScreenTime); clearTimeout(timerId); }) }, showToolbarMsg:function (msg, flag) { this.getDom('toolbarmsg_label').innerHTML = msg; this.getDom('toolbarmsg').style.display = ''; // if (!flag) { var w = this.getDom('upload_dialog'); w.style.display = 'none'; } }, hideToolbarMsg:function () { this.getDom('toolbarmsg').style.display = 'none'; }, mapUrl:function (url) { return url ? url.replace('~/', this.editor.options.UEDITOR_HOME_URL || '') : '' }, triggerLayout:function () { var dom = this.getDom(); if (dom.style.zoom == '1') { dom.style.zoom = '100%'; } else { dom.style.zoom = '1'; } } }; utils.inherits(EditorUI, baidu.editor.ui.UIBase); var instances = {}; UE.ui.Editor = function (options) { var editor = new UE.Editor(options); editor.options.editor = editor; utils.loadFile(document, { href:editor.options.themePath + editor.options.theme + "/css/ueditor.css", tag:"link", type:"text/css", rel:"stylesheet" }); var oldRender = editor.render; editor.render = function (holder) { if (holder.constructor === String) { editor.key = holder; instances[holder] = editor; } utils.domReady(function () { editor.langIsReady ? renderUI() : editor.addListener("langReady", renderUI); function renderUI() { editor.setOpt({ labelMap:editor.options.labelMap || editor.getLang('labelMap') }); new EditorUI(editor.options); if (holder) { if (holder.constructor === String) { holder = document.getElementById(holder); } holder && holder.getAttribute('name') && ( editor.options.textarea = holder.getAttribute('name')); if (holder && /script|textarea/ig.test(holder.tagName)) { var newDiv = document.createElement('div'); holder.parentNode.insertBefore(newDiv, holder); var cont = holder.value || holder.innerHTML; editor.options.initialContent = /^[\t\r\n ]*$/.test(cont) ? editor.options.initialContent : cont.replace(/>[\n\r\t]+([ ]{4})+/g, '>') .replace(/[\n\r\t]+([ ]{4})+[\n\r\t]+<'); holder.className && (newDiv.className = holder.className); holder.style.cssText && (newDiv.style.cssText = holder.style.cssText); if (/textarea/i.test(holder.tagName)) { editor.textarea = holder; editor.textarea.style.display = 'none'; } else { holder.parentNode.removeChild(holder); } if(holder.id){ newDiv.id = holder.id; domUtils.removeAttributes(holder,'id'); } holder = newDiv; holder.innerHTML = ''; } } domUtils.addClass(holder, "edui-" + editor.options.theme); editor.ui.render(holder); var opt = editor.options; //给实例添加一个编辑器的容器引用 editor.container = editor.ui.getDom(); var parents = domUtils.findParents(holder,true); var displays = []; for(var i = 0 ,ci;ci=parents[i];i++){ displays[i] = ci.style.display; ci.style.display = 'block' } if (opt.initialFrameWidth) { opt.minFrameWidth = opt.initialFrameWidth; } else { opt.minFrameWidth = opt.initialFrameWidth = holder.offsetWidth; var styleWidth = holder.style.width; if(/%$/.test(styleWidth)) { opt.initialFrameWidth = styleWidth; } } if (opt.initialFrameHeight) { opt.minFrameHeight = opt.initialFrameHeight; } else { opt.initialFrameHeight = opt.minFrameHeight = holder.offsetHeight; } for(var i = 0 ,ci;ci=parents[i];i++){ ci.style.display = displays[i] } //编辑器最外容器设置了高度,会导致,编辑器不占位 //todo 先去掉,没有找到原因 if(holder.style.height){ holder.style.height = '' } editor.container.style.width = opt.initialFrameWidth + (/%$/.test(opt.initialFrameWidth) ? '' : 'px'); editor.container.style.zIndex = opt.zIndex; oldRender.call(editor, editor.ui.getDom('iframeholder')); editor.fireEvent("afteruiready"); } }) }; return editor; }; /** * @file * @name UE * @short UE * @desc UEditor的顶部命名空间 */ /** * @name getEditor * @since 1.2.4+ * @grammar UE.getEditor(id,[opt]) => Editor实例 * @desc 提供一个全局的方法得到编辑器实例 * * * ''id'' 放置编辑器的容器id, 如果容器下的编辑器已经存在,就直接返回 * * ''opt'' 编辑器的可选参数 * @example * UE.getEditor('containerId',{onready:function(){//创建一个编辑器实例 * this.setContent('hello') * }}); * UE.getEditor('containerId'); //返回刚创建的实例 * */ UE.getEditor = function (id, opt) { var editor = instances[id]; if (!editor) { editor = instances[id] = new UE.ui.Editor(opt); editor.render(id); } return editor; }; UE.delEditor = function (id) { var editor; if (editor = instances[id]) { editor.key && editor.destroy(); delete instances[id] } }; UE.registerUI = function(uiName,fn,index,editorId){ utils.each(uiName.split(/\s+/), function (name) { UE._customizeUI[name] = { id : editorId, execFn:fn, index:index }; }) } })(); // adapter/message.js UE.registerUI('message', function(editor) { var editorui = baidu.editor.ui; var Message = editorui.Message; var holder; var _messageItems = []; var me = editor; me.addListener('ready', function(){ holder = document.getElementById(me.ui.id + '_message_holder'); updateHolderPos(); // HaoChuan9421 // setTimeout(function(){ // updateHolderPos(); // }, 500); }); me.addListener('showmessage', function(type, opt){ opt = utils.isString(opt) ? { 'content': opt } : opt; var message = new Message({ 'timeout': opt.timeout, 'type': opt.type, 'content': opt.content, 'keepshow': opt.keepshow, 'editor': me }), mid = opt.id || ('msg_' + (+new Date()).toString(36)); message.render(holder); _messageItems[mid] = message; message.reset(opt); updateHolderPos(); return mid; }); me.addListener('updatemessage',function(type, id, opt){ opt = utils.isString(opt) ? { 'content': opt } : opt; var message = _messageItems[id]; message.render(holder); message && message.reset(opt); }); me.addListener('hidemessage',function(type, id){ var message = _messageItems[id]; message && message.hide(); }); function updateHolderPos(){ var toolbarbox = me.ui.getDom('toolbarbox'); if (toolbarbox) { holder.style.top = toolbarbox.offsetHeight + 3 + 'px'; } holder.style.zIndex = Math.max(me.options.zIndex, me.iframe.style.zIndex) + 1; } }); // adapter/autosave.js UE.registerUI('autosave', function(editor) { var timer = null,uid = null; editor.on('afterautosave',function(){ clearTimeout(timer); timer = setTimeout(function(){ if(uid){ editor.trigger('hidemessage',uid); } uid = editor.trigger('showmessage',{ content : editor.getLang('autosave.success'), timeout : 2000 }); },2000) }) }); })(); ================================================ FILE: yshop-drink-vue3/public/UEditor/ueditor.config.js ================================================ /** * ueditor完整配置项 * 可以在这里配置整个编辑器的特性 */ /** ************************提示******************************** * 所有被注释的配置项均为UEditor默认值。 * 修改默认配置请首先确保已经完全明确该参数的真实用途。 * 主要有两种修改方案,一种是取消此处注释,然后修改成对应参数;另一种是在实例化编辑器时传入对应参数。 * 当升级编辑器时,可直接使用旧版配置文件替换新版配置文件,不用担心旧版配置文件中因缺少新功能所需的参数而导致脚本报错。 **************************提示********************************/ (function () { /** * 编辑器资源文件根路径。它所表示的含义是:以编辑器实例化页面为当前路径,指向编辑器资源文件(即dialog等文件夹)的路径。 * 鉴于很多同学在使用编辑器的时候出现的种种路径问题,此处强烈建议大家使用"相对于网站根目录的相对路径"进行配置。 * "相对于网站根目录的相对路径"也就是以斜杠开头的形如"/myProject/ueditor/"这样的路径。 * 如果站点中有多个不在同一层级的页面需要实例化编辑器,且引用了同一UEditor的时候,此处的URL可能不适用于每个页面的编辑器。 * 因此,UEditor提供了针对不同页面的编辑器可单独配置的根路径,具体来说,在需要实例化编辑器的页面最顶部写上如下代码即可。当然,需要令此处的URL等于对应的配置。 * window.UEDITOR_HOME_URL = "/xxxx/xxxx/"; */ var URL = window.UEDITOR_HOME_URL || getUEBasePath(); /** * 配置项主体。注意,此处所有涉及到路径的配置别遗漏URL变量。 */ window.UEDITOR_CONFIG = { // 为编辑器实例添加一个路径,这个不能被注释 UEDITOR_HOME_URL: URL, // 服务器统一请求接口路径 serverUrl: URL + 'php/controller.php', // 工具栏上的所有的功能按钮和下拉框,可以在new编辑器的实例时选择自己需要的重新定义 toolbars: [[ 'source', '|', 'undo', 'redo', '|', 'bold', 'italic', 'underline', 'strikethrough', '|', 'superscript', 'subscript', '|', 'forecolor', 'backcolor', '|', 'removeformat', '|', 'insertorderedlist', 'insertunorderedlist', '|', 'selectall', 'cleardoc', 'paragraph', '|', 'fontfamily', 'fontsize', '|', 'justifyleft', 'justifycenter', 'justifyright', 'justifyjustify', '|', 'link', 'unlink', '|', 'emotion', '|', '|', 'horizontal', 'print', 'preview', 'fullscreen', 'drafts', 'formula' ]], // 当鼠标放在工具栏上时显示的tooltip提示,留空支持自动多语言配置,否则以配置值为准 //, labelMap:{ // 'anchor':'', 'undo':'' // } // 语言配置项,默认是zh-cn。有需要的话也可以使用如下这样的方式来自动多语言切换,当然,前提条件是lang文件夹下存在对应的语言文件: // lang值也可以通过自动获取 (navigator.language||navigator.browserLanguage ||navigator.userLanguage).toLowerCase() //, lang:"zh-cn" //, langPath:URL +"lang/" // 主题配置项,默认是default。有需要的话也可以使用如下这样的方式来自动多主题切换,当然,前提条件是themes文件夹下存在对应的主题文件: // 现有如下皮肤:default //, theme:'default' //, themePath:URL +"themes/" //, zIndex : 900 //编辑器层级的基数,默认是900 // 针对getAllHtml方法,会在对应的head标签中增加该编码设置。 //, charset:"utf-8" // 若实例化编辑器的页面手动修改的domain,此处需要设置为true //, customDomain:false // 常用配置项目 //, isShow : true //默认显示编辑器 //, textarea:'editorValue' // 提交表单时,服务器获取编辑器提交内容的所用的参数,多实例时可以给容器name属性,会将name给定的值最为每个实例的键值,不用每次实例化的时候都设置这个值 //, initialContent:'欢迎使用ueditor!' //初始化编辑器的内容,也可以通过textarea/script给值,看官网例子 //, autoClearinitialContent:true //是否自动清除编辑器初始内容,注意:如果focus属性设置为true,这个也为真,那么编辑器一上来就会触发导致初始化的内容看不到了 //, focus:false //初始化时,是否让编辑器获得焦点true或false // 如果自定义,最好给p标签如下的行高,要不输入中文时,会有跳动感 //, initialStyle:'p{line-height:1em}'//编辑器层级的基数,可以用来改变字体等 //, iframeCssUrl: URL + '/themes/iframe.css' //给编辑区域的iframe引入一个css文件 // indentValue // 首行缩进距离,默认是2em //, indentValue:'2em' //, initialFrameWidth:1000 //初始化编辑器宽度,默认1000 //, initialFrameHeight:320 //初始化编辑器高度,默认320 //, readonly : false //编辑器初始化结束后,编辑区域是否是只读的,默认是false //, autoClearEmptyNode : true //getContent时,是否删除空的inlineElement节点(包括嵌套的情况) // 启用自动保存 //, enableAutoSave: true // 自动保存间隔时间, 单位ms //, saveInterval: 500 //, fullscreen : false //是否开启初始化时即全屏,默认关闭 //, imagePopup:true //图片操作的浮层开关,默认打开 //, autoSyncData:true //自动同步编辑器要提交的数据 //, emotionLocalization:false //是否开启表情本地化,默认关闭。若要开启请确保emotion文件夹下包含官网提供的images表情文件夹 // 粘贴只保留标签,去除标签所有属性 //, retainOnlyLabelPasted: false //, pasteplain:false //是否默认为纯文本粘贴。false为不使用纯文本粘贴,true为使用纯文本粘贴 // 纯文本粘贴模式下的过滤规则 // 'filterTxtRules' : function(){ // function transP(node){ // node.tagName = 'p'; // node.setStyle(); // } // return { // //直接删除及其字节点内容 // '-' : 'script style object iframe embed input select', // 'p': {$:{}}, // 'br':{$:{}}, // 'div':{'$':{}}, // 'li':{'$':{}}, // 'caption':transP, // 'th':transP, // 'tr':transP, // 'h1':transP,'h2':transP,'h3':transP,'h4':transP,'h5':transP,'h6':transP, // 'td':function(node){ // //没有内容的td直接删掉 // var txt = !!node.innerText(); // if(txt){ // node.parentNode.insertAfter(UE.uNode.createText('    '),node); // } // node.parentNode.removeChild(node,node.innerText()) // } // } // }() //, allHtmlEnabled:false //提交到后台的数据是否包含整个html字符串 // insertorderedlist // 有序列表的下拉配置,值留空时支持多语言自动识别,若配置值,则以此值为准 //, 'insertorderedlist':{ // //自定的样式 // 'num':'1,2,3...', // 'num1':'1),2),3)...', // 'num2':'(1),(2),(3)...', // 'cn':'一,二,三....', // 'cn1':'一),二),三)....', // 'cn2':'(一),(二),(三)....', // //系统自带 // 'decimal' : '' , //'1,2,3...' // 'lower-alpha' : '' , // 'a,b,c...' // 'lower-roman' : '' , //'i,ii,iii...' // 'upper-alpha' : '' , lang //'A,B,C' // 'upper-roman' : '' //'I,II,III...' // } // insertunorderedlist // 无序列表的下拉配置,值留空时支持多语言自动识别,若配置值,则以此值为准 //, insertunorderedlist : { //自定的样式 // 'dash' :'— 破折号', //-破折号 // 'dot':' 。 小圆圈', //系统自带 // 'circle' : '', // '○ 小圆圈' // 'disc' : '', // '● 小圆点' // 'square' : '' //'■ 小方块' // } //, listDefaultPaddingLeft : '30'//默认的左边缩进的基数倍 //, listiconpath : 'http://bs.baidu.com/listicon/'//自定义标号的路径 //, maxListLevel : 3 //限制可以tab的级数, 设置-1为不限制 //, autoTransWordToList:false //禁止word中粘贴进来的列表自动变成列表标签 // fontfamily // 字体设置 label留空支持多语言自动切换,若配置,则以配置值为准 //, 'fontfamily':[ // { label:'',name:'songti',val:'宋体,SimSun'}, // { label:'',name:'kaiti',val:'楷体,楷体_GB2312, SimKai'}, // { label:'',name:'yahei',val:'微软雅黑,Microsoft YaHei'}, // { label:'',name:'heiti',val:'黑体, SimHei'}, // { label:'',name:'lishu',val:'隶书, SimLi'}, // { label:'',name:'andaleMono',val:'andale mono'}, // { label:'',name:'arial',val:'arial, helvetica,sans-serif'}, // { label:'',name:'arialBlack',val:'arial black,avant garde'}, // { label:'',name:'comicSansMs',val:'comic sans ms'}, // { label:'',name:'impact',val:'impact,chicago'}, // { label:'',name:'timesNewRoman',val:'times new roman'} // ] // fontsize // 字号 //, 'fontsize':[10, 11, 12, 14, 16, 18, 20, 24, 36] // paragraph // 段落格式 值留空时支持多语言自动识别,若配置,则以配置值为准 //, 'paragraph':{'p':'', 'h1':'', 'h2':'', 'h3':'', 'h4':'', 'h5':'', 'h6':''} // rowspacingtop // 段间距 值和显示的名字相同 //, 'rowspacingtop':['5', '10', '15', '20', '25'] // rowspacingBottom // 段间距 值和显示的名字相同 //, 'rowspacingbottom':['5', '10', '15', '20', '25'] // lineheight // 行内间距 值和显示的名字相同 //, 'lineheight':['1', '1.5','1.75','2', '3', '4', '5'] // customstyle // 自定义样式,不支持国际化,此处配置值即可最后显示值 // block的元素是依据设置段落的逻辑设置的,inline的元素依据BIU的逻辑设置 // 尽量使用一些常用的标签 // 参数说明 // tag 使用的标签名字 // label 显示的名字也是用来标识不同类型的标识符,注意这个值每个要不同, // style 添加的样式 // 每一个对象就是一个自定义的样式 //, 'customstyle':[ // {tag:'h1', name:'tc', label:'', style:'border-bottom:#ccc 2px solid;padding:0 4px 0 0;text-align:center;margin:0 0 20px 0;'}, // {tag:'h1', name:'tl',label:'', style:'border-bottom:#ccc 2px solid;padding:0 4px 0 0;margin:0 0 10px 0;'}, // {tag:'span',name:'im', label:'', style:'font-style:italic;font-weight:bold'}, // {tag:'span',name:'hi', label:'', style:'font-style:italic;font-weight:bold;color:rgb(51, 153, 204)'} // ] // 打开右键菜单功能 //, enableContextMenu: true // 右键菜单的内容,可以参考plugins/contextmenu.js里边的默认菜单的例子,label留空支持国际化,否则以此配置为准 //, contextMenu:[ // { // label:'', //显示的名称 // cmdName:'selectall',//执行的command命令,当点击这个右键菜单时 // //exec可选,有了exec就会在点击时执行这个function,优先级高于cmdName // exec:function () { // //this是当前编辑器的实例 // //this.ui._dialogs['inserttableDialog'].open(); // } // } // ] // 快捷菜单 //, shortcutMenu:["fontfamily", "fontsize", "bold", "italic", "underline", "forecolor", "backcolor", "insertorderedlist", "insertunorderedlist"] // elementPathEnabled // 是否启用元素路径,默认是显示 //, elementPathEnabled : true // wordCount //, wordCount:true //是否开启字数统计 //, maximumWords:10000 //允许的最大字符数 // 字数统计提示,{#count}代表当前字数,{#leave}代表还可以输入多少字符数,留空支持多语言自动切换,否则按此配置显示 //, wordCountMsg:'' //当前已输入 {#count} 个字符,您还可以输入{#leave} 个字符 // 超出字数限制提示 留空支持多语言自动切换,否则按此配置显示 //, wordOverFlowMsg:'' //你输入的字符个数已经超出最大允许值,服务器可能会拒绝保存! // tab // 点击tab键时移动的距离,tabSize倍数,tabNode什么字符做为单位 //, tabSize:4 //, tabNode:' ' // removeFormat // 清除格式时可以删除的标签和属性 // removeForamtTags标签 //, removeFormatTags:'b,big,code,del,dfn,em,font,i,ins,kbd,q,samp,small,span,strike,strong,sub,sup,tt,u,var' // removeFormatAttributes属性 //, removeFormatAttributes:'class,style,lang,width,height,align,hspace,valign' // undo // 可以最多回退的次数,默认20 //, maxUndoCount:20 // 当输入的字符数超过该值时,保存一次现场 //, maxInputCount:1 // autoHeightEnabled // 是否自动长高,默认true //, autoHeightEnabled:true // scaleEnabled // 是否可以拉伸长高,默认true(当开启时,自动长高失效) //, scaleEnabled:false //, minFrameWidth:800 //编辑器拖动时最小宽度,默认800 //, minFrameHeight:220 //编辑器拖动时最小高度,默认220 // autoFloatEnabled // 是否保持toolbar的位置不动,默认true //, autoFloatEnabled:true // 浮动时工具栏距离浏览器顶部的高度,用于某些具有固定头部的页面 //, topOffset:30 // 编辑器底部距离工具栏高度(如果参数大于等于编辑器高度,则设置无效) //, toolbarTopOffset:400 // 设置远程图片是否抓取到本地保存 //, catchRemoteImageEnable: true //设置是否抓取远程图片 // pageBreakTag // 分页标识符,默认是_ueditor_page_break_tag_ //, pageBreakTag:'_ueditor_page_break_tag_' // autotypeset // 自动排版参数 //, autotypeset: { // mergeEmptyline: true, //合并空行 // removeClass: true, //去掉冗余的class // removeEmptyline: false, //去掉空行 // textAlign:"left", //段落的排版方式,可以是 left,right,center,justify 去掉这个属性表示不执行排版 // imageBlockLine: 'center', //图片的浮动方式,独占一行剧中,左右浮动,默认: center,left,right,none 去掉这个属性表示不执行排版 // pasteFilter: false, //根据规则过滤没事粘贴进来的内容 // clearFontSize: false, //去掉所有的内嵌字号,使用编辑器默认的字号 // clearFontFamily: false, //去掉所有的内嵌字体,使用编辑器默认的字体 // removeEmptyNode: false, // 去掉空节点 // //可以去掉的标签 // removeTagNames: {标签名字:1}, // indent: false, // 行首缩进 // indentValue : '2em', //行首缩进的大小 // bdc2sb: false, // tobdc: false // } // tableDragable // 表格是否可以拖拽 //, tableDragable: true // sourceEditor // 源码的查看方式,codemirror 是代码高亮,textarea是文本框,默认是codemirror // 注意默认codemirror只能在ie8+和非ie中使用 //, sourceEditor:"codemirror" // 如果sourceEditor是codemirror,还用配置一下两个参数 // codeMirrorJsUrl js加载的路径,默认是 URL + "third-party/codemirror/codemirror.js" //, codeMirrorJsUrl:URL + "third-party/codemirror/codemirror.js" // codeMirrorCssUrl css加载的路径,默认是 URL + "third-party/codemirror/codemirror.css" //, codeMirrorCssUrl:URL + "third-party/codemirror/codemirror.css" // 编辑器初始化完成后是否进入源码模式,默认为否。 //, sourceEditorFirst:false // iframeUrlMap // dialog内容的路径 ~会被替换成URL,垓属性一旦打开,将覆盖所有的dialog的默认路径 //, iframeUrlMap:{ // 'anchor':'~/dialogs/anchor/anchor.html', // } // allowLinkProtocol 允许的链接地址,有这些前缀的链接地址不会自动添加http //, allowLinkProtocols: ['http:', 'https:', '#', '/', 'ftp:', 'mailto:', 'tel:', 'git:', 'svn:'] // webAppKey 百度应用的APIkey,每个站长必须首先去百度官网注册一个key后方能正常使用app功能,注册介绍,http://app.baidu.com/static/cms/getapikey.html //, webAppKey: "" // 默认过滤规则相关配置项目 //, disabledTableInTable:true //禁止表格嵌套 //, allowDivTransToP:true //允许进入编辑器的div标签自动变成p标签 //, rgb2Hex:true //默认产出的数据中的color自动从rgb格式变成16进制格式 // xss 过滤是否开启,inserthtml等操作 xssFilterRules: true, // input xss过滤 inputXssFilter: true, // output xss过滤 outputXssFilter: true, // xss过滤白名单 名单来源: https://raw.githubusercontent.com/leizongmin/js-xss/master/lib/default.js whiteList: { a: ['target', 'href', 'title', 'class', 'style'], abbr: ['title', 'class', 'style'], address: ['class', 'style'], area: ['shape', 'coords', 'href', 'alt'], article: [], aside: [], audio: ['autoplay', 'controls', 'loop', 'preload', 'src', 'class', 'style'], b: ['class', 'style'], bdi: ['dir'], bdo: ['dir'], big: [], blockquote: ['cite', 'class', 'style'], br: [], caption: ['class', 'style'], center: [], cite: [], code: ['class', 'style'], col: ['align', 'valign', 'span', 'width', 'class', 'style'], colgroup: ['align', 'valign', 'span', 'width', 'class', 'style'], dd: ['class', 'style'], del: ['datetime'], details: ['open'], div: ['class', 'style'], dl: ['class', 'style'], dt: ['class', 'style'], em: ['class', 'style'], font: ['color', 'size', 'face'], footer: [], h1: ['class', 'style'], h2: ['class', 'style'], h3: ['class', 'style'], h4: ['class', 'style'], h5: ['class', 'style'], h6: ['class', 'style'], header: [], hr: [], i: ['class', 'style'], img: ['src', 'alt', 'title', 'width', 'height', 'id', '_src', 'loadingclass', 'class', 'data-latex'], ins: ['datetime'], li: ['class', 'style'], mark: [], nav: [], ol: ['class', 'style'], p: ['class', 'style'], pre: ['class', 'style'], s: [], section: [], small: [], span: ['class', 'style'], sub: ['class', 'style'], sup: ['class', 'style'], strong: ['class', 'style'], table: ['width', 'border', 'align', 'valign', 'class', 'style'], tbody: ['align', 'valign', 'class', 'style'], td: ['width', 'rowspan', 'colspan', 'align', 'valign', 'class', 'style'], tfoot: ['align', 'valign', 'class', 'style'], th: ['width', 'rowspan', 'colspan', 'align', 'valign', 'class', 'style'], thead: ['align', 'valign', 'class', 'style'], tr: ['rowspan', 'align', 'valign', 'class', 'style'], tt: [], u: [], ul: ['class', 'style'], video: ['autoplay', 'controls', 'loop', 'preload', 'src', 'height', 'width', 'class', 'style'] } }; function getUEBasePath (docUrl, confUrl) { return getBasePath(docUrl || self.document.URL || self.location.href, confUrl || getConfigFilePath()); } function getConfigFilePath () { var configPath = document.getElementsByTagName('script'); return configPath[ configPath.length - 1 ].src; } function getBasePath (docUrl, confUrl) { var basePath = confUrl; if (/^(\/|\\\\)/.test(confUrl)) { basePath = /^.+?\w(\/|\\\\)/.exec(docUrl)[0] + confUrl.replace(/^(\/|\\\\)/, ''); } else if (!/^[a-z]+:/i.test(confUrl)) { docUrl = docUrl.split('#')[0].split('?')[0].replace(/[^\\\/]+$/, ''); basePath = docUrl + '' + confUrl; } return optimizationPath(basePath); } function optimizationPath (path) { let protocol = /^[a-z]+:\/\//.exec(path)[ 0 ], tmp = null, res = []; path = path.replace(protocol, '').split('?')[0].split('#')[0]; path = path.replace(/\\/g, '/').split(/\//); path[ path.length - 1 ] = ''; while (path.length) { if ((tmp = path.shift()) === '..') { res.pop(); } else if (tmp !== '.') { res.push(tmp); } } return protocol + res.join('/'); } window.UE = { getUEBasePath: getUEBasePath }; })(); ================================================ FILE: yshop-drink-vue3/public/UEditor/ueditor.parse.js ================================================ /*! * UEditor * version: ueditor * build: Wed Dec 26 2018 17:25:05 GMT+0800 (CST) */ (function(){ (function(){ UE = window.UE || {}; var isIE = !!window.ActiveXObject; //定义utils工具 var utils = { removeLastbs : function(url){ return url.replace(/\/$/,'') }, extend : function(t,s){ var a = arguments, notCover = this.isBoolean(a[a.length - 1]) ? a[a.length - 1] : false, len = this.isBoolean(a[a.length - 1]) ? a.length - 1 : a.length; for (var i = 1; i < len; i++) { var x = a[i]; for (var k in x) { if (!notCover || !t.hasOwnProperty(k)) { t[k] = x[k]; } } } return t; }, isIE : isIE, cssRule : isIE ? function(key,style,doc){ var indexList,index; doc = doc || document; if(doc.indexList){ indexList = doc.indexList; }else{ indexList = doc.indexList = {}; } var sheetStyle; if(!indexList[key]){ if(style === undefined){ return '' } sheetStyle = doc.createStyleSheet('',index = doc.styleSheets.length); indexList[key] = index; }else{ sheetStyle = doc.styleSheets[indexList[key]]; } if(style === undefined){ return sheetStyle.cssText } sheetStyle.cssText = sheetStyle.cssText + '\n' + (style || '') } : function(key,style,doc){ doc = doc || document; var head = doc.getElementsByTagName('head')[0],node; if(!(node = doc.getElementById(key))){ if(style === undefined){ return '' } node = doc.createElement('style'); node.id = key; head.appendChild(node) } if(style === undefined){ return node.innerHTML } if(style !== ''){ node.innerHTML = node.innerHTML + '\n' + style; }else{ head.removeChild(node) } }, domReady : function (onready) { var doc = window.document; if (doc.readyState === "complete") { onready(); }else{ if (isIE) { (function () { if (doc.isReady) return; try { doc.documentElement.doScroll("left"); } catch (error) { setTimeout(arguments.callee, 0); return; } onready(); })(); window.attachEvent('onload', function(){ onready() }); } else { doc.addEventListener("DOMContentLoaded", function () { doc.removeEventListener("DOMContentLoaded", arguments.callee, false); onready(); }, false); window.addEventListener('load', function(){onready()}, false); } } }, each : function(obj, iterator, context) { if (obj == null) return; if (obj.length === +obj.length) { for (var i = 0, l = obj.length; i < l; i++) { if(iterator.call(context, obj[i], i, obj) === false) return false; } } else { for (var key in obj) { if (obj.hasOwnProperty(key)) { if(iterator.call(context, obj[key], key, obj) === false) return false; } } } }, inArray : function(arr,item){ var index = -1; this.each(arr,function(v,i){ if(v === item){ index = i; return false; } }); return index; }, pushItem : function(arr,item){ if(this.inArray(arr,item)==-1){ arr.push(item) } }, trim: function (str) { return str.replace(/(^[ \t\n\r]+)|([ \t\n\r]+$)/g, ''); }, indexOf: function (array, item, start) { var index = -1; start = this.isNumber(start) ? start : 0; this.each(array, function (v, i) { if (i >= start && v === item) { index = i; return false; } }); return index; }, hasClass: function (element, className) { className = className.replace(/(^[ ]+)|([ ]+$)/g, '').replace(/[ ]{2,}/g, ' ').split(' '); for (var i = 0, ci, cls = element.className; ci = className[i++];) { if (!new RegExp('\\b' + ci + '\\b', 'i').test(cls)) { return false; } } return i - 1 == className.length; }, addClass:function (elm, classNames) { if(!elm)return; classNames = this.trim(classNames).replace(/[ ]{2,}/g,' ').split(' '); for(var i = 0,ci,cls = elm.className;ci=classNames[i++];){ if(!new RegExp('\\b' + ci + '\\b').test(cls)){ cls += ' ' + ci; } } elm.className = utils.trim(cls); }, removeClass:function (elm, classNames) { classNames = this.isArray(classNames) ? classNames : this.trim(classNames).replace(/[ ]{2,}/g,' ').split(' '); for(var i = 0,ci,cls = elm.className;ci=classNames[i++];){ cls = cls.replace(new RegExp('\\b' + ci + '\\b'),'') } cls = this.trim(cls).replace(/[ ]{2,}/g,' '); elm.className = cls; !cls && elm.removeAttribute('className'); }, on: function (element, type, handler) { var types = this.isArray(type) ? type : type.split(/\s+/), k = types.length; if (k) while (k--) { type = types[k]; if (element.addEventListener) { element.addEventListener(type, handler, false); } else { if (!handler._d) { handler._d = { els : [] }; } var key = type + handler.toString(),index = utils.indexOf(handler._d.els,element); if (!handler._d[key] || index == -1) { if(index == -1){ handler._d.els.push(element); } if(!handler._d[key]){ handler._d[key] = function (evt) { return handler.call(evt.srcElement, evt || window.event); }; } element.attachEvent('on' + type, handler._d[key]); } } } element = null; }, off: function (element, type, handler) { var types = this.isArray(type) ? type : type.split(/\s+/), k = types.length; if (k) while (k--) { type = types[k]; if (element.removeEventListener) { element.removeEventListener(type, handler, false); } else { var key = type + handler.toString(); try{ element.detachEvent('on' + type, handler._d ? handler._d[key] : handler); }catch(e){} if (handler._d && handler._d[key]) { var index = utils.indexOf(handler._d.els,element); if(index!=-1){ handler._d.els.splice(index,1); } handler._d.els.length == 0 && delete handler._d[key]; } } } }, loadFile : function () { var tmpList = []; function getItem(doc,obj){ try{ for(var i= 0,ci;ci=tmpList[i++];){ if(ci.doc === doc && ci.url == (obj.src || obj.href)){ return ci; } } }catch(e){ return null; } } return function (doc, obj, fn) { var item = getItem(doc,obj); if (item) { if(item.ready){ fn && fn(); }else{ item.funs.push(fn) } return; } tmpList.push({ doc:doc, url:obj.src||obj.href, funs:[fn] }); if (!doc.body) { var html = []; for(var p in obj){ if(p == 'tag')continue; html.push(p + '="' + obj[p] + '"') } doc.write('<' + obj.tag + ' ' + html.join(' ') + ' >'); return; } if (obj.id && doc.getElementById(obj.id)) { return; } var element = doc.createElement(obj.tag); delete obj.tag; for (var p in obj) { element.setAttribute(p, obj[p]); } element.onload = element.onreadystatechange = function () { if (!this.readyState || /loaded|complete/.test(this.readyState)) { item = getItem(doc,obj); if (item.funs.length > 0) { item.ready = 1; for (var fi; fi = item.funs.pop();) { fi(); } } element.onload = element.onreadystatechange = null; } }; element.onerror = function(){ throw Error('The load '+(obj.href||obj.src)+' fails,check the url') }; doc.getElementsByTagName("head")[0].appendChild(element); } }() }; utils.each(['String', 'Function', 'Array', 'Number', 'RegExp', 'Object','Boolean'], function (v) { utils['is' + v] = function (obj) { return Object.prototype.toString.apply(obj) == '[object ' + v + ']'; } }); var parselist = {}; UE.parse = { register : function(parseName,fn){ parselist[parseName] = fn; }, load : function(opt){ utils.each(parselist,function(v){ v.call(opt,utils); }) } }; uParse = function(selector,opt){ utils.domReady(function(){ var contents; if(document.querySelectorAll){ contents = document.querySelectorAll(selector) }else{ if(/^#/.test(selector)){ contents = [document.getElementById(selector.replace(/^#/,''))] }else if(/^\./.test(selector)){ var contents = []; utils.each(document.getElementsByTagName('*'),function(node){ if(node.className && new RegExp('\\b' + selector.replace(/^\./,'') + '\\b','i').test(node.className)){ contents.push(node) } }) }else{ contents = document.getElementsByTagName(selector) } } utils.each(contents,function(v){ UE.parse.load(utils.extend({root:v,selector:selector},opt)) }) }) } })(); UE.parse.register('insertcode',function(utils){ var pres = this.root.getElementsByTagName('pre'); if(pres.length){ if(typeof XRegExp == "undefined"){ var jsurl,cssurl; if(this.rootPath !== undefined){ jsurl = utils.removeLastbs(this.rootPath) + '/third-party/SyntaxHighlighter/shCore.js'; cssurl = utils.removeLastbs(this.rootPath) + '/third-party/SyntaxHighlighter/shCoreDefault.css'; }else{ jsurl = this.highlightJsUrl; cssurl = this.highlightCssUrl; } utils.loadFile(document,{ id : "syntaxhighlighter_css", tag : "link", rel : "stylesheet", type : "text/css", href : cssurl }); utils.loadFile(document,{ id : "syntaxhighlighter_js", src : jsurl, tag : "script", type : "text/javascript", defer : "defer" },function(){ utils.each(pres,function(pi){ if(pi && /brush/i.test(pi.className)){ SyntaxHighlighter.highlight(pi); } }); }); }else{ utils.each(pres,function(pi){ if(pi && /brush/i.test(pi.className)){ SyntaxHighlighter.highlight(pi); } }); } } }); UE.parse.register('table', function (utils) { var me = this, root = this.root, tables = root.getElementsByTagName('table'); if (tables.length) { var selector = this.selector; //追加默认的表格样式 utils.cssRule('table', selector + ' table.noBorderTable td,' + selector + ' table.noBorderTable th,' + selector + ' table.noBorderTable caption{border:1px dashed #ddd !important}' + selector + ' table.sortEnabled tr.firstRow th,' + selector + ' table.sortEnabled tr.firstRow td{padding-right:20px; background-repeat: no-repeat;' + 'background-position: center right; background-image:url(' + this.rootPath + 'themes/default/images/sortable.png);}' + selector + ' table.sortEnabled tr.firstRow th:hover,' + selector + ' table.sortEnabled tr.firstRow td:hover{background-color: #EEE;}' + selector + ' table{margin-bottom:10px;border-collapse:collapse;display:table;}' + selector + ' td,' + selector + ' th{ background:white; padding: 5px 10px;border: 1px solid #DDD;}' + selector + ' caption{border:1px dashed #DDD;border-bottom:0;padding:3px;text-align:center;}' + selector + ' th{border-top:1px solid #BBB;background:#F7F7F7;}' + selector + ' table tr.firstRow th{border-top:2px solid #BBB;background:#F7F7F7;}' + selector + ' tr.ue-table-interlace-color-single td{ background: #fcfcfc; }' + selector + ' tr.ue-table-interlace-color-double td{ background: #f7faff; }' + selector + ' td p{margin:0;padding:0;}', document); //填充空的单元格 utils.each('td th caption'.split(' '), function (tag) { var cells = root.getElementsByTagName(tag); cells.length && utils.each(cells, function (node) { if (!node.firstChild) { node.innerHTML = ' '; } }) }); //表格可排序 var tables = root.getElementsByTagName('table'); utils.each(tables, function (table) { if (/\bsortEnabled\b/.test(table.className)) { utils.on(table, 'click', function(e){ var target = e.target || e.srcElement, cell = findParentByTagName(target, ['td', 'th']); var table = findParentByTagName(target, 'table'), colIndex = utils.indexOf(table.rows[0].cells, cell), sortType = table.getAttribute('data-sort-type'); if(colIndex != -1) { sortTable(table, colIndex, me.tableSortCompareFn || sortType); updateTable(table); } }); } }); //按照标签名查找父节点 function findParentByTagName(target, tagNames) { var i, current = target; tagNames = utils.isArray(tagNames) ? tagNames:[tagNames]; while(current){ for(i = 0;i < tagNames.length; i++) { if(current.tagName == tagNames[i].toUpperCase()) return current; } current = current.parentNode; } return null; } //表格排序 function sortTable(table, sortByCellIndex, compareFn) { var rows = table.rows, trArray = [], flag = rows[0].cells[0].tagName === "TH", lastRowIndex = 0; for (var i = 0,len = rows.length; i < len; i++) { trArray[i] = rows[i]; } var Fn = { 'reversecurrent': function(td1,td2){ return 1; }, 'orderbyasc': function(td1,td2){ var value1 = td1.innerText||td1.textContent, value2 = td2.innerText||td2.textContent; return value1.localeCompare(value2); }, 'reversebyasc': function(td1,td2){ var value1 = td1.innerHTML, value2 = td2.innerHTML; return value2.localeCompare(value1); }, 'orderbynum': function(td1,td2){ var value1 = td1[utils.isIE ? 'innerText':'textContent'].match(/\d+/), value2 = td2[utils.isIE ? 'innerText':'textContent'].match(/\d+/); if(value1) value1 = +value1[0]; if(value2) value2 = +value2[0]; return (value1||0) - (value2||0); }, 'reversebynum': function(td1,td2){ var value1 = td1[utils.isIE ? 'innerText':'textContent'].match(/\d+/), value2 = td2[utils.isIE ? 'innerText':'textContent'].match(/\d+/); if(value1) value1 = +value1[0]; if(value2) value2 = +value2[0]; return (value2||0) - (value1||0); } }; //对表格设置排序的标记data-sort-type table.setAttribute('data-sort-type', compareFn && typeof compareFn === "string" && Fn[compareFn] ? compareFn:''); //th不参与排序 flag && trArray.splice(0, 1); trArray = sort(trArray,function (tr1, tr2) { var result; if (compareFn && typeof compareFn === "function") { result = compareFn.call(this, tr1.cells[sortByCellIndex], tr2.cells[sortByCellIndex]); } else if (compareFn && typeof compareFn === "number") { result = 1; } else if (compareFn && typeof compareFn === "string" && Fn[compareFn]) { result = Fn[compareFn].call(this, tr1.cells[sortByCellIndex], tr2.cells[sortByCellIndex]); } else { result = Fn['orderbyasc'].call(this, tr1.cells[sortByCellIndex], tr2.cells[sortByCellIndex]); } return result; }); var fragment = table.ownerDocument.createDocumentFragment(); for (var j = 0, len = trArray.length; j < len; j++) { fragment.appendChild(trArray[j]); } var tbody = table.getElementsByTagName("tbody")[0]; if(!lastRowIndex){ tbody.appendChild(fragment); }else{ tbody.insertBefore(fragment,rows[lastRowIndex- range.endRowIndex + range.beginRowIndex - 1]) } } //冒泡排序 function sort(array, compareFn){ compareFn = compareFn || function(item1, item2){ return item1.localeCompare(item2);}; for(var i= 0,len = array.length; i 0){ var t = array[i]; array[i] = array[j]; array[j] = t; } } } return array; } //更新表格 function updateTable(table) { //给第一行设置firstRow的样式名称,在排序图标的样式上使用到 if(!utils.hasClass(table.rows[0], "firstRow")) { for(var i = 1; i< table.rows.length; i++) { utils.removeClass(table.rows[i], "firstRow"); } utils.addClass(table.rows[0], "firstRow"); } } } }); UE.parse.register('charts',function( utils ){ utils.cssRule('chartsContainerHeight','.edui-chart-container { height:'+(this.chartContainerHeight||300)+'px}'); var resourceRoot = this.rootPath, containers = this.root, sources = null; //不存在指定的根路径, 则直接退出 if ( !resourceRoot ) { return; } if ( sources = parseSources() ) { loadResources(); } function parseSources () { if ( !containers ) { return null; } return extractChartData( containers ); } /** * 提取数据 */ function extractChartData ( rootNode ) { var data = [], tables = rootNode.getElementsByTagName( "table" ); for ( var i = 0, tableNode; tableNode = tables[ i ]; i++ ) { if ( tableNode.getAttribute( "data-chart" ) !== null ) { data.push( formatData( tableNode ) ); } } return data.length ? data : null; } function formatData ( tableNode ) { var meta = tableNode.getAttribute( "data-chart" ), metaConfig = {}, data = []; //提取table数据 for ( var i = 0, row; row = tableNode.rows[ i ]; i++ ) { var rowData = []; for ( var j = 0, cell; cell = row.cells[ j ]; j++ ) { var value = ( cell.innerText || cell.textContent || '' ); rowData.push( cell.tagName == 'TH' ? value:(value | 0) ); } data.push( rowData ); } //解析元信息 meta = meta.split( ";" ); for ( var i = 0, metaData; metaData = meta[ i ]; i++ ) { metaData = metaData.split( ":" ); metaConfig[ metaData[ 0 ] ] = metaData[ 1 ]; } return { table: tableNode, meta: metaConfig, data: data }; } //加载资源 function loadResources () { loadJQuery(); } function loadJQuery () { //不存在jquery, 则加载jquery if ( !window.jQuery ) { utils.loadFile(document,{ src : resourceRoot + "/third-party/jquery-1.10.2.min.js", tag : "script", type : "text/javascript", defer : "defer" },function(){ loadHighcharts(); }); } else { loadHighcharts(); } } function loadHighcharts () { //不存在Highcharts, 则加载Highcharts if ( !window.Highcharts ) { utils.loadFile(document,{ src : resourceRoot + "/third-party/highcharts/highcharts.js", tag : "script", type : "text/javascript", defer : "defer" },function(){ loadTypeConfig(); }); } else { loadTypeConfig(); } } //加载图表差异化配置文件 function loadTypeConfig () { utils.loadFile(document,{ src : resourceRoot + "/dialogs/charts/chart.config.js", tag : "script", type : "text/javascript", defer : "defer" },function(){ render(); }); } //渲染图表 function render () { var config = null, chartConfig = null, container = null; for ( var i = 0, len = sources.length; i < len; i++ ) { config = sources[ i ]; chartConfig = analysisConfig( config ); container = createContainer( config.table ); renderChart( container, typeConfig[ config.meta.chartType ], chartConfig ); } } /** * 渲染图表 * @param container 图表容器节点对象 * @param typeConfig 图表类型配置 * @param config 图表通用配置 * */ function renderChart ( container, typeConfig, config ) { $( container ).highcharts( $.extend( {}, typeConfig, { credits: { enabled: false }, exporting: { enabled: false }, title: { text: config.title, x: -20 //center }, subtitle: { text: config.subTitle, x: -20 }, xAxis: { title: { text: config.xTitle }, categories: config.categories }, yAxis: { title: { text: config.yTitle }, plotLines: [{ value: 0, width: 1, color: '#808080' }] }, tooltip: { enabled: true, valueSuffix: config.suffix }, legend: { layout: 'vertical', align: 'right', verticalAlign: 'middle', borderWidth: 1 }, series: config.series } )); } /** * 创建图表的容器 * 新创建的容器会替换掉对应的table对象 * */ function createContainer ( tableNode ) { var container = document.createElement( "div" ); container.className = "edui-chart-container"; tableNode.parentNode.replaceChild( container, tableNode ); return container; } //根据config解析出正确的类别和图表数据信息 function analysisConfig ( config ) { var series = [], //数据类别 categories = [], result = [], data = config.data, meta = config.meta; //数据对齐方式为相反的方式, 需要反转数据 if ( meta.dataFormat != "1" ) { for ( var i = 0, len = data.length; i < len ; i++ ) { for ( var j = 0, jlen = data[ i ].length; j < jlen; j++ ) { if ( !result[ j ] ) { result[ j ] = []; } result[ j ][ i ] = data[ i ][ j ]; } } data = result; } result = {}; //普通图表 if ( meta.chartType != typeConfig.length - 1 ) { categories = data[ 0 ].slice( 1 ); for ( var i = 1, curData; curData = data[ i ]; i++ ) { series.push( { name: curData[ 0 ], data: curData.slice( 1 ) } ); } result.series = series; result.categories = categories; result.title = meta.title; result.subTitle = meta.subTitle; result.xTitle = meta.xTitle; result.yTitle = meta.yTitle; result.suffix = meta.suffix; } else { var curData = []; for ( var i = 1, len = data[ 0 ].length; i < len; i++ ) { curData.push( [ data[ 0 ][ i ], data[ 1 ][ i ] | 0 ] ); } //饼图 series[ 0 ] = { type: 'pie', name: meta.tip, data: curData }; result.series = series; result.title = meta.title; result.suffix = meta.suffix; } return result; } }); UE.parse.register('background', function (utils) { var me = this, root = me.root, p = root.getElementsByTagName('p'), styles; for (var i = 0,ci; ci = p[i++];) { styles = ci.getAttribute('data-background'); if (styles){ ci.parentNode.removeChild(ci); } } //追加默认的表格样式 styles && utils.cssRule('ueditor_background', me.selector + '{' + styles + '}', document); }); UE.parse.register('list',function(utils){ var customCss = [], customStyle = { 'cn' : 'cn-1-', 'cn1' : 'cn-2-', 'cn2' : 'cn-3-', 'num' : 'num-1-', 'num1' : 'num-2-', 'num2' : 'num-3-', 'dash' : 'dash', 'dot' : 'dot' }; utils.extend(this,{ liiconpath : 'http://bs.baidu.com/listicon/', listDefaultPaddingLeft : '20' }); var root = this.root, ols = root.getElementsByTagName('ol'), uls = root.getElementsByTagName('ul'), selector = this.selector; if(ols.length){ applyStyle.call(this,ols); } if(uls.length){ applyStyle.call(this,uls); } if(ols.length || uls.length){ customCss.push(selector +' .list-paddingleft-1{padding-left:0}'); customCss.push(selector +' .list-paddingleft-2{padding-left:'+ this.listDefaultPaddingLeft+'px}'); customCss.push(selector +' .list-paddingleft-3{padding-left:'+ this.listDefaultPaddingLeft*2+'px}'); utils.cssRule('list', selector +' ol,'+selector +' ul{margin:0;padding:0;}li{clear:both;}'+customCss.join('\n'), document); } function applyStyle(nodes){ var T = this; utils.each(nodes,function(list){ if(list.className && /custom_/i.test(list.className)){ var listStyle = list.className.match(/custom_(\w+)/)[1]; if(listStyle == 'dash' || listStyle == 'dot'){ utils.pushItem(customCss,selector +' li.list-' + customStyle[listStyle] + '{background-image:url(' + T.liiconpath +customStyle[listStyle]+'.gif)}'); utils.pushItem(customCss,selector +' ul.custom_'+listStyle+'{list-style:none;} '+ selector +' ul.custom_'+listStyle+' li{background-position:0 3px;background-repeat:no-repeat}'); }else{ var index = 1; utils.each(list.childNodes,function(li){ if(li.tagName == 'LI'){ utils.pushItem(customCss,selector + ' li.list-' + customStyle[listStyle] + index + '{background-image:url(' + T.liiconpath + 'list-'+customStyle[listStyle] +index + '.gif)}'); index++; } }); utils.pushItem(customCss,selector + ' ol.custom_'+listStyle+'{list-style:none;}'+selector+' ol.custom_'+listStyle+' li{background-position:0 3px;background-repeat:no-repeat}'); } switch(listStyle){ case 'cn': utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-1{padding-left:25px}'); utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-2{padding-left:40px}'); utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-3{padding-left:55px}'); break; case 'cn1': utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-1{padding-left:30px}'); utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-2{padding-left:40px}'); utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-3{padding-left:55px}'); break; case 'cn2': utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-1{padding-left:40px}'); utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-2{padding-left:55px}'); utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-3{padding-left:68px}'); break; case 'num': case 'num1': utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-1{padding-left:25px}'); break; case 'num2': utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-1{padding-left:35px}'); utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-2{padding-left:40px}'); break; case 'dash': utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft{padding-left:35px}'); break; case 'dot': utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft{padding-left:20px}'); } } }); } }); UE.parse.register('vedio',function(utils){ var video = this.root.getElementsByTagName('video'), audio = this.root.getElementsByTagName('audio'); document.createElement('video');document.createElement('audio'); if(video.length || audio.length){ var sourcePath = utils.removeLastbs(this.rootPath), jsurl = sourcePath + '/third-party/video-js/video.js', cssurl = sourcePath + '/third-party/video-js/video-js.min.css', swfUrl = sourcePath + '/third-party/video-js/video-js.swf'; if(window.videojs) { videojs.autoSetup(); } else { utils.loadFile(document,{ id : "video_css", tag : "link", rel : "stylesheet", type : "text/css", href : cssurl }); utils.loadFile(document,{ id : "video_js", src : jsurl, tag : "script", type : "text/javascript" },function(){ videojs.options.flash.swf = swfUrl; videojs.autoSetup(); }); } } }); })(); ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/anchor/anchor.html ================================================
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/attachment/attachment.css ================================================ @charset "utf-8"; /* dialog样式 */ .wrapper { zoom: 1; width: 630px; *width: 626px; height: 380px; margin: 0 auto; padding: 10px; position: relative; font-family: sans-serif; } /*tab样式框大小*/ .tabhead { float:left; } .tabbody { width: 100%; height: 346px; position: relative; clear: both; } .tabbody .panel { position: absolute; width: 0; height: 0; background: #fff; overflow: hidden; display: none; } .tabbody .panel.focus { width: 100%; height: 346px; display: block; } /* 上传附件 */ .tabbody #upload.panel { width: 0; height: 0; overflow: hidden; position: absolute !important; clip: rect(1px, 1px, 1px, 1px); background: #fff; display: block; } .tabbody #upload.panel.focus { width: 100%; height: 346px; display: block; clip: auto; } #upload .queueList { margin: 0; width: 100%; height: 100%; position: absolute; overflow: hidden; } #upload p { margin: 0; } .element-invisible { width: 0 !important; height: 0 !important; border: 0; padding: 0; margin: 0; overflow: hidden; position: absolute !important; clip: rect(1px, 1px, 1px, 1px); } #upload .placeholder { margin: 10px; border: 2px dashed #e6e6e6; *border: 0px dashed #e6e6e6; height: 172px; padding-top: 150px; text-align: center; background: url(./images/image.png) center 70px no-repeat; color: #cccccc; font-size: 18px; position: relative; top:0; *top: 10px; } #upload .placeholder .webuploader-pick { font-size: 18px; background: #00b7ee; border-radius: 3px; line-height: 44px; padding: 0 30px; *width: 120px; color: #fff; display: inline-block; margin: 0 auto 20px auto; cursor: pointer; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); } #upload .placeholder .webuploader-pick-hover { background: #00a2d4; } #filePickerContainer { text-align: center; } #upload .placeholder .flashTip { color: #666666; font-size: 12px; position: absolute; width: 100%; text-align: center; bottom: 20px; } #upload .placeholder .flashTip a { color: #0785d1; text-decoration: none; } #upload .placeholder .flashTip a:hover { text-decoration: underline; } #upload .placeholder.webuploader-dnd-over { border-color: #999999; } #upload .filelist { list-style: none; margin: 0; padding: 0; overflow-x: hidden; overflow-y: auto; position: relative; height: 300px; } #upload .filelist:after { content: ''; display: block; width: 0; height: 0; overflow: hidden; clear: both; } #upload .filelist li { width: 113px; height: 113px; background: url(./images/bg.png); text-align: center; margin: 9px 0 0 9px; *margin: 6px 0 0 6px; position: relative; display: block; float: left; overflow: hidden; font-size: 12px; } #upload .filelist li p.log { position: relative; top: -45px; } #upload .filelist li p.title { position: absolute; top: 0; left: 0; width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; top: 5px; text-indent: 5px; text-align: left; } #upload .filelist li p.progress { position: absolute; width: 100%; bottom: 0; left: 0; height: 8px; overflow: hidden; z-index: 50; margin: 0; border-radius: 0; background: none; -webkit-box-shadow: 0 0 0; } #upload .filelist li p.progress span { display: none; overflow: hidden; width: 0; height: 100%; background: #1483d8 url(./images/progress.png) repeat-x; -webit-transition: width 200ms linear; -moz-transition: width 200ms linear; -o-transition: width 200ms linear; -ms-transition: width 200ms linear; transition: width 200ms linear; -webkit-animation: progressmove 2s linear infinite; -moz-animation: progressmove 2s linear infinite; -o-animation: progressmove 2s linear infinite; -ms-animation: progressmove 2s linear infinite; animation: progressmove 2s linear infinite; -webkit-transform: translateZ(0); } @-webkit-keyframes progressmove { 0% { background-position: 0 0; } 100% { background-position: 17px 0; } } @-moz-keyframes progressmove { 0% { background-position: 0 0; } 100% { background-position: 17px 0; } } @keyframes progressmove { 0% { background-position: 0 0; } 100% { background-position: 17px 0; } } #upload .filelist li p.imgWrap { position: relative; z-index: 2; line-height: 113px; vertical-align: middle; overflow: hidden; width: 113px; height: 113px; -webkit-transform-origin: 50% 50%; -moz-transform-origin: 50% 50%; -o-transform-origin: 50% 50%; -ms-transform-origin: 50% 50%; transform-origin: 50% 50%; -webit-transition: 200ms ease-out; -moz-transition: 200ms ease-out; -o-transition: 200ms ease-out; -ms-transition: 200ms ease-out; transition: 200ms ease-out; } #upload .filelist li p.imgWrap.notimage { margin-top: 0; width: 111px; height: 111px; border: 1px #eeeeee solid; } #upload .filelist li p.imgWrap.notimage i.file-preview { margin-top: 15px; } #upload .filelist li img { width: 100%; } #upload .filelist li p.error { background: #f43838; color: #fff; position: absolute; bottom: 0; left: 0; height: 28px; line-height: 28px; width: 100%; z-index: 100; display:none; } #upload .filelist li .success { display: block; position: absolute; left: 0; bottom: 0; height: 40px; width: 100%; z-index: 200; background: url(./images/success.png) no-repeat right bottom; background-image: url(./images/success.gif) \9; } #upload .filelist li.filePickerBlock { width: 113px; height: 113px; background: url(./images/image.png) no-repeat center 12px; border: 1px solid #eeeeee; border-radius: 0; } #upload .filelist li.filePickerBlock div.webuploader-pick { width: 100%; height: 100%; margin: 0; padding: 0; opacity: 0; background: none; font-size: 0; } #upload .filelist div.file-panel { position: absolute; height: 0; filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#80000000', endColorstr='#80000000') \0; background: rgba(0, 0, 0, 0.5); width: 100%; top: 0; left: 0; overflow: hidden; z-index: 300; } #upload .filelist div.file-panel span { width: 24px; height: 24px; display: inline; float: right; text-indent: -9999px; overflow: hidden; background: url(./images/icons.png) no-repeat; background: url(./images/icons.gif) no-repeat \9; margin: 5px 1px 1px; cursor: pointer; -webkit-tap-highlight-color: rgba(0,0,0,0); -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } #upload .filelist div.file-panel span.rotateLeft { display:none; background-position: 0 -24px; } #upload .filelist div.file-panel span.rotateLeft:hover { background-position: 0 0; } #upload .filelist div.file-panel span.rotateRight { display:none; background-position: -24px -24px; } #upload .filelist div.file-panel span.rotateRight:hover { background-position: -24px 0; } #upload .filelist div.file-panel span.cancel { background-position: -48px -24px; } #upload .filelist div.file-panel span.cancel:hover { background-position: -48px 0; } #upload .statusBar { height: 45px; border-bottom: 1px solid #dadada; margin: 0 10px; padding: 0; line-height: 45px; vertical-align: middle; position: relative; } #upload .statusBar .progress { border: 1px solid #1483d8; width: 198px; background: #fff; height: 18px; position: absolute; top: 12px; display: none; text-align: center; line-height: 18px; color: #6dbfff; margin: 0 10px 0 0; } #upload .statusBar .progress span.percentage { width: 0; height: 100%; left: 0; top: 0; background: #1483d8; position: absolute; } #upload .statusBar .progress span.text { position: relative; z-index: 10; } #upload .statusBar .info { display: inline-block; font-size: 14px; color: #666666; } #upload .statusBar .btns { position: absolute; top: 7px; right: 0; line-height: 30px; } #filePickerBtn { display: inline-block; float: left; } #upload .statusBar .btns .webuploader-pick, #upload .statusBar .btns .uploadBtn, #upload .statusBar .btns .uploadBtn.state-uploading, #upload .statusBar .btns .uploadBtn.state-paused { background: #ffffff; border: 1px solid #cfcfcf; color: #565656; padding: 0 18px; display: inline-block; border-radius: 3px; margin-left: 10px; cursor: pointer; font-size: 14px; float: left; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } #upload .statusBar .btns .webuploader-pick-hover, #upload .statusBar .btns .uploadBtn:hover, #upload .statusBar .btns .uploadBtn.state-uploading:hover, #upload .statusBar .btns .uploadBtn.state-paused:hover { background: #f0f0f0; } #upload .statusBar .btns .uploadBtn, #upload .statusBar .btns .uploadBtn.state-paused{ background: #00b7ee; color: #fff; border-color: transparent; } #upload .statusBar .btns .uploadBtn:hover, #upload .statusBar .btns .uploadBtn.state-paused:hover{ background: #00a2d4; } #upload .statusBar .btns .uploadBtn.disabled { pointer-events: none; filter:alpha(opacity=60); -moz-opacity:0.6; -khtml-opacity: 0.6; opacity: 0.6; } /* 图片管理样式 */ #online { width: 100%; height: 336px; padding: 10px 0 0 0; } #online #fileList{ width: 100%; height: 100%; overflow-x: hidden; overflow-y: auto; position: relative; } #online ul { display: block; list-style: none; margin: 0; padding: 0; } #online li { float: left; display: block; list-style: none; padding: 0; width: 113px; height: 113px; margin: 0 0 9px 9px; *margin: 0 0 6px 6px; background-color: #eee; overflow: hidden; cursor: pointer; position: relative; } #online li.clearFloat { float: none; clear: both; display: block; width:0; height:0; margin: 0; padding: 0; } #online li img { cursor: pointer; } #online li div.file-wrapper { cursor: pointer; position: absolute; display: block; width: 111px; height: 111px; border: 1px solid #eee; background: url("./images/bg.png") repeat; } #online li div span.file-title{ display: block; padding: 0 3px; margin: 3px 0 0 0; font-size: 12px; height: 13px; color: #555555; text-align: center; width: 107px; white-space: nowrap; word-break: break-all; overflow: hidden; text-overflow: ellipsis; } #online li .icon { cursor: pointer; width: 113px; height: 113px; position: absolute; top: 0; left: 0; z-index: 2; border: 0; background-repeat: no-repeat; } #online li .icon:hover { width: 107px; height: 107px; border: 3px solid #1094fa; } #online li.selected .icon { background-image: url(images/success.png); background-image: url(images/success.gif) \9; background-position: 75px 75px; } #online li.selected .icon:hover { width: 107px; height: 107px; border: 3px solid #1094fa; background-position: 72px 72px; } /* 在线文件的文件预览图标 */ i.file-preview { display: block; margin: 10px auto; width: 70px; height: 70px; background-image: url("./images/file-icons.png"); background-image: url("./images/file-icons.gif") \9; background-position: -140px center; background-repeat: no-repeat; } i.file-preview.file-type-dir{ background-position: 0 center; } i.file-preview.file-type-file{ background-position: -140px center; } i.file-preview.file-type-filelist{ background-position: -210px center; } i.file-preview.file-type-zip, i.file-preview.file-type-rar, i.file-preview.file-type-7z, i.file-preview.file-type-tar, i.file-preview.file-type-gz, i.file-preview.file-type-bz2{ background-position: -280px center; } i.file-preview.file-type-xls, i.file-preview.file-type-xlsx{ background-position: -350px center; } i.file-preview.file-type-doc, i.file-preview.file-type-docx{ background-position: -420px center; } i.file-preview.file-type-ppt, i.file-preview.file-type-pptx{ background-position: -490px center; } i.file-preview.file-type-vsd{ background-position: -560px center; } i.file-preview.file-type-pdf{ background-position: -630px center; } i.file-preview.file-type-txt, i.file-preview.file-type-md, i.file-preview.file-type-json, i.file-preview.file-type-htm, i.file-preview.file-type-xml, i.file-preview.file-type-html, i.file-preview.file-type-js, i.file-preview.file-type-css, i.file-preview.file-type-php, i.file-preview.file-type-jsp, i.file-preview.file-type-asp{ background-position: -700px center; } i.file-preview.file-type-apk{ background-position: -770px center; } i.file-preview.file-type-exe{ background-position: -840px center; } i.file-preview.file-type-ipa{ background-position: -910px center; } i.file-preview.file-type-mp4, i.file-preview.file-type-swf, i.file-preview.file-type-mkv, i.file-preview.file-type-avi, i.file-preview.file-type-flv, i.file-preview.file-type-mov, i.file-preview.file-type-mpg, i.file-preview.file-type-mpeg, i.file-preview.file-type-ogv, i.file-preview.file-type-webm, i.file-preview.file-type-rm, i.file-preview.file-type-rmvb{ background-position: -980px center; } i.file-preview.file-type-ogg, i.file-preview.file-type-wav, i.file-preview.file-type-wmv, i.file-preview.file-type-mid, i.file-preview.file-type-mp3{ background-position: -1050px center; } i.file-preview.file-type-jpg, i.file-preview.file-type-jpeg, i.file-preview.file-type-gif, i.file-preview.file-type-bmp, i.file-preview.file-type-png, i.file-preview.file-type-psd{ background-position: -140px center; } ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/attachment/attachment.html ================================================ ueditor图片对话框
    0%
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/attachment/attachment.js ================================================ /** * User: Jinqn * Date: 14-04-08 * Time: 下午16:34 * 上传图片对话框逻辑代码,包括tab: 远程图片/上传图片/在线图片/搜索图片 */ (function () { var uploadFile, onlineFile; window.onload = function () { initTabs(); initButtons(); }; /* 初始化tab标签 */ function initTabs() { var tabs = $G('tabhead').children; for (var i = 0; i < tabs.length; i++) { domUtils.on(tabs[i], "click", function (e) { var target = e.target || e.srcElement; setTabFocus(target.getAttribute('data-content-id')); }); } setTabFocus('upload'); } /* 初始化tabbody */ function setTabFocus(id) { if(!id) return; var i, bodyId, tabs = $G('tabhead').children; for (i = 0; i < tabs.length; i++) { bodyId = tabs[i].getAttribute('data-content-id') if (bodyId == id) { domUtils.addClass(tabs[i], 'focus'); domUtils.addClass($G(bodyId), 'focus'); } else { domUtils.removeClasses(tabs[i], 'focus'); domUtils.removeClasses($G(bodyId), 'focus'); } } switch (id) { case 'upload': uploadFile = uploadFile || new UploadFile('queueList'); break; case 'online': onlineFile = onlineFile || new OnlineFile('fileList'); break; } } /* 初始化onok事件 */ function initButtons() { dialog.onok = function () { var list = [], id, tabs = $G('tabhead').children; for (var i = 0; i < tabs.length; i++) { if (domUtils.hasClass(tabs[i], 'focus')) { id = tabs[i].getAttribute('data-content-id'); break; } } switch (id) { case 'upload': list = uploadFile.getInsertList(); var count = uploadFile.getQueueCount(); if (count) { $('.info', '#queueList').html('' + '还有2个未上传文件'.replace(/[\d]/, count) + ''); return false; } break; case 'online': list = onlineFile.getInsertList(); break; } editor.execCommand('insertfile', list); }; } /* 上传附件 */ function UploadFile(target) { this.$wrap = target.constructor == String ? $('#' + target) : $(target); this.init(); } UploadFile.prototype = { init: function () { this.fileList = []; this.initContainer(); this.initUploader(); }, initContainer: function () { this.$queue = this.$wrap.find('.filelist'); }, /* 初始化容器 */ initUploader: function () { var _this = this, $ = jQuery, // just in case. Make sure it's not an other libaray. $wrap = _this.$wrap, // 图片容器 $queue = $wrap.find('.filelist'), // 状态栏,包括进度和控制按钮 $statusBar = $wrap.find('.statusBar'), // 文件总体选择信息。 $info = $statusBar.find('.info'), // 上传按钮 $upload = $wrap.find('.uploadBtn'), // 上传按钮 $filePickerBtn = $wrap.find('.filePickerBtn'), // 上传按钮 $filePickerBlock = $wrap.find('.filePickerBlock'), // 没选择文件之前的内容。 $placeHolder = $wrap.find('.placeholder'), // 总体进度条 $progress = $statusBar.find('.progress').hide(), // 添加的文件数量 fileCount = 0, // 添加的文件总大小 fileSize = 0, // 优化retina, 在retina下这个值是2 ratio = window.devicePixelRatio || 1, // 缩略图大小 thumbnailWidth = 113 * ratio, thumbnailHeight = 113 * ratio, // 可能有pedding, ready, uploading, confirm, done. state = '', // 所有文件的进度信息,key为file id percentages = {}, supportTransition = (function () { var s = document.createElement('p').style, r = 'transition' in s || 'WebkitTransition' in s || 'MozTransition' in s || 'msTransition' in s || 'OTransition' in s; s = null; return r; })(), // WebUploader实例 uploader, actionUrl = editor.getActionUrl(editor.getOpt('fileActionName')), fileMaxSize = editor.getOpt('fileMaxSize'), acceptExtensions = (editor.getOpt('fileAllowFiles') || []).join('').replace(/\./g, ',').replace(/^[,]/, '');; if (!WebUploader.Uploader.support()) { $('#filePickerReady').after($('
    ').html(lang.errorNotSupport)).hide(); return; } else if (!editor.getOpt('fileActionName')) { $('#filePickerReady').after($('
    ').html(lang.errorLoadConfig)).hide(); return; } uploader = _this.uploader = WebUploader.create({ pick: { id: '#filePickerReady', label: lang.uploadSelectFile }, swf: '../../third-party/webuploader/Uploader.swf', server: actionUrl, fileVal: editor.getOpt('fileFieldName'), duplicate: true, fileSingleSizeLimit: fileMaxSize, compress: false }); uploader.addButton({ id: '#filePickerBlock' }); uploader.addButton({ id: '#filePickerBtn', label: lang.uploadAddFile }); setState('pedding'); // 当有文件添加进来时执行,负责view的创建 function addFile(file) { var $li = $('
  • ' + '

    ' + file.name + '

    ' + '

    ' + '

    ' + '
  • '), $btns = $('
    ' + '' + lang.uploadDelete + '' + '' + lang.uploadTurnRight + '' + '' + lang.uploadTurnLeft + '
    ').appendTo($li), $prgress = $li.find('p.progress span'), $wrap = $li.find('p.imgWrap'), $info = $('

    ').hide().appendTo($li), showError = function (code) { switch (code) { case 'exceed_size': text = lang.errorExceedSize; break; case 'interrupt': text = lang.errorInterrupt; break; case 'http': text = lang.errorHttp; break; case 'not_allow_type': text = lang.errorFileType; break; default: text = lang.errorUploadRetry; break; } $info.text(text).show(); }; if (file.getStatus() === 'invalid') { showError(file.statusText); } else { $wrap.text(lang.uploadPreview); if ('|png|jpg|jpeg|bmp|gif|'.indexOf('|'+file.ext.toLowerCase()+'|') == -1) { $wrap.empty().addClass('notimage').append('' + '' + file.name + ''); } else { if (browser.ie && browser.version <= 7) { $wrap.text(lang.uploadNoPreview); } else { uploader.makeThumb(file, function (error, src) { if (error || !src) { $wrap.text(lang.uploadNoPreview); } else { var $img = $(''); $wrap.empty().append($img); $img.on('error', function () { $wrap.text(lang.uploadNoPreview); }); } }, thumbnailWidth, thumbnailHeight); } } percentages[ file.id ] = [ file.size, 0 ]; file.rotation = 0; /* 检查文件格式 */ if (!file.ext || acceptExtensions.indexOf(file.ext.toLowerCase()) == -1) { showError('not_allow_type'); uploader.removeFile(file); } } file.on('statuschange', function (cur, prev) { if (prev === 'progress') { $prgress.hide().width(0); } else if (prev === 'queued') { $li.off('mouseenter mouseleave'); $btns.remove(); } // 成功 if (cur === 'error' || cur === 'invalid') { showError(file.statusText); percentages[ file.id ][ 1 ] = 1; } else if (cur === 'interrupt') { showError('interrupt'); } else if (cur === 'queued') { percentages[ file.id ][ 1 ] = 0; } else if (cur === 'progress') { $info.hide(); $prgress.css('display', 'block'); } else if (cur === 'complete') { } $li.removeClass('state-' + prev).addClass('state-' + cur); }); $li.on('mouseenter', function () { $btns.stop().animate({height: 30}); }); $li.on('mouseleave', function () { $btns.stop().animate({height: 0}); }); $btns.on('click', 'span', function () { var index = $(this).index(), deg; switch (index) { case 0: uploader.removeFile(file); return; case 1: file.rotation += 90; break; case 2: file.rotation -= 90; break; } if (supportTransition) { deg = 'rotate(' + file.rotation + 'deg)'; $wrap.css({ '-webkit-transform': deg, '-mos-transform': deg, '-o-transform': deg, 'transform': deg }); } else { $wrap.css('filter', 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' + (~~((file.rotation / 90) % 4 + 4) % 4) + ')'); } }); $li.insertBefore($filePickerBlock); } // 负责view的销毁 function removeFile(file) { var $li = $('#' + file.id); delete percentages[ file.id ]; updateTotalProgress(); $li.off().find('.file-panel').off().end().remove(); } function updateTotalProgress() { var loaded = 0, total = 0, spans = $progress.children(), percent; $.each(percentages, function (k, v) { total += v[ 0 ]; loaded += v[ 0 ] * v[ 1 ]; }); percent = total ? loaded / total : 0; spans.eq(0).text(Math.round(percent * 100) + '%'); spans.eq(1).css('width', Math.round(percent * 100) + '%'); updateStatus(); } function setState(val, files) { if (val != state) { var stats = uploader.getStats(); $upload.removeClass('state-' + state); $upload.addClass('state-' + val); switch (val) { /* 未选择文件 */ case 'pedding': $queue.addClass('element-invisible'); $statusBar.addClass('element-invisible'); $placeHolder.removeClass('element-invisible'); $progress.hide(); $info.hide(); uploader.refresh(); break; /* 可以开始上传 */ case 'ready': $placeHolder.addClass('element-invisible'); $queue.removeClass('element-invisible'); $statusBar.removeClass('element-invisible'); $progress.hide(); $info.show(); $upload.text(lang.uploadStart); uploader.refresh(); break; /* 上传中 */ case 'uploading': $progress.show(); $info.hide(); $upload.text(lang.uploadPause); break; /* 暂停上传 */ case 'paused': $progress.show(); $info.hide(); $upload.text(lang.uploadContinue); break; case 'confirm': $progress.show(); $info.hide(); $upload.text(lang.uploadStart); stats = uploader.getStats(); if (stats.successNum && !stats.uploadFailNum) { setState('finish'); return; } break; case 'finish': $progress.hide(); $info.show(); if (stats.uploadFailNum) { $upload.text(lang.uploadRetry); } else { $upload.text(lang.uploadStart); } break; } state = val; updateStatus(); } if (!_this.getQueueCount()) { $upload.addClass('disabled') } else { $upload.removeClass('disabled') } } function updateStatus() { var text = '', stats; if (state === 'ready') { text = lang.updateStatusReady.replace('_', fileCount).replace('_KB', WebUploader.formatSize(fileSize)); } else if (state === 'confirm') { stats = uploader.getStats(); if (stats.uploadFailNum) { text = lang.updateStatusConfirm.replace('_', stats.successNum).replace('_', stats.successNum); } } else { stats = uploader.getStats(); text = lang.updateStatusFinish.replace('_', fileCount). replace('_KB', WebUploader.formatSize(fileSize)). replace('_', stats.successNum); if (stats.uploadFailNum) { text += lang.updateStatusError.replace('_', stats.uploadFailNum); } } $info.html(text); } uploader.on('fileQueued', function (file) { fileCount++; fileSize += file.size; if (fileCount === 1) { $placeHolder.addClass('element-invisible'); $statusBar.show(); } addFile(file); }); uploader.on('fileDequeued', function (file) { fileCount--; fileSize -= file.size; removeFile(file); updateTotalProgress(); }); uploader.on('filesQueued', function (file) { if (!uploader.isInProgress() && (state == 'pedding' || state == 'finish' || state == 'confirm' || state == 'ready')) { setState('ready'); } updateTotalProgress(); }); uploader.on('all', function (type, files) { switch (type) { case 'uploadFinished': setState('confirm', files); break; case 'startUpload': /* 添加额外的GET参数 */ var params = utils.serializeParam(editor.queryCommandValue('serverparam')) || '', url = utils.formatUrl(actionUrl + (actionUrl.indexOf('?') == -1 ? '?':'&') + 'encode=utf-8&' + params); uploader.option('server', url); setState('uploading', files); break; case 'stopUpload': setState('paused', files); break; } }); uploader.on('uploadBeforeSend', function (file, data, header) { //这里可以通过data对象添加POST参数 header['X_Requested_With'] = 'XMLHttpRequest'; // HaoChuan9421 if(editor.options.headers && Object.prototype.toString.apply(editor.options.headers) === "[object Object]"){ for(var key in editor.options.headers){ header[key] = editor.options.headers[key] } } }); uploader.on('uploadProgress', function (file, percentage) { var $li = $('#' + file.id), $percent = $li.find('.progress span'); $percent.css('width', percentage * 100 + '%'); percentages[ file.id ][ 1 ] = percentage; updateTotalProgress(); }); uploader.on('uploadSuccess', function (file, ret) { var $file = $('#' + file.id); try { var responseText = (ret._raw || ret), json = utils.str2json(responseText); if (json.state == 'SUCCESS') { _this.fileList.push(json); $file.append(''); } else { $file.find('.error').text(json.state).show(); } } catch (e) { $file.find('.error').text(lang.errorServerUpload).show(); } }); uploader.on('uploadError', function (file, code) { }); uploader.on('error', function (code, file) { if (code == 'Q_TYPE_DENIED' || code == 'F_EXCEED_SIZE') { addFile(file); } }); uploader.on('uploadComplete', function (file, ret) { }); $upload.on('click', function () { if ($(this).hasClass('disabled')) { return false; } if (state === 'ready') { uploader.upload(); } else if (state === 'paused') { uploader.upload(); } else if (state === 'uploading') { uploader.stop(); } }); $upload.addClass('state-' + state); updateTotalProgress(); }, getQueueCount: function () { var file, i, status, readyFile = 0, files = this.uploader.getFiles(); for (i = 0; file = files[i++]; ) { status = file.getStatus(); if (status == 'queued' || status == 'uploading' || status == 'progress') readyFile++; } return readyFile; }, getInsertList: function () { var i, link, data, list = [], prefix = editor.getOpt('fileUrlPrefix'); for (i = 0; i < this.fileList.length; i++) { data = this.fileList[i]; link = data.url; list.push({ title: data.original || link.substr(link.lastIndexOf('/') + 1), url: prefix + link }); } return list; } }; /* 在线附件 */ function OnlineFile(target) { this.container = utils.isString(target) ? document.getElementById(target) : target; this.init(); } OnlineFile.prototype = { init: function () { this.initContainer(); this.initEvents(); this.initData(); }, /* 初始化容器 */ initContainer: function () { this.container.innerHTML = ''; this.list = document.createElement('ul'); this.clearFloat = document.createElement('li'); domUtils.addClass(this.list, 'list'); domUtils.addClass(this.clearFloat, 'clearFloat'); this.list.appendChild(this.clearFloat); this.container.appendChild(this.list); }, /* 初始化滚动事件,滚动到地步自动拉取数据 */ initEvents: function () { var _this = this; /* 滚动拉取图片 */ domUtils.on($G('fileList'), 'scroll', function(e){ var panel = this; if (panel.scrollHeight - (panel.offsetHeight + panel.scrollTop) < 10) { _this.getFileData(); } }); /* 选中图片 */ domUtils.on(this.list, 'click', function (e) { var target = e.target || e.srcElement, li = target.parentNode; if (li.tagName.toLowerCase() == 'li') { if (domUtils.hasClass(li, 'selected')) { domUtils.removeClasses(li, 'selected'); } else { domUtils.addClass(li, 'selected'); } } }); }, /* 初始化第一次的数据 */ initData: function () { /* 拉取数据需要使用的值 */ this.state = 0; this.listSize = editor.getOpt('fileManagerListSize'); this.listIndex = 0; this.listEnd = false; /* 第一次拉取数据 */ this.getFileData(); }, /* 向后台拉取图片列表数据 */ getFileData: function () { var _this = this; if(!_this.listEnd && !this.isLoadingData) { this.isLoadingData = true; ajax.request(editor.getActionUrl(editor.getOpt('fileManagerActionName')), { timeout: 100000, data: utils.extend({ start: this.listIndex, size: this.listSize }, editor.queryCommandValue('serverparam')), method: 'get', onsuccess: function (r) { try { var json = eval('(' + r.responseText + ')'); if (json.state == 'SUCCESS') { _this.pushData(json.list); _this.listIndex = parseInt(json.start) + parseInt(json.list.length); if(_this.listIndex >= json.total) { _this.listEnd = true; } _this.isLoadingData = false; } } catch (e) { if(r.responseText.indexOf('ue_separate_ue') != -1) { var list = r.responseText.split(r.responseText); _this.pushData(list); _this.listIndex = parseInt(list.length); _this.listEnd = true; _this.isLoadingData = false; } } }, onerror: function () { _this.isLoadingData = false; } }); } }, /* 添加图片到列表界面上 */ pushData: function (list) { var i, item, img, filetype, preview, icon, _this = this, urlPrefix = editor.getOpt('fileManagerUrlPrefix'); for (i = 0; i < list.length; i++) { if(list[i] && list[i].url) { item = document.createElement('li'); icon = document.createElement('span'); filetype = list[i].url.substr(list[i].url.lastIndexOf('.') + 1); if ( "png|jpg|jpeg|gif|bmp".indexOf(filetype) != -1 ) { preview = document.createElement('img'); domUtils.on(preview, 'load', (function(image){ return function(){ _this.scale(image, image.parentNode.offsetWidth, image.parentNode.offsetHeight); }; })(preview)); preview.width = 113; preview.setAttribute('src', urlPrefix + list[i].url + (list[i].url.indexOf('?') == -1 ? '?noCache=':'&noCache=') + (+new Date()).toString(36) ); } else { var ic = document.createElement('i'), textSpan = document.createElement('span'); textSpan.innerHTML = list[i].url.substr(list[i].url.lastIndexOf('/') + 1); preview = document.createElement('div'); preview.appendChild(ic); preview.appendChild(textSpan); domUtils.addClass(preview, 'file-wrapper'); domUtils.addClass(textSpan, 'file-title'); domUtils.addClass(ic, 'file-type-' + filetype); domUtils.addClass(ic, 'file-preview'); } domUtils.addClass(icon, 'icon'); item.setAttribute('data-url', urlPrefix + list[i].url); if (list[i].original) { item.setAttribute('data-title', list[i].original); } item.appendChild(preview); item.appendChild(icon); this.list.insertBefore(item, this.clearFloat); } } }, /* 改变图片大小 */ scale: function (img, w, h, type) { var ow = img.width, oh = img.height; if (type == 'justify') { if (ow >= oh) { img.width = w; img.height = h * oh / ow; img.style.marginLeft = '-' + parseInt((img.width - w) / 2) + 'px'; } else { img.width = w * ow / oh; img.height = h; img.style.marginTop = '-' + parseInt((img.height - h) / 2) + 'px'; } } else { if (ow >= oh) { img.width = w * ow / oh; img.height = h; img.style.marginLeft = '-' + parseInt((img.width - w) / 2) + 'px'; } else { img.width = w; img.height = h * oh / ow; img.style.marginTop = '-' + parseInt((img.height - h) / 2) + 'px'; } } }, getInsertList: function () { var i, lis = this.list.children, list = []; for (i = 0; i < lis.length; i++) { if (domUtils.hasClass(lis[i], 'selected')) { var url = lis[i].getAttribute('data-url'); var title = lis[i].getAttribute('data-title') || url.substr(url.lastIndexOf('/') + 1); list.push({ title: title, url: url }); } } return list; } }; })(); ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/background/background.css ================================================ .wrapper{ width: 424px;margin: 10px auto; zoom:1;position: relative} .tabbody{height:225px;} .tabbody .panel { position: absolute;width:100%; height:100%;background: #fff; display: none;} .tabbody .focus { display: block;} body{font-size: 12px;color: #888;overflow: hidden;} input,label{vertical-align:middle} .clear{clear: both;} .pl{padding-left: 18px;padding-left: 23px\9;} #imageList {width: 420px;height: 215px;margin-top: 10px;overflow: hidden;overflow-y: auto;} #imageList div {float: left;width: 100px;height: 95px;margin: 5px 10px;} #imageList img {cursor: pointer;border: 2px solid white;} .bgarea{margin: 10px;padding: 5px;height: 84%;border: 1px solid #A8A297;} .content div{margin: 10px 0 10px 5px;} .content .iptradio{margin: 0px 5px 5px 0px;} .txt{width:280px;} .wrapcolor{height: 19px;} div.color{float: left;margin: 0;} #colorPicker{width: 17px;height: 17px;border: 1px solid #CCC;display: inline-block;border-radius: 3px;box-shadow: 2px 2px 5px #D3D6DA;margin: 0;float: left;} div.alignment,#custom{margin-left: 23px;margin-left: 28px\9;} #custom input{height: 15px;min-height: 15px;width:20px;} #repeatType{width:100px;} /* 图片管理样式 */ #imgManager { width: 100%; height: 225px; } #imgManager #imageList{ width: 100%; overflow-x: hidden; overflow-y: auto; } #imgManager ul { display: block; list-style: none; margin: 0; padding: 0; } #imgManager li { float: left; display: block; list-style: none; padding: 0; width: 113px; height: 113px; margin: 9px 0 0 19px; background-color: #eee; overflow: hidden; cursor: pointer; position: relative; } #imgManager li.clearFloat { float: none; clear: both; display: block; width:0; height:0; margin: 0; padding: 0; } #imgManager li img { cursor: pointer; } #imgManager li .icon { cursor: pointer; width: 113px; height: 113px; position: absolute; top: 0; left: 0; z-index: 2; border: 0; background-repeat: no-repeat; } #imgManager li .icon:hover { width: 107px; height: 107px; border: 3px solid #1094fa; } #imgManager li.selected .icon { background-image: url(images/success.png); background-position: 75px 75px; } #imgManager li.selected .icon:hover { width: 107px; height: 107px; border: 3px solid #1094fa; background-position: 72px 72px; } ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/background/background.html ================================================
    :
    :
    :x:px  y:px
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/background/background.js ================================================ (function () { var onlineImage, backupStyle = editor.queryCommandValue('background'); window.onload = function () { initTabs(); initColorSelector(); }; /* 初始化tab标签 */ function initTabs(){ var tabs = $G('tabHeads').children; for (var i = 0; i < tabs.length; i++) { domUtils.on(tabs[i], "click", function (e) { var target = e.target || e.srcElement; for (var j = 0; j < tabs.length; j++) { if(tabs[j] == target){ tabs[j].className = "focus"; var contentId = tabs[j].getAttribute('data-content-id'); $G(contentId).style.display = "block"; if(contentId == 'imgManager') { initImagePanel(); } }else { tabs[j].className = ""; $G(tabs[j].getAttribute('data-content-id')).style.display = "none"; } } }); } } /* 初始化颜色设置 */ function initColorSelector () { var obj = editor.queryCommandValue('background'); if (obj) { var color = obj['background-color'], repeat = obj['background-repeat'] || 'repeat', image = obj['background-image'] || '', position = obj['background-position'] || 'center center', pos = position.split(' '), x = parseInt(pos[0]) || 0, y = parseInt(pos[1]) || 0; if(repeat == 'no-repeat' && (x || y)) repeat = 'self'; image = image.match(/url[\s]*\(([^\)]*)\)/); image = image ? image[1]:''; updateFormState('colored', color, image, repeat, x, y); } else { updateFormState(); } var updateHandler = function () { updateFormState(); updateBackground(); } domUtils.on($G('nocolorRadio'), 'click', updateBackground); domUtils.on($G('coloredRadio'), 'click', updateHandler); domUtils.on($G('url'), 'keyup', function(){ if($G('url').value && $G('alignment').style.display == "none") { utils.each($G('repeatType').children, function(item){ item.selected = ('repeat' == item.getAttribute('value') ? 'selected':false); }); } updateHandler(); }); domUtils.on($G('repeatType'), 'change', updateHandler); domUtils.on($G('x'), 'keyup', updateBackground); domUtils.on($G('y'), 'keyup', updateBackground); initColorPicker(); } /* 初始化颜色选择器 */ function initColorPicker() { var me = editor, cp = $G("colorPicker"); /* 生成颜色选择器ui对象 */ var popup = new UE.ui.Popup({ content: new UE.ui.ColorPicker({ noColorText: me.getLang("clearColor"), editor: me, onpickcolor: function (t, color) { updateFormState('colored', color); updateBackground(); UE.ui.Popup.postHide(); }, onpicknocolor: function (t, color) { updateFormState('colored', 'transparent'); updateBackground(); UE.ui.Popup.postHide(); } }), editor: me, onhide: function () { } }); /* 设置颜色选择器 */ domUtils.on(cp, "click", function () { popup.showAnchor(this); }); domUtils.on(document, 'mousedown', function (evt) { var el = evt.target || evt.srcElement; UE.ui.Popup.postHide(el); }); domUtils.on(window, 'scroll', function () { UE.ui.Popup.postHide(); }); } /* 初始化在线图片列表 */ function initImagePanel() { onlineImage = onlineImage || new OnlineImage('imageList'); } /* 更新背景色设置面板 */ function updateFormState (radio, color, url, align, x, y) { var nocolorRadio = $G('nocolorRadio'), coloredRadio = $G('coloredRadio'); if(radio) { nocolorRadio.checked = (radio == 'colored' ? false:'checked'); coloredRadio.checked = (radio == 'colored' ? 'checked':false); } if(color) { domUtils.setStyle($G("colorPicker"), "background-color", color); } if(url && /^\//.test(url)) { var a = document.createElement('a'); a.href = url; browser.ie && (a.href = a.href); url = browser.ie ? a.href:(a.protocol + '//' + a.host + a.pathname + a.search + a.hash); } if(url || url === '') { $G('url').value = url; } if(align) { utils.each($G('repeatType').children, function(item){ item.selected = (align == item.getAttribute('value') ? 'selected':false); }); } if(x || y) { $G('x').value = parseInt(x) || 0; $G('y').value = parseInt(y) || 0; } $G('alignment').style.display = coloredRadio.checked && $G('url').value ? '':'none'; $G('custom').style.display = coloredRadio.checked && $G('url').value && $G('repeatType').value == 'self' ? '':'none'; } /* 更新背景颜色 */ function updateBackground () { if ($G('coloredRadio').checked) { var color = domUtils.getStyle($G("colorPicker"), "background-color"), bgimg = $G("url").value, align = $G("repeatType").value, backgroundObj = { "background-repeat": "no-repeat", "background-position": "center center" }; if (color) backgroundObj["background-color"] = color; if (bgimg) backgroundObj["background-image"] = 'url(' + bgimg + ')'; if (align == 'self') { backgroundObj["background-position"] = $G("x").value + "px " + $G("y").value + "px"; } else if (align == 'repeat-x' || align == 'repeat-y' || align == 'repeat') { backgroundObj["background-repeat"] = align; } editor.execCommand('background', backgroundObj); } else { editor.execCommand('background', null); } } /* 在线图片 */ function OnlineImage(target) { this.container = utils.isString(target) ? document.getElementById(target) : target; this.init(); } OnlineImage.prototype = { init: function () { this.reset(); this.initEvents(); }, /* 初始化容器 */ initContainer: function () { this.container.innerHTML = ''; this.list = document.createElement('ul'); this.clearFloat = document.createElement('li'); domUtils.addClass(this.list, 'list'); domUtils.addClass(this.clearFloat, 'clearFloat'); this.list.id = 'imageListUl'; this.list.appendChild(this.clearFloat); this.container.appendChild(this.list); }, /* 初始化滚动事件,滚动到地步自动拉取数据 */ initEvents: function () { var _this = this; /* 滚动拉取图片 */ domUtils.on($G('imageList'), 'scroll', function(e){ var panel = this; if (panel.scrollHeight - (panel.offsetHeight + panel.scrollTop) < 10) { _this.getImageData(); } }); /* 选中图片 */ domUtils.on(this.container, 'click', function (e) { var target = e.target || e.srcElement, li = target.parentNode, nodes = $G('imageListUl').childNodes; if (li.tagName.toLowerCase() == 'li') { updateFormState('nocolor', null, ''); for (var i = 0, node; node = nodes[i++];) { if (node == li && !domUtils.hasClass(node, 'selected')) { domUtils.addClass(node, 'selected'); updateFormState('colored', null, li.firstChild.getAttribute("_src"), 'repeat'); } else { domUtils.removeClasses(node, 'selected'); } } updateBackground(); } }); }, /* 初始化第一次的数据 */ initData: function () { /* 拉取数据需要使用的值 */ this.state = 0; this.listSize = editor.getOpt('imageManagerListSize'); this.listIndex = 0; this.listEnd = false; /* 第一次拉取数据 */ this.getImageData(); }, /* 重置界面 */ reset: function() { this.initContainer(); this.initData(); }, /* 向后台拉取图片列表数据 */ getImageData: function () { var _this = this; if(!_this.listEnd && !this.isLoadingData) { this.isLoadingData = true; var url = editor.getActionUrl(editor.getOpt('imageManagerActionName')), isJsonp = utils.isCrossDomainUrl(url); ajax.request(url, { 'timeout': 100000, 'dataType': isJsonp ? 'jsonp':'', 'data': utils.extend({ start: this.listIndex, size: this.listSize }, editor.queryCommandValue('serverparam')), 'method': 'get', 'onsuccess': function (r) { try { var json = isJsonp ? r:eval('(' + r.responseText + ')'); if (json.state == 'SUCCESS') { _this.pushData(json.list); _this.listIndex = parseInt(json.start) + parseInt(json.list.length); if(_this.listIndex >= json.total) { _this.listEnd = true; } _this.isLoadingData = false; } } catch (e) { if(r.responseText.indexOf('ue_separate_ue') != -1) { var list = r.responseText.split(r.responseText); _this.pushData(list); _this.listIndex = parseInt(list.length); _this.listEnd = true; _this.isLoadingData = false; } } }, 'onerror': function () { _this.isLoadingData = false; } }); } }, /* 添加图片到列表界面上 */ pushData: function (list) { var i, item, img, icon, _this = this, urlPrefix = editor.getOpt('imageManagerUrlPrefix'); for (i = 0; i < list.length; i++) { if(list[i] && list[i].url) { item = document.createElement('li'); img = document.createElement('img'); icon = document.createElement('span'); domUtils.on(img, 'load', (function(image){ return function(){ _this.scale(image, image.parentNode.offsetWidth, image.parentNode.offsetHeight); } })(img)); img.width = 113; img.setAttribute('src', urlPrefix + list[i].url + (list[i].url.indexOf('?') == -1 ? '?noCache=':'&noCache=') + (+new Date()).toString(36) ); img.setAttribute('_src', urlPrefix + list[i].url); domUtils.addClass(icon, 'icon'); item.appendChild(img); item.appendChild(icon); this.list.insertBefore(item, this.clearFloat); } } }, /* 改变图片大小 */ scale: function (img, w, h, type) { var ow = img.width, oh = img.height; if (type == 'justify') { if (ow >= oh) { img.width = w; img.height = h * oh / ow; img.style.marginLeft = '-' + parseInt((img.width - w) / 2) + 'px'; } else { img.width = w * ow / oh; img.height = h; img.style.marginTop = '-' + parseInt((img.height - h) / 2) + 'px'; } } else { if (ow >= oh) { img.width = w * ow / oh; img.height = h; img.style.marginLeft = '-' + parseInt((img.width - w) / 2) + 'px'; } else { img.width = w; img.height = h * oh / ow; img.style.marginTop = '-' + parseInt((img.height - h) / 2) + 'px'; } } }, getInsertList: function () { var i, lis = this.list.children, list = [], align = getAlign(); for (i = 0; i < lis.length; i++) { if (domUtils.hasClass(lis[i], 'selected')) { var img = lis[i].firstChild, src = img.getAttribute('_src'); list.push({ src: src, _src: src, floatStyle: align }); } } return list; } }; dialog.onok = function () { updateBackground(); editor.fireEvent('saveScene'); }; dialog.oncancel = function () { editor.execCommand('background', backupStyle); }; })(); ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/charts/chart.config.js ================================================ /* * 图表配置文件 * */ //不同类型的配置 var typeConfig = [ { chart: { type: 'line' }, plotOptions: { line: { dataLabels: { enabled: false }, enableMouseTracking: true } } }, { chart: { type: 'line' }, plotOptions: { line: { dataLabels: { enabled: true }, enableMouseTracking: false } } }, { chart: { type: 'area' } }, { chart: { type: 'bar' } }, { chart: { type: 'column' } }, { chart: { plotBackgroundColor: null, plotBorderWidth: null, plotShadow: false }, plotOptions: { pie: { allowPointSelect: true, cursor: 'pointer', dataLabels: { enabled: true, color: '#000000', connectorColor: '#000000', formatter: function() { return ''+ this.point.name +': '+ ( Math.round( this.point.percentage*100 ) / 100 ) +' %'; } } } } } ]; ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/charts/charts.css ================================================ html, body { width: 100%; height: 100%; margin: 0; padding: 0; overflow-x: hidden; } .main { width: 100%; overflow: hidden; } .table-view { height: 100%; float: left; margin: 20px; width: 40%; } .table-view .table-container { width: 100%; margin-bottom: 50px; overflow: scroll; } .table-view th { padding: 5px 10px; background-color: #F7F7F7; } .table-view td { width: 50px; text-align: center; padding:0; } .table-container input { width: 40px; padding: 5px; border: none; outline: none; } .table-view caption { font-size: 18px; text-align: left; } .charts-view { /*margin-left: 49%!important;*/ width: 50%; margin-left: 49%; height: 400px; } .charts-container { border-left: 1px solid #c3c3c3; } .charts-format fieldset { padding-left: 20px; margin-bottom: 50px; } .charts-format legend { padding-left: 10px; padding-right: 10px; } .format-item-container { padding: 20px; } .format-item-container label { display: block; margin: 10px 0; } .charts-format .data-item { border: 1px solid black; outline: none; padding: 2px 3px; } /* 图表类型 */ .charts-type { margin-top: 50px; height: 300px; } .scroll-view { border: 1px solid #c3c3c3; border-left: none; border-right: none; overflow: hidden; } .scroll-container { margin: 20px; width: 100%; overflow: hidden; } .scroll-bed { width: 10000px; _margin-top: 20px; -webkit-transition: margin-left .5s ease; -moz-transition: margin-left .5s ease; transition: margin-left .5s ease; } .view-box { display: inline-block; *display: inline; *zoom: 1; margin-right: 20px; border: 2px solid white; line-height: 0; overflow: hidden; cursor: pointer; } .view-box img { border: 1px solid #cecece; } .view-box.selected { border-color: #7274A7; } .button-container { margin-bottom: 20px; text-align: center; } .button-container a { display: inline-block; width: 100px; height: 25px; line-height: 25px; border: 1px solid #c2ccd1; margin-right: 30px; text-decoration: none; color: black; -webkit-border-radius: 2px; -moz-border-radius: 2px; border-radius: 2px; } .button-container a:HOVER { background: #fcfcfc; } .button-container a:ACTIVE { border-top-color: #c2ccd1; box-shadow:inset 0 5px 4px -4px rgba(49, 49, 64, 0.1); } .edui-charts-not-data { height: 100px; line-height: 100px; text-align: center; } ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/charts/charts.html ================================================ chart


    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/charts/charts.js ================================================ /* * 图片转换对话框脚本 **/ var tableData = [], //编辑器页面table editorTable = null, chartsConfig = window.typeConfig, resizeTimer = null, //初始默认图表类型 currentChartType = 0; window.onload = function () { editorTable = domUtils.findParentByTagName( editor.selection.getRange().startContainer, 'table', true); //未找到表格, 显示错误页面 if ( !editorTable ) { document.body.innerHTML = "
    未找到数据
    "; return; } //初始化图表类型选择 initChartsTypeView(); renderTable( editorTable ); initEvent(); initUserConfig( editorTable.getAttribute( "data-chart" ) ); $( "#scrollBed .view-box:eq("+ currentChartType +")" ).trigger( "click" ); updateViewType( currentChartType ); dialog.addListener( "resize", function () { if ( resizeTimer != null ) { window.clearTimeout( resizeTimer ); } resizeTimer = window.setTimeout( function () { resizeTimer = null; renderCharts(); }, 500 ); } ); }; function initChartsTypeView () { var contents = []; for ( var i = 0, len = chartsConfig.length; i
    ' ); } $( "#scrollBed" ).html( contents.join( "" ) ); } //渲染table, 以便用户修改数据 function renderTable ( table ) { var tableHtml = []; //构造数据 for ( var i = 0, row; row = table.rows[ i ]; i++ ) { tableData[ i ] = []; tableHtml[ i ] = []; for ( var j = 0, cell; cell = row.cells[ j ]; j++ ) { var value = getCellValue( cell ); if ( i > 0 && j > 0 ) { value = +value; } if ( i === 0 || j === 0 ) { tableHtml[ i ].push( ''+ value +'' ); } else { tableHtml[ i ].push( '' ); } tableData[ i ][ j ] = value; } tableHtml[ i ] = tableHtml[ i ].join( "" ); } //draw 表格 $( "#tableContainer" ).html( ''+ tableHtml.join( "" ) +'
    ' ); } /* * 根据表格已有的图表属性初始化当前图表属性 */ function initUserConfig ( config ) { var parsedConfig = {}; if ( !config ) { return; } config = config.split( ";" ); $.each( config, function ( index, item ) { item = item.split( ":" ); parsedConfig[ item[ 0 ] ] = item[ 1 ]; } ); setUserConfig( parsedConfig ); } function initEvent () { var cacheValue = null, //图表类型数 typeViewCount = chartsConfig.length- 1, $chartsTypeViewBox = $( '#scrollBed .view-box' ); $( ".charts-format" ).delegate( ".format-ctrl", "change", function () { renderCharts(); } ) $( ".table-view" ).delegate( ".data-item", "focus", function () { cacheValue = this.value; } ).delegate( ".data-item", "blur", function () { if ( this.value !== cacheValue ) { renderCharts(); } cacheValue = null; } ); $( "#buttonContainer" ).delegate( "a", "click", function (e) { e.preventDefault(); if ( this.getAttribute( "data-title" ) === 'prev' ) { if ( currentChartType > 0 ) { currentChartType--; updateViewType( currentChartType ); } } else { if ( currentChartType < typeViewCount ) { currentChartType++; updateViewType( currentChartType ); } } } ); //图表类型变化 $( '#scrollBed' ).delegate( ".view-box", "click", function (e) { var index = $( this ).attr( "data-chart-type" ); $chartsTypeViewBox.removeClass( "selected" ); $( $chartsTypeViewBox[ index ] ).addClass( "selected" ); currentChartType = index | 0; //饼图, 禁用部分配置 if ( currentChartType === chartsConfig.length - 1 ) { disableNotPieConfig(); //启用完整配置 } else { enableNotPieConfig(); } renderCharts(); } ); } function renderCharts () { var data = collectData(); $('#chartsContainer').highcharts( $.extend( {}, chartsConfig[ currentChartType ], { credits: { enabled: false }, exporting: { enabled: false }, title: { text: data.title, x: -20 //center }, subtitle: { text: data.subTitle, x: -20 }, xAxis: { title: { text: data.xTitle }, categories: data.categories }, yAxis: { title: { text: data.yTitle }, plotLines: [{ value: 0, width: 1, color: '#808080' }] }, tooltip: { enabled: true, valueSuffix: data.suffix }, legend: { layout: 'vertical', align: 'right', verticalAlign: 'middle', borderWidth: 1 }, series: data.series } )); } function updateViewType ( index ) { $( "#scrollBed" ).css( 'marginLeft', -index*324+'px' ); } function collectData () { var form = document.forms[ 'data-form' ], data = null; if ( currentChartType !== chartsConfig.length - 1 ) { data = getSeriesAndCategories(); $.extend( data, getUserConfig() ); //饼图数据格式 } else { data = getSeriesForPieChart(); data.title = form[ 'title' ].value; data.suffix = form[ 'unit' ].value; } return data; } /** * 获取用户配置信息 */ function getUserConfig () { var form = document.forms[ 'data-form' ], info = { title: form[ 'title' ].value, subTitle: form[ 'sub-title' ].value, xTitle: form[ 'x-title' ].value, yTitle: form[ 'y-title' ].value, suffix: form[ 'unit' ].value, //数据对齐方式 tableDataFormat: getTableDataFormat (), //饼图提示文字 tip: $( "#tipInput" ).val() }; return info; } function setUserConfig ( config ) { var form = document.forms[ 'data-form' ]; config.title && ( form[ 'title' ].value = config.title ); config.subTitle && ( form[ 'sub-title' ].value = config.subTitle ); config.xTitle && ( form[ 'x-title' ].value = config.xTitle ); config.yTitle && ( form[ 'y-title' ].value = config.yTitle ); config.suffix && ( form[ 'unit' ].value = config.suffix ); config.dataFormat == "-1" && ( form[ 'charts-format' ][ 1 ].checked = true ); config.tip && ( form[ 'tip' ].value = config.tip ); currentChartType = config.chartType || 0; } function getSeriesAndCategories () { var form = document.forms[ 'data-form' ], series = [], categories = [], tmp = [], tableData = getTableData(); //反转数据 if ( getTableDataFormat() === "-1" ) { for ( var i = 0, len = tableData.length; i < len; i++ ) { for ( var j = 0, jlen = tableData[ i ].length; j < jlen; j++ ) { if ( !tmp[ j ] ) { tmp[ j ] = []; } tmp[ j ][ i ] = tableData[ i ][ j ]; } } tableData = tmp; } categories = tableData[0].slice( 1 ); for ( var i = 1, data; data = tableData[ i ]; i++ ) { series.push( { name: data[ 0 ], data: data.slice( 1 ) } ); } return { series: series, categories: categories }; } /* * 获取数据源数据对齐方式 */ function getTableDataFormat () { var form = document.forms[ 'data-form' ], items = form['charts-format']; return items[ 0 ].checked ? items[ 0 ].value : items[ 1 ].value; } /* * 禁用非饼图类型的配置项 */ function disableNotPieConfig() { updateConfigItem( 'disable' ); } /* * 启用非饼图类型的配置项 */ function enableNotPieConfig() { updateConfigItem( 'enable' ); } function updateConfigItem ( value ) { var table = $( "#showTable" )[ 0 ], isDisable = value === 'disable' ? true : false; //table中的input处理 for ( var i = 2 , row; row = table.rows[ i ]; i++ ) { for ( var j = 1, cell; cell = row.cells[ j ]; j++ ) { $( "input", cell ).attr( "disabled", isDisable ); } } //其他项处理 $( "input.not-pie-item" ).attr( "disabled", isDisable ); $( "#tipInput" ).attr( "disabled", !isDisable ) } /* * 获取饼图数据 * 饼图的数据只取第一行的 **/ function getSeriesForPieChart () { var series = { type: 'pie', name: $("#tipInput").val(), data: [] }, tableData = getTableData(); for ( var j = 1, jlen = tableData[ 0 ].length; j < jlen; j++ ) { var title = tableData[ 0 ][ j ], val = tableData[ 1 ][ j ]; series.data.push( [ title, val ] ); } return { series: [ series ] }; } function getTableData () { var table = document.getElementById( "showTable" ), xCount = table.rows[0].cells.length - 1, values = getTableInputValue(); for ( var i = 0, value; value = values[ i ]; i++ ) { tableData[ Math.floor( i / xCount ) + 1 ][ i % xCount + 1 ] = values[ i ]; } return tableData; } function getTableInputValue () { var table = document.getElementById( "showTable" ), inputs = table.getElementsByTagName( "input" ), values = []; for ( var i = 0, input; input = inputs[ i ]; i++ ) { values.push( input.value | 0 ); } return values; } function getCellValue ( cell ) { var value = utils.trim( ( cell.innerText || cell.textContent || '' ) ); return value.replace( new RegExp( UE.dom.domUtils.fillChar, 'g' ), '' ).replace( /^\s+|\s+$/g, '' ); } //dialog确认事件 dialog.onok = function () { //收集信息 var form = document.forms[ 'data-form' ], info = getUserConfig(); //添加图表类型 info.chartType = currentChartType; //同步表格数据到编辑器 syncTableData(); //执行图表命令 editor.execCommand( 'charts', info ); }; /* * 同步图表编辑视图的表格数据到编辑器里的原始表格 */ function syncTableData () { var tableData = getTableData(); for ( var i = 1, row; row = editorTable.rows[ i ]; i++ ) { for ( var j = 1, cell; cell = row.cells[ j ]; j++ ) { cell.innerHTML = tableData[ i ] [ j ]; } } } ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/emotion/emotion.css ================================================ .jd img{ background:transparent url(images/jxface2.gif?v=1.1) no-repeat scroll left top; cursor:pointer;width:35px;height:35px;display:block; } .pp img{ background:transparent url(images/fface.gif?v=1.1) no-repeat scroll left top; cursor:pointer;width:25px;height:25px;display:block; } .ldw img{ background:transparent url(images/wface.gif?v=1.1) no-repeat scroll left top; cursor:pointer;width:35px;height:35px;display:block; } .tsj img{ background:transparent url(images/tface.gif?v=1.1) no-repeat scroll left top; cursor:pointer;width:35px;height:35px;display:block; } .cat img{ background:transparent url(images/cface.gif?v=1.1) no-repeat scroll left top; cursor:pointer;width:35px;height:35px;display:block; } .bb img{ background:transparent url(images/bface.gif?v=1.1) no-repeat scroll left top; cursor:pointer;width:35px;height:35px;display:block; } .youa img{ background:transparent url(images/yface.gif?v=1.1) no-repeat scroll left top; cursor:pointer;width:35px;height:35px;display:block; } .smileytable td {height: 37px;} #tabPanel{margin-left:5px;overflow: hidden;} #tabContent {float:left;background:#FFFFFF;} #tabContent div{display: none;width:480px;overflow:hidden;} #tabIconReview.show{left:17px;display:block;} .menuFocus{background:#ACCD3C;} .menuDefault{background:#FFFFFF;} #tabIconReview{position:absolute;left:406px;left:398px \9;top:41px;z-index:65533;width:90px;height:76px;} img.review{width:90px;height:76px;border:2px solid #9cb945;background:#FFFFFF;background-position:center;background-repeat:no-repeat;} .wrapper .tabbody{position:relative;float:left;clear:both;padding:10px;width: 95%;} .tabbody table{width: 100%;} .tabbody td{border:1px solid #BAC498;} .tabbody td span{display: block;zoom:1;padding:0 4px;} ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/emotion/emotion.html ================================================
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/emotion/emotion.js ================================================ window.onload = function () { editor.setOpt({ emotionLocalization:false }); emotion.SmileyPath = editor.options.emotionLocalization === true ? 'images/' : "http://img.baidu.com/hi/"; emotion.SmileyBox = createTabList( emotion.tabNum ); emotion.tabExist = createArr( emotion.tabNum ); initImgName(); initEvtHandler( "tabHeads" ); }; function initImgName() { for ( var pro in emotion.SmilmgName ) { var tempName = emotion.SmilmgName[pro], tempBox = emotion.SmileyBox[pro], tempStr = ""; if ( tempBox.length ) return; for ( var i = 1; i <= tempName[1]; i++ ) { tempStr = tempName[0]; if ( i < 10 ) tempStr = tempStr + '0'; tempStr = tempStr + i + '.gif'; tempBox.push( tempStr ); } } } function initEvtHandler( conId ) { var tabHeads = $G( conId ); for ( var i = 0, j = 0; i < tabHeads.childNodes.length; i++ ) { var tabObj = tabHeads.childNodes[i]; if ( tabObj.nodeType == 1 ) { domUtils.on( tabObj, "click", (function ( index ) { return function () { switchTab( index ); }; })( j ) ); j++; } } switchTab( 0 ); $G( "tabIconReview" ).style.display = 'none'; } function InsertSmiley( url, evt ) { var obj = { src:editor.options.emotionLocalization ? editor.options.UEDITOR_HOME_URL + "dialogs/emotion/" + url : url }; obj._src = obj.src; editor.execCommand( 'insertimage', obj ); if ( !evt.ctrlKey ) { dialog.popup.hide(); } } function switchTab( index ) { autoHeight( index ); if ( emotion.tabExist[index] == 0 ) { emotion.tabExist[index] = 1; createTab( 'tab' + index ); } //获取呈现元素句柄数组 var tabHeads = $G( "tabHeads" ).getElementsByTagName( "span" ), tabBodys = $G( "tabBodys" ).getElementsByTagName( "div" ), i = 0, L = tabHeads.length; //隐藏所有呈现元素 for ( ; i < L; i++ ) { tabHeads[i].className = ""; tabBodys[i].style.display = "none"; } //显示对应呈现元素 tabHeads[index].className = "focus"; tabBodys[index].style.display = "block"; } function autoHeight( index ) { var iframe = dialog.getDom( "iframe" ), parent = iframe.parentNode.parentNode; switch ( index ) { case 0: iframe.style.height = "380px"; parent.style.height = "392px"; break; case 1: iframe.style.height = "220px"; parent.style.height = "232px"; break; case 2: iframe.style.height = "260px"; parent.style.height = "272px"; break; case 3: iframe.style.height = "300px"; parent.style.height = "312px"; break; case 4: iframe.style.height = "140px"; parent.style.height = "152px"; break; case 5: iframe.style.height = "260px"; parent.style.height = "272px"; break; case 6: iframe.style.height = "230px"; parent.style.height = "242px"; break; default: } } function createTab( tabName ) { var faceVersion = "?v=1.1", //版本号 tab = $G( tabName ), //获取将要生成的Div句柄 imagePath = emotion.SmileyPath + emotion.imageFolders[tabName], //获取显示表情和预览表情的路径 positionLine = 11 / 2, //中间数 iWidth = iHeight = 35, //图片长宽 iColWidth = 3, //表格剩余空间的显示比例 tableCss = emotion.imageCss[tabName], cssOffset = emotion.imageCssOffset[tabName], textHTML = [''], i = 0, imgNum = emotion.SmileyBox[tabName].length, imgColNum = 11, faceImage, sUrl, realUrl, posflag, offset, infor; for ( ; i < imgNum; ) { textHTML.push( '' ); for ( var j = 0; j < imgColNum; j++, i++ ) { faceImage = emotion.SmileyBox[tabName][i]; if ( faceImage ) { sUrl = imagePath + faceImage + faceVersion; realUrl = imagePath + faceImage; posflag = j < positionLine ? 0 : 1; offset = cssOffset * i * (-1) - 1; infor = emotion.SmileyInfor[tabName][i]; textHTML.push( '' ); } textHTML.push( '' ); } textHTML.push( '
    ' ); textHTML.push( '' ); textHTML.push( '' ); textHTML.push( '' ); } else { textHTML.push( '' ); } textHTML.push( '
    ' ); textHTML = textHTML.join( "" ); tab.innerHTML = textHTML; } function over( td, srcPath, posFlag ) { td.style.backgroundColor = "#ACCD3C"; $G( 'faceReview' ).style.backgroundImage = "url(" + srcPath + ")"; if ( posFlag == 1 ) $G( "tabIconReview" ).className = "show"; $G( "tabIconReview" ).style.display = 'block'; } function out( td ) { td.style.backgroundColor = "transparent"; var tabIconRevew = $G( "tabIconReview" ); tabIconRevew.className = ""; tabIconRevew.style.display = 'none'; } function createTabList( tabNum ) { var obj = {}; for ( var i = 0; i < tabNum; i++ ) { obj["tab" + i] = []; } return obj; } function createArr( tabNum ) { var arr = []; for ( var i = 0; i < tabNum; i++ ) { arr[i] = 0; } return arr; } ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/gmap/gmap.html ================================================
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/help/help.css ================================================ .wrapper{width: 370px;margin: 10px auto;zoom: 1;} .tabbody{height: 360px;} .tabbody .panel{width:100%;height: 360px;position: absolute;background: #fff;} .tabbody .panel h1{font-size:26px;margin: 5px 0 0 5px;} .tabbody .panel p{font-size:12px;margin: 5px 0 0 5px;} .tabbody table{width:90%;line-height: 20px;margin: 5px 0 0 5px;;} .tabbody table thead{font-weight: bold;line-height: 25px;} ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/help/help.html ================================================ 帮助

    UEditor

    ctrl+b
    ctrl+c
    ctrl+x
    ctrl+v
    ctrl+y
    ctrl+z
    ctrl+i
    ctrl+u
    ctrl+a
    shift+enter
    alt+z
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/help/help.js ================================================ /** * Created with JetBrains PhpStorm. * User: xuheng * Date: 12-9-26 * Time: 下午1:06 * To change this template use File | Settings | File Templates. */ /** * tab点击处理事件 * @param tabHeads * @param tabBodys * @param obj */ function clickHandler( tabHeads,tabBodys,obj ) { //head样式更改 for ( var k = 0, len = tabHeads.length; k < len; k++ ) { tabHeads[k].className = ""; } obj.className = "focus"; //body显隐 var tabSrc = obj.getAttribute( "tabSrc" ); for ( var j = 0, length = tabBodys.length; j < length; j++ ) { var body = tabBodys[j], id = body.getAttribute( "id" ); body.onclick = function(){ this.style.zoom = 1; }; if ( id != tabSrc ) { body.style.zIndex = 1; } else { body.style.zIndex = 200; } } } /** * TAB切换 * @param tabParentId tab的父节点ID或者对象本身 */ function switchTab( tabParentId ) { var tabElements = $G( tabParentId ).children, tabHeads = tabElements[0].children, tabBodys = tabElements[1].children; for ( var i = 0, length = tabHeads.length; i < length; i++ ) { var head = tabHeads[i]; if ( head.className === "focus" )clickHandler(tabHeads,tabBodys, head ); head.onclick = function () { clickHandler(tabHeads,tabBodys,this); } } } switchTab("helptab"); document.getElementById('version').innerHTML = parent.UE.version; ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/image/image.css ================================================ @charset "utf-8"; /* dialog样式 */ .wrapper { zoom: 1; width: 630px; *width: 626px; height: 380px; margin: 0 auto; padding: 10px; position: relative; font-family: sans-serif; } /*tab样式框大小*/ .tabhead { float:left; } .tabbody { width: 100%; height: 346px; position: relative; clear: both; } .tabbody .panel { position: absolute; width: 0; height: 0; background: #fff; overflow: hidden; display: none; } .tabbody .panel.focus { width: 100%; height: 346px; display: block; } /* 图片对齐方式 */ .alignBar{ float:right; margin-top: 5px; position: relative; } .alignBar .algnLabel{ float:left; height: 20px; line-height: 20px; } .alignBar #alignIcon{ zoom:1; _display: inline; display: inline-block; position: relative; } .alignBar #alignIcon span{ float: left; cursor: pointer; display: block; width: 19px; height: 17px; margin-right: 3px; margin-left: 3px; background-image: url(./images/alignicon.jpg); } .alignBar #alignIcon .none-align{ background-position: 0 -18px; } .alignBar #alignIcon .left-align{ background-position: -20px -18px; } .alignBar #alignIcon .right-align{ background-position: -40px -18px; } .alignBar #alignIcon .center-align{ background-position: -60px -18px; } .alignBar #alignIcon .none-align.focus{ background-position: 0 0; } .alignBar #alignIcon .left-align.focus{ background-position: -20px 0; } .alignBar #alignIcon .right-align.focus{ background-position: -40px 0; } .alignBar #alignIcon .center-align.focus{ background-position: -60px 0; } /* 远程图片样式 */ #remote { z-index: 200; } #remote .top{ width: 100%; margin-top: 25px; } #remote .left{ display: block; float: left; width: 300px; height:10px; } #remote .right{ display: block; float: right; width: 300px; height:10px; } #remote .row{ margin-left: 20px; clear: both; height: 40px; } #remote .row label{ text-align: center; width: 50px; zoom:1; _display: inline; display:inline-block; vertical-align: middle; } #remote .row label.algnLabel{ float: left; } #remote input.text{ width: 150px; padding: 3px 6px; font-size: 14px; line-height: 1.42857143; color: #555; background-color: #fff; background-image: none; border: 1px solid #ccc; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; } #remote input.text:focus { border-color: #66afe9; outline: 0; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6); } #remote #url{ width: 500px; margin-bottom: 2px; } #remote #width, #remote #height{ width: 20px; margin-left: 2px; margin-right: 2px; } #remote #border, #remote #vhSpace, #remote #title{ width: 180px; margin-right: 5px; } #remote #lock{ } #remote #lockicon{ zoom: 1; _display:inline; display: inline-block; width: 20px; height: 20px; background: url("../../themes/default/images/lock.gif") -13px -13px no-repeat; vertical-align: middle; } #remote #preview{ clear: both; width: 260px; height: 240px; z-index: 9999; margin-top: 10px; background-color: #eee; overflow: hidden; } /* 上传图片 */ .tabbody #upload.panel { width: 0; height: 0; overflow: hidden; position: absolute !important; clip: rect(1px, 1px, 1px, 1px); background: #fff; display: block; } .tabbody #upload.panel.focus { width: 100%; height: 346px; display: block; clip: auto; } #upload .queueList { margin: 0; width: 100%; height: 100%; position: absolute; overflow: hidden; } #upload p { margin: 0; } .element-invisible { width: 0 !important; height: 0 !important; border: 0; padding: 0; margin: 0; overflow: hidden; position: absolute !important; clip: rect(1px, 1px, 1px, 1px); } #upload .placeholder { margin: 10px; border: 2px dashed #e6e6e6; *border: 0px dashed #e6e6e6; height: 172px; padding-top: 150px; text-align: center; background: url(./images/image.png) center 70px no-repeat; color: #cccccc; font-size: 18px; position: relative; top:0; *top: 10px; } #upload .placeholder .webuploader-pick { font-size: 18px; background: #00b7ee; border-radius: 3px; line-height: 44px; padding: 0 30px; *width: 120px; color: #fff; display: inline-block; margin: 0 auto 20px auto; cursor: pointer; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); } #upload .placeholder .webuploader-pick-hover { background: #00a2d4; } #filePickerContainer { text-align: center; } #upload .placeholder .flashTip { color: #666666; font-size: 12px; position: absolute; width: 100%; text-align: center; bottom: 20px; } #upload .placeholder .flashTip a { color: #0785d1; text-decoration: none; } #upload .placeholder .flashTip a:hover { text-decoration: underline; } #upload .placeholder.webuploader-dnd-over { border-color: #999999; } #upload .filelist { list-style: none; margin: 0; padding: 0; overflow-x: hidden; overflow-y: auto; position: relative; height: 300px; } #upload .filelist:after { content: ''; display: block; width: 0; height: 0; overflow: hidden; clear: both; position: relative; } #upload .filelist li { width: 113px; height: 113px; background: url(./images/bg.png); text-align: center; margin: 9px 0 0 9px; *margin: 6px 0 0 6px; position: relative; display: block; float: left; overflow: hidden; font-size: 12px; } #upload .filelist li p.log { position: relative; top: -45px; } #upload .filelist li p.title { position: absolute; top: 0; left: 0; width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; top: 5px; text-indent: 5px; text-align: left; } #upload .filelist li p.progress { position: absolute; width: 100%; bottom: 0; left: 0; height: 8px; overflow: hidden; z-index: 50; margin: 0; border-radius: 0; background: none; -webkit-box-shadow: 0 0 0; } #upload .filelist li p.progress span { display: none; overflow: hidden; width: 0; height: 100%; background: #1483d8 url(./images/progress.png) repeat-x; -webit-transition: width 200ms linear; -moz-transition: width 200ms linear; -o-transition: width 200ms linear; -ms-transition: width 200ms linear; transition: width 200ms linear; -webkit-animation: progressmove 2s linear infinite; -moz-animation: progressmove 2s linear infinite; -o-animation: progressmove 2s linear infinite; -ms-animation: progressmove 2s linear infinite; animation: progressmove 2s linear infinite; -webkit-transform: translateZ(0); } @-webkit-keyframes progressmove { 0% { background-position: 0 0; } 100% { background-position: 17px 0; } } @-moz-keyframes progressmove { 0% { background-position: 0 0; } 100% { background-position: 17px 0; } } @keyframes progressmove { 0% { background-position: 0 0; } 100% { background-position: 17px 0; } } #upload .filelist li p.imgWrap { position: relative; z-index: 2; line-height: 113px; vertical-align: middle; overflow: hidden; width: 113px; height: 113px; -webkit-transform-origin: 50% 50%; -moz-transform-origin: 50% 50%; -o-transform-origin: 50% 50%; -ms-transform-origin: 50% 50%; transform-origin: 50% 50%; -webit-transition: 200ms ease-out; -moz-transition: 200ms ease-out; -o-transition: 200ms ease-out; -ms-transition: 200ms ease-out; transition: 200ms ease-out; } #upload .filelist li img { width: 100%; } #upload .filelist li p.error { background: #f43838; color: #fff; position: absolute; bottom: 0; left: 0; height: 28px; line-height: 28px; width: 100%; z-index: 100; display:none; } #upload .filelist li .success { display: block; position: absolute; left: 0; bottom: 0; height: 40px; width: 100%; z-index: 200; background: url(./images/success.png) no-repeat right bottom; background: url(./images/success.gif) no-repeat right bottom \9; } #upload .filelist li.filePickerBlock { width: 113px; height: 113px; background: url(./images/image.png) no-repeat center 12px; border: 1px solid #eeeeee; border-radius: 0; } #upload .filelist li.filePickerBlock div.webuploader-pick { width: 100%; height: 100%; margin: 0; padding: 0; opacity: 0; background: none; font-size: 0; } #upload .filelist div.file-panel { position: absolute; height: 0; filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#80000000', endColorstr='#80000000') \0; background: rgba(0, 0, 0, 0.5); width: 100%; top: 0; left: 0; overflow: hidden; z-index: 300; } #upload .filelist div.file-panel span { width: 24px; height: 24px; display: inline; float: right; text-indent: -9999px; overflow: hidden; background: url(./images/icons.png) no-repeat; background: url(./images/icons.gif) no-repeat \9; margin: 5px 1px 1px; cursor: pointer; -webkit-tap-highlight-color: rgba(0,0,0,0); -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } #upload .filelist div.file-panel span.rotateLeft { display:none; background-position: 0 -24px; } #upload .filelist div.file-panel span.rotateLeft:hover { background-position: 0 0; } #upload .filelist div.file-panel span.rotateRight { display:none; background-position: -24px -24px; } #upload .filelist div.file-panel span.rotateRight:hover { background-position: -24px 0; } #upload .filelist div.file-panel span.cancel { background-position: -48px -24px; } #upload .filelist div.file-panel span.cancel:hover { background-position: -48px 0; } #upload .statusBar { height: 45px; border-bottom: 1px solid #dadada; margin: 0 10px; padding: 0; line-height: 45px; vertical-align: middle; position: relative; } #upload .statusBar .progress { border: 1px solid #1483d8; width: 198px; background: #fff; height: 18px; position: absolute; top: 12px; display: none; text-align: center; line-height: 18px; color: #6dbfff; margin: 0 10px 0 0; } #upload .statusBar .progress span.percentage { width: 0; height: 100%; left: 0; top: 0; background: #1483d8; position: absolute; } #upload .statusBar .progress span.text { position: relative; z-index: 10; } #upload .statusBar .info { display: inline-block; font-size: 14px; color: #666666; } #upload .statusBar .btns { position: absolute; top: 7px; right: 0; line-height: 30px; } #filePickerBtn { display: inline-block; float: left; } #upload .statusBar .btns .webuploader-pick, #upload .statusBar .btns .uploadBtn, #upload .statusBar .btns .uploadBtn.state-uploading, #upload .statusBar .btns .uploadBtn.state-paused { background: #ffffff; border: 1px solid #cfcfcf; color: #565656; padding: 0 18px; display: inline-block; border-radius: 3px; margin-left: 10px; cursor: pointer; font-size: 14px; float: left; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } #upload .statusBar .btns .webuploader-pick-hover, #upload .statusBar .btns .uploadBtn:hover, #upload .statusBar .btns .uploadBtn.state-uploading:hover, #upload .statusBar .btns .uploadBtn.state-paused:hover { background: #f0f0f0; } #upload .statusBar .btns .uploadBtn, #upload .statusBar .btns .uploadBtn.state-paused{ background: #00b7ee; color: #fff; border-color: transparent; } #upload .statusBar .btns .uploadBtn:hover, #upload .statusBar .btns .uploadBtn.state-paused:hover{ background: #00a2d4; } #upload .statusBar .btns .uploadBtn.disabled { pointer-events: none; filter:alpha(opacity=60); -moz-opacity:0.6; -khtml-opacity: 0.6; opacity: 0.6; } /* 图片管理样式 */ #online { width: 100%; height: 336px; padding: 10px 0 0 0; } #online #imageList{ width: 100%; height: 100%; overflow-x: hidden; overflow-y: auto; position: relative; } #online ul { display: block; list-style: none; margin: 0; padding: 0; } #online li { float: left; display: block; list-style: none; padding: 0; width: 113px; height: 113px; margin: 0 0 9px 9px; *margin: 0 0 6px 6px; background-color: #eee; overflow: hidden; cursor: pointer; position: relative; } #online li.clearFloat { float: none; clear: both; display: block; width:0; height:0; margin: 0; padding: 0; } #online li img { cursor: pointer; } #online li .icon { cursor: pointer; width: 113px; height: 113px; position: absolute; top: 0; left: 0; z-index: 2; border: 0; background-repeat: no-repeat; } #online li .icon:hover { width: 107px; height: 107px; border: 3px solid #1094fa; } #online li.selected .icon { background-image: url(images/success.png); background-image: url(images/success.gif)\9; background-position: 75px 75px; } #online li.selected .icon:hover { width: 107px; height: 107px; border: 3px solid #1094fa; background-position: 72px 72px; } /* 图片搜索样式 */ #search .searchBar { width: 100%; height: 30px; margin: 10px 0 5px 0; padding: 0; } #search input.text{ width: 150px; padding: 3px 6px; font-size: 14px; line-height: 1.42857143; color: #555; background-color: #fff; background-image: none; border: 1px solid #ccc; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; } #search input.text:focus { border-color: #66afe9; outline: 0; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6); } #search input.searchTxt { margin-left:5px; padding-left: 5px; background: #FFF; width: 300px; *width: 260px; height: 21px; line-height: 21px; float: left; dislay: block; } #search .searchType { width: 65px; height: 28px; padding:0; line-height: 28px; border: 1px solid #d7d7d7; border-radius: 0; vertical-align: top; margin-left: 5px; float: left; dislay: block; } #search #searchBtn, #search #searchReset { display: inline-block; margin-bottom: 0; margin-right: 5px; padding: 4px 10px; font-weight: 400; text-align: center; vertical-align: middle; cursor: pointer; background-image: none; border: 1px solid transparent; white-space: nowrap; font-size: 14px; border-radius: 4px; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; vertical-align: top; float: right; } #search #searchBtn { color: white; border-color: #285e8e; background-color: #3b97d7; } #search #searchReset { color: #333; border-color: #ccc; background-color: #fff; } #search #searchBtn:hover { background-color: #3276b1; } #search #searchReset:hover { background-color: #eee; } #search .msg { margin-left: 5px; } #search .searchList{ width: 100%; height: 300px; overflow: hidden; clear: both; } #search .searchList ul{ margin:0; padding:0; list-style:none; clear: both; width: 100%; height: 100%; overflow-x: hidden; overflow-y: auto; zoom: 1; position: relative; } #search .searchList li { list-style:none; float: left; display: block; width: 115px; margin: 5px 10px 5px 20px; *margin: 5px 10px 5px 15px; padding:0; font-size: 12px; box-shadow: 0 1px 3px rgba(0, 0, 0, .3); -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, .3); -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, .3); position: relative; vertical-align: top; text-align: center; overflow: hidden; cursor: pointer; filter: alpha(Opacity=100); -moz-opacity: 1; opacity: 1; border: 2px solid #eee; } #search .searchList li.selected { filter: alpha(Opacity=40); -moz-opacity: 0.4; opacity: 0.4; border: 2px solid #00a0e9; } #search .searchList li p { background-color: #eee; margin: 0; padding: 0; position: relative; width:100%; height:115px; overflow: hidden; } #search .searchList li p img { cursor: pointer; border: 0; } #search .searchList li a { color: #999; border-top: 1px solid #F2F2F2; background: #FAFAFA; text-align: center; display: block; padding: 0 5px; width: 105px; height:32px; line-height:32px; white-space:nowrap; text-overflow:ellipsis; text-decoration: none; overflow: hidden; word-break: break-all; } #search .searchList a:hover { text-decoration: underline; color: #333; } #search .searchList .clearFloat{ clear: both; } ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/image/image.html ================================================ ueditor图片对话框
      px   px
    px
    px
    0%
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/image/image.js ================================================ /** * User: Jinqn * Date: 14-04-08 * Time: 下午16:34 * 上传图片对话框逻辑代码,包括tab: 远程图片/上传图片/在线图片/搜索图片 */ (function () { var remoteImage, uploadImage, onlineImage, searchImage; window.onload = function () { initTabs(); initAlign(); initButtons(); }; /* 初始化tab标签 */ function initTabs() { var tabs = $G('tabhead').children; for (var i = 0; i < tabs.length; i++) { domUtils.on(tabs[i], "click", function (e) { var target = e.target || e.srcElement; setTabFocus(target.getAttribute('data-content-id')); }); } var img = editor.selection.getRange().getClosedNode(); if (img && img.tagName && img.tagName.toLowerCase() == 'img') { setTabFocus('remote'); } else { setTabFocus('upload'); } } /* 初始化tabbody */ function setTabFocus(id) { if(!id) return; var i, bodyId, tabs = $G('tabhead').children; for (i = 0; i < tabs.length; i++) { bodyId = tabs[i].getAttribute('data-content-id'); if (bodyId == id) { domUtils.addClass(tabs[i], 'focus'); domUtils.addClass($G(bodyId), 'focus'); } else { domUtils.removeClasses(tabs[i], 'focus'); domUtils.removeClasses($G(bodyId), 'focus'); } } switch (id) { case 'remote': remoteImage = remoteImage || new RemoteImage(); break; case 'upload': setAlign(editor.getOpt('imageInsertAlign')); uploadImage = uploadImage || new UploadImage('queueList'); break; case 'online': setAlign(editor.getOpt('imageManagerInsertAlign')); onlineImage = onlineImage || new OnlineImage('imageList'); onlineImage.reset(); break; case 'search': setAlign(editor.getOpt('imageManagerInsertAlign')); searchImage = searchImage || new SearchImage(); break; } } /* 初始化onok事件 */ function initButtons() { dialog.onok = function () { var remote = false, list = [], id, tabs = $G('tabhead').children; for (var i = 0; i < tabs.length; i++) { if (domUtils.hasClass(tabs[i], 'focus')) { id = tabs[i].getAttribute('data-content-id'); break; } } switch (id) { case 'remote': list = remoteImage.getInsertList(); break; case 'upload': list = uploadImage.getInsertList(); var count = uploadImage.getQueueCount(); if (count) { $('.info', '#queueList').html('' + '还有2个未上传文件'.replace(/[\d]/, count) + ''); return false; } break; case 'online': list = onlineImage.getInsertList(); break; case 'search': list = searchImage.getInsertList(); remote = true; break; } if(list) { editor.execCommand('insertimage', list); remote && editor.fireEvent("catchRemoteImage"); } }; } /* 初始化对其方式的点击事件 */ function initAlign(){ /* 点击align图标 */ domUtils.on($G("alignIcon"), 'click', function(e){ var target = e.target || e.srcElement; if(target.className && target.className.indexOf('-align') != -1) { setAlign(target.getAttribute('data-align')); } }); } /* 设置对齐方式 */ function setAlign(align){ align = align || 'none'; var aligns = $G("alignIcon").children; for(i = 0; i < aligns.length; i++){ if(aligns[i].getAttribute('data-align') == align) { domUtils.addClass(aligns[i], 'focus'); $G("align").value = aligns[i].getAttribute('data-align'); } else { domUtils.removeClasses(aligns[i], 'focus'); } } } /* 获取对齐方式 */ function getAlign(){ var align = $G("align").value || 'none'; return align == 'none' ? '':align; } /* 在线图片 */ function RemoteImage(target) { this.container = utils.isString(target) ? document.getElementById(target) : target; this.init(); } RemoteImage.prototype = { init: function () { this.initContainer(); this.initEvents(); }, initContainer: function () { this.dom = { 'url': $G('url'), 'width': $G('width'), 'height': $G('height'), 'border': $G('border'), 'vhSpace': $G('vhSpace'), 'title': $G('title'), 'align': $G('align') }; var img = editor.selection.getRange().getClosedNode(); if (img) { this.setImage(img); } }, initEvents: function () { var _this = this, locker = $G('lock'); /* 改变url */ domUtils.on($G("url"), 'keyup', updatePreview); domUtils.on($G("border"), 'keyup', updatePreview); domUtils.on($G("title"), 'keyup', updatePreview); domUtils.on($G("width"), 'keyup', function(){ updatePreview(); if(locker.checked) { var proportion =locker.getAttribute('data-proportion'); $G('height').value = Math.round(this.value / proportion); } else { _this.updateLocker(); } }); domUtils.on($G("height"), 'keyup', function(){ updatePreview(); if(locker.checked) { var proportion =locker.getAttribute('data-proportion'); $G('width').value = Math.round(this.value * proportion); } else { _this.updateLocker(); } }); domUtils.on($G("lock"), 'change', function(){ var proportion = parseInt($G("width").value) /parseInt($G("height").value); locker.setAttribute('data-proportion', proportion); }); function updatePreview(){ _this.setPreview(); } }, updateLocker: function(){ var width = $G('width').value, height = $G('height').value, locker = $G('lock'); if(width && height && width == parseInt(width) && height == parseInt(height)) { locker.disabled = false; locker.title = ''; } else { locker.checked = false; locker.disabled = 'disabled'; locker.title = lang.remoteLockError; } }, setImage: function(img){ /* 不是正常的图片 */ if (!img.tagName || img.tagName.toLowerCase() != 'img' && !img.getAttribute("src") || !img.src) return; var wordImgFlag = img.getAttribute("word_img"), src = wordImgFlag ? wordImgFlag.replace("&", "&") : (img.getAttribute('_src') || img.getAttribute("src", 2).replace("&", "&")), align = editor.queryCommandValue("imageFloat"); /* 防止onchange事件循环调用 */ if (src !== $G("url").value) $G("url").value = src; if(src) { /* 设置表单内容 */ $G("width").value = img.width || ''; $G("height").value = img.height || ''; $G("border").value = img.getAttribute("border") || '0'; $G("vhSpace").value = img.getAttribute("vspace") || '0'; $G("title").value = img.title || img.alt || ''; setAlign(align); this.setPreview(); this.updateLocker(); } }, getData: function(){ var data = {}; for(var k in this.dom){ data[k] = this.dom[k].value; } return data; }, setPreview: function(){ var url = $G('url').value, ow = parseInt($G('width').value, 10) || 0, oh = parseInt($G('height').value, 10) || 0, border = parseInt($G('border').value, 10) || 0, title = $G('title').value, preview = $G('preview'), width, height; url = utils.unhtmlForUrl(url); title = utils.unhtml(title); width = ((!ow || !oh) ? preview.offsetWidth:Math.min(ow, preview.offsetWidth)); width = width+(border*2) > preview.offsetWidth ? width:(preview.offsetWidth - (border*2)); height = (!ow || !oh) ? '':width*oh/ow; if(url) { preview.innerHTML = ''; } }, getInsertList: function () { var data = this.getData(); if(data['url']) { return [{ src: data['url'], _src: data['url'], width: data['width'] || '', height: data['height'] || '', border: data['border'] || '', floatStyle: data['align'] || '', vspace: data['vhSpace'] || '', title: data['title'] || '', alt: data['title'] || '', style: "width:" + data['width'] + "px;height:" + data['height'] + "px;" }]; } else { return []; } } }; /* 上传图片 */ function UploadImage(target) { this.$wrap = target.constructor == String ? $('#' + target) : $(target); this.init(); } UploadImage.prototype = { init: function () { this.imageList = []; this.initContainer(); this.initUploader(); }, initContainer: function () { this.$queue = this.$wrap.find('.filelist'); }, /* 初始化容器 */ initUploader: function () { var _this = this, $ = jQuery, // just in case. Make sure it's not an other libaray. $wrap = _this.$wrap, // 图片容器 $queue = $wrap.find('.filelist'), // 状态栏,包括进度和控制按钮 $statusBar = $wrap.find('.statusBar'), // 文件总体选择信息。 $info = $statusBar.find('.info'), // 上传按钮 $upload = $wrap.find('.uploadBtn'), // 上传按钮 $filePickerBtn = $wrap.find('.filePickerBtn'), // 上传按钮 $filePickerBlock = $wrap.find('.filePickerBlock'), // 没选择文件之前的内容。 $placeHolder = $wrap.find('.placeholder'), // 总体进度条 $progress = $statusBar.find('.progress').hide(), // 添加的文件数量 fileCount = 0, // 添加的文件总大小 fileSize = 0, // 优化retina, 在retina下这个值是2 ratio = window.devicePixelRatio || 1, // 缩略图大小 thumbnailWidth = 113 * ratio, thumbnailHeight = 113 * ratio, // 可能有pedding, ready, uploading, confirm, done. state = '', // 所有文件的进度信息,key为file id percentages = {}, supportTransition = (function () { var s = document.createElement('p').style, r = 'transition' in s || 'WebkitTransition' in s || 'MozTransition' in s || 'msTransition' in s || 'OTransition' in s; s = null; return r; })(), // WebUploader实例 uploader, actionUrl = editor.getActionUrl(editor.getOpt('imageActionName')), acceptExtensions = (editor.getOpt('imageAllowFiles') || []).join('').replace(/\./g, ',').replace(/^[,]/, ''), imageMaxSize = editor.getOpt('imageMaxSize'), imageCompressBorder = editor.getOpt('imageCompressBorder'); if (!WebUploader.Uploader.support()) { $('#filePickerReady').after($('
    ').html(lang.errorNotSupport)).hide(); return; } else if (!editor.getOpt('imageActionName')) { $('#filePickerReady').after($('
    ').html(lang.errorLoadConfig)).hide(); return; } uploader = _this.uploader = WebUploader.create({ pick: { id: '#filePickerReady', label: lang.uploadSelectFile }, accept: { title: 'Images', extensions: acceptExtensions, mimeTypes: 'image/*' }, swf: '../../third-party/webuploader/Uploader.swf', server: actionUrl, fileVal: editor.getOpt('imageFieldName'), duplicate: true, fileSingleSizeLimit: imageMaxSize, // 默认 2 M compress: editor.getOpt('imageCompressEnable') ? { width: imageCompressBorder, height: imageCompressBorder, // 图片质量,只有type为`image/jpeg`的时候才有效。 quality: 90, // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. allowMagnify: false, // 是否允许裁剪。 crop: false, // 是否保留头部meta信息。 preserveHeaders: true }:false }); uploader.addButton({ id: '#filePickerBlock' }); uploader.addButton({ id: '#filePickerBtn', label: lang.uploadAddFile }); setState('pedding'); // 当有文件添加进来时执行,负责view的创建 function addFile(file) { var $li = $('
  • ' + '

    ' + file.name + '

    ' + '

    ' + '

    ' + '
  • '), $btns = $('
    ' + '' + lang.uploadDelete + '' + '' + lang.uploadTurnRight + '' + '' + lang.uploadTurnLeft + '
    ').appendTo($li), $prgress = $li.find('p.progress span'), $wrap = $li.find('p.imgWrap'), $info = $('

    ').hide().appendTo($li), showError = function (code) { switch (code) { case 'exceed_size': text = lang.errorExceedSize; break; case 'interrupt': text = lang.errorInterrupt; break; case 'http': text = lang.errorHttp; break; case 'not_allow_type': text = lang.errorFileType; break; default: text = lang.errorUploadRetry; break; } $info.text(text).show(); }; if (file.getStatus() === 'invalid') { showError(file.statusText); } else { $wrap.text(lang.uploadPreview); if (browser.ie && browser.version <= 7) { $wrap.text(lang.uploadNoPreview); } else { uploader.makeThumb(file, function (error, src) { if (error || !src) { $wrap.text(lang.uploadNoPreview); } else { var $img = $(''); $wrap.empty().append($img); $img.on('error', function () { $wrap.text(lang.uploadNoPreview); }); } }, thumbnailWidth, thumbnailHeight); } percentages[ file.id ] = [ file.size, 0 ]; file.rotation = 0; /* 检查文件格式 */ if (!file.ext || acceptExtensions.indexOf(file.ext.toLowerCase()) == -1) { showError('not_allow_type'); uploader.removeFile(file); } } file.on('statuschange', function (cur, prev) { if (prev === 'progress') { $prgress.hide().width(0); } else if (prev === 'queued') { $li.off('mouseenter mouseleave'); $btns.remove(); } // 成功 if (cur === 'error' || cur === 'invalid') { showError(file.statusText); percentages[ file.id ][ 1 ] = 1; } else if (cur === 'interrupt') { showError('interrupt'); } else if (cur === 'queued') { percentages[ file.id ][ 1 ] = 0; } else if (cur === 'progress') { $info.hide(); $prgress.css('display', 'block'); } else if (cur === 'complete') { } $li.removeClass('state-' + prev).addClass('state-' + cur); }); $li.on('mouseenter', function () { $btns.stop().animate({height: 30}); }); $li.on('mouseleave', function () { $btns.stop().animate({height: 0}); }); $btns.on('click', 'span', function () { var index = $(this).index(), deg; switch (index) { case 0: uploader.removeFile(file); return; case 1: file.rotation += 90; break; case 2: file.rotation -= 90; break; } if (supportTransition) { deg = 'rotate(' + file.rotation + 'deg)'; $wrap.css({ '-webkit-transform': deg, '-mos-transform': deg, '-o-transform': deg, 'transform': deg }); } else { $wrap.css('filter', 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' + (~~((file.rotation / 90) % 4 + 4) % 4) + ')'); } }); $li.insertBefore($filePickerBlock); } // 负责view的销毁 function removeFile(file) { var $li = $('#' + file.id); delete percentages[ file.id ]; updateTotalProgress(); $li.off().find('.file-panel').off().end().remove(); } function updateTotalProgress() { var loaded = 0, total = 0, spans = $progress.children(), percent; $.each(percentages, function (k, v) { total += v[ 0 ]; loaded += v[ 0 ] * v[ 1 ]; }); percent = total ? loaded / total : 0; spans.eq(0).text(Math.round(percent * 100) + '%'); spans.eq(1).css('width', Math.round(percent * 100) + '%'); updateStatus(); } function setState(val, files) { if (val != state) { var stats = uploader.getStats(); $upload.removeClass('state-' + state); $upload.addClass('state-' + val); switch (val) { /* 未选择文件 */ case 'pedding': $queue.addClass('element-invisible'); $statusBar.addClass('element-invisible'); $placeHolder.removeClass('element-invisible'); $progress.hide(); $info.hide(); uploader.refresh(); break; /* 可以开始上传 */ case 'ready': $placeHolder.addClass('element-invisible'); $queue.removeClass('element-invisible'); $statusBar.removeClass('element-invisible'); $progress.hide(); $info.show(); $upload.text(lang.uploadStart); uploader.refresh(); break; /* 上传中 */ case 'uploading': $progress.show(); $info.hide(); $upload.text(lang.uploadPause); break; /* 暂停上传 */ case 'paused': $progress.show(); $info.hide(); $upload.text(lang.uploadContinue); break; case 'confirm': $progress.show(); $info.hide(); $upload.text(lang.uploadStart); stats = uploader.getStats(); if (stats.successNum && !stats.uploadFailNum) { setState('finish'); return; } break; case 'finish': $progress.hide(); $info.show(); if (stats.uploadFailNum) { $upload.text(lang.uploadRetry); } else { $upload.text(lang.uploadStart); } break; } state = val; updateStatus(); } if (!_this.getQueueCount()) { $upload.addClass('disabled') } else { $upload.removeClass('disabled') } } function updateStatus() { var text = '', stats; if (state === 'ready') { text = lang.updateStatusReady.replace('_', fileCount).replace('_KB', WebUploader.formatSize(fileSize)); } else if (state === 'confirm') { stats = uploader.getStats(); if (stats.uploadFailNum) { text = lang.updateStatusConfirm.replace('_', stats.successNum).replace('_', stats.successNum); } } else { stats = uploader.getStats(); text = lang.updateStatusFinish.replace('_', fileCount). replace('_KB', WebUploader.formatSize(fileSize)). replace('_', stats.successNum); if (stats.uploadFailNum) { text += lang.updateStatusError.replace('_', stats.uploadFailNum); } } $info.html(text); } uploader.on('fileQueued', function (file) { fileCount++; fileSize += file.size; if (fileCount === 1) { $placeHolder.addClass('element-invisible'); $statusBar.show(); } addFile(file); }); uploader.on('fileDequeued', function (file) { fileCount--; fileSize -= file.size; removeFile(file); updateTotalProgress(); }); uploader.on('filesQueued', function (file) { if (!uploader.isInProgress() && (state == 'pedding' || state == 'finish' || state == 'confirm' || state == 'ready')) { setState('ready'); } updateTotalProgress(); }); uploader.on('all', function (type, files) { switch (type) { case 'uploadFinished': setState('confirm', files); break; case 'startUpload': /* 添加额外的GET参数 */ var params = utils.serializeParam(editor.queryCommandValue('serverparam')) || '', url = utils.formatUrl(actionUrl + (actionUrl.indexOf('?') == -1 ? '?':'&') + 'encode=utf-8&' + params); uploader.option('server', url); setState('uploading', files); break; case 'stopUpload': setState('paused', files); break; } }); uploader.on('uploadBeforeSend', function (file, data, header) { //这里可以通过data对象添加POST参数 header['X_Requested_With'] = 'XMLHttpRequest'; // HaoChuan9421 if(editor.options.headers && Object.prototype.toString.apply(editor.options.headers) === "[object Object]"){ for(var key in editor.options.headers){ header[key] = editor.options.headers[key] } } }); uploader.on('uploadProgress', function (file, percentage) { var $li = $('#' + file.id), $percent = $li.find('.progress span'); $percent.css('width', percentage * 100 + '%'); percentages[ file.id ][ 1 ] = percentage; updateTotalProgress(); }); uploader.on('uploadSuccess', function (file, ret) { var $file = $('#' + file.id); try { var responseText = (ret._raw || ret), json = utils.str2json(responseText); if (json.state == 'SUCCESS') { _this.imageList.push(json); $file.append(''); } else { $file.find('.error').text(json.state).show(); } } catch (e) { $file.find('.error').text(lang.errorServerUpload).show(); } }); uploader.on('uploadError', function (file, code) { }); uploader.on('error', function (code, file) { if (code == 'Q_TYPE_DENIED' || code == 'F_EXCEED_SIZE') { addFile(file); } }); uploader.on('uploadComplete', function (file, ret) { }); $upload.on('click', function () { if ($(this).hasClass('disabled')) { return false; } if (state === 'ready') { uploader.upload(); } else if (state === 'paused') { uploader.upload(); } else if (state === 'uploading') { uploader.stop(); } }); $upload.addClass('state-' + state); updateTotalProgress(); }, getQueueCount: function () { var file, i, status, readyFile = 0, files = this.uploader.getFiles(); for (i = 0; file = files[i++]; ) { status = file.getStatus(); if (status == 'queued' || status == 'uploading' || status == 'progress') readyFile++; } return readyFile; }, destroy: function () { this.$wrap.remove(); }, getInsertList: function () { var i, data, list = [], align = getAlign(), prefix = editor.getOpt('imageUrlPrefix'); for (i = 0; i < this.imageList.length; i++) { data = this.imageList[i]; list.push({ src: prefix + data.url, _src: prefix + data.url, title: data.title, alt: data.original, floatStyle: align }); } return list; } }; /* 在线图片 */ function OnlineImage(target) { this.container = utils.isString(target) ? document.getElementById(target) : target; this.init(); } OnlineImage.prototype = { init: function () { this.reset(); this.initEvents(); }, /* 初始化容器 */ initContainer: function () { this.container.innerHTML = ''; this.list = document.createElement('ul'); this.clearFloat = document.createElement('li'); domUtils.addClass(this.list, 'list'); domUtils.addClass(this.clearFloat, 'clearFloat'); this.list.appendChild(this.clearFloat); this.container.appendChild(this.list); }, /* 初始化滚动事件,滚动到地步自动拉取数据 */ initEvents: function () { var _this = this; /* 滚动拉取图片 */ domUtils.on($G('imageList'), 'scroll', function(e){ var panel = this; if (panel.scrollHeight - (panel.offsetHeight + panel.scrollTop) < 10) { _this.getImageData(); } }); /* 选中图片 */ domUtils.on(this.container, 'click', function (e) { var target = e.target || e.srcElement, li = target.parentNode; if (li.tagName.toLowerCase() == 'li') { if (domUtils.hasClass(li, 'selected')) { domUtils.removeClasses(li, 'selected'); } else { domUtils.addClass(li, 'selected'); } } }); }, /* 初始化第一次的数据 */ initData: function () { /* 拉取数据需要使用的值 */ this.state = 0; this.listSize = editor.getOpt('imageManagerListSize'); this.listIndex = 0; this.listEnd = false; /* 第一次拉取数据 */ this.getImageData(); }, /* 重置界面 */ reset: function() { this.initContainer(); this.initData(); }, /* 向后台拉取图片列表数据 */ getImageData: function () { var _this = this; if(!_this.listEnd && !this.isLoadingData) { this.isLoadingData = true; var url = editor.getActionUrl(editor.getOpt('imageManagerActionName')), isJsonp = utils.isCrossDomainUrl(url); ajax.request(url, { 'timeout': 100000, 'dataType': isJsonp ? 'jsonp':'', 'data': utils.extend({ start: this.listIndex, size: this.listSize }, editor.queryCommandValue('serverparam')), 'method': 'get', 'onsuccess': function (r) { try { var json = isJsonp ? r:eval('(' + r.responseText + ')'); if (json.state == 'SUCCESS') { _this.pushData(json.list); _this.listIndex = parseInt(json.start) + parseInt(json.list.length); if(_this.listIndex >= json.total) { _this.listEnd = true; } _this.isLoadingData = false; } } catch (e) { if(r.responseText.indexOf('ue_separate_ue') != -1) { var list = r.responseText.split(r.responseText); _this.pushData(list); _this.listIndex = parseInt(list.length); _this.listEnd = true; _this.isLoadingData = false; } } }, 'onerror': function () { _this.isLoadingData = false; } }); } }, /* 添加图片到列表界面上 */ pushData: function (list) { var i, item, img, icon, _this = this, urlPrefix = editor.getOpt('imageManagerUrlPrefix'); for (i = 0; i < list.length; i++) { if(list[i] && list[i].url) { item = document.createElement('li'); img = document.createElement('img'); icon = document.createElement('span'); domUtils.on(img, 'load', (function(image){ return function(){ _this.scale(image, image.parentNode.offsetWidth, image.parentNode.offsetHeight); } })(img)); img.width = 113; img.setAttribute('src', urlPrefix + list[i].url + (list[i].url.indexOf('?') == -1 ? '?noCache=':'&noCache=') + (+new Date()).toString(36) ); img.setAttribute('_src', urlPrefix + list[i].url); domUtils.addClass(icon, 'icon'); item.appendChild(img); item.appendChild(icon); this.list.insertBefore(item, this.clearFloat); } } }, /* 改变图片大小 */ scale: function (img, w, h, type) { var ow = img.width, oh = img.height; if (type == 'justify') { if (ow >= oh) { img.width = w; img.height = h * oh / ow; img.style.marginLeft = '-' + parseInt((img.width - w) / 2) + 'px'; } else { img.width = w * ow / oh; img.height = h; img.style.marginTop = '-' + parseInt((img.height - h) / 2) + 'px'; } } else { if (ow >= oh) { img.width = w * ow / oh; img.height = h; img.style.marginLeft = '-' + parseInt((img.width - w) / 2) + 'px'; } else { img.width = w; img.height = h * oh / ow; img.style.marginTop = '-' + parseInt((img.height - h) / 2) + 'px'; } } }, getInsertList: function () { var i, lis = this.list.children, list = [], align = getAlign(); for (i = 0; i < lis.length; i++) { if (domUtils.hasClass(lis[i], 'selected')) { var img = lis[i].firstChild, src = img.getAttribute('_src'); list.push({ src: src, _src: src, alt: src.substr(src.lastIndexOf('/') + 1), floatStyle: align }); } } return list; } }; /*搜索图片 */ function SearchImage() { this.init(); } SearchImage.prototype = { init: function () { this.initEvents(); }, initEvents: function(){ var _this = this; /* 点击搜索按钮 */ domUtils.on($G('searchBtn'), 'click', function(){ var key = $G('searchTxt').value; if(key && key != lang.searchRemind) { _this.getImageData(); } }); /* 点击清除妞 */ domUtils.on($G('searchReset'), 'click', function(){ $G('searchTxt').value = lang.searchRemind; $G('searchListUl').innerHTML = ''; $G('searchType').selectedIndex = 0; }); /* 搜索框聚焦 */ domUtils.on($G('searchTxt'), 'focus', function(){ var key = $G('searchTxt').value; if(key && key == lang.searchRemind) { $G('searchTxt').value = ''; } }); /* 搜索框回车键搜索 */ domUtils.on($G('searchTxt'), 'keydown', function(e){ var keyCode = e.keyCode || e.which; if (keyCode == 13) { $G('searchBtn').click(); } }); /* 选中图片 */ domUtils.on($G('searchList'), 'click', function(e){ var target = e.target || e.srcElement, li = target.parentNode.parentNode; if (li.tagName.toLowerCase() == 'li') { if (domUtils.hasClass(li, 'selected')) { domUtils.removeClasses(li, 'selected'); } else { domUtils.addClass(li, 'selected'); } } }); }, encodeToGb2312:function (str){ if(!str) return ''; var strOut = "", z = 'D2BBB6A18140C6DF814181428143CDF2D5C9C8FDC9CFCFC2D8A2B2BBD3EB8144D8A4B3F38145D7A8C7D2D8A7CAC08146C7F0B1FBD2B5B4D4B6ABCBBFD8A9814781488149B6AA814AC1BDD1CF814BC9A5D8AD814CB8F6D1BEE3DCD6D0814D814EB7E1814FB4AE8150C1D98151D8BC8152CDE8B5A4CEAAD6F78153C0F6BED9D8AF815481558156C4CB8157BEC38158D8B1C3B4D2E58159D6AECEDAD5A7BAF5B7A6C0D6815AC6B9C5D2C7C7815BB9D4815CB3CBD2D2815D815ED8BFBEC5C6F2D2B2CFB0CFE7815F816081618162CAE981638164D8C081658166816781688169816AC2F2C2D2816BC8E9816C816D816E816F817081718172817381748175C7AC8176817781788179817A817B817CC1CB817DD3E8D5F9817ECAC2B6FED8A1D3DABFF78180D4C6BBA5D8C1CEE5BEAE81818182D8A88183D1C7D0A9818481858186D8BDD9EFCDF6BFBA8187BDBBBAA5D2E0B2FABAE0C4B68188CFEDBEA9CDA4C1C18189818A818BC7D7D9F1818CD9F4818D818E818F8190C8CBD8E9819181928193D2DACAB2C8CAD8ECD8EAD8C6BDF6C6CDB3F08194D8EBBDF1BDE98195C8D4B4D381968197C2D88198B2D6D7D0CACBCBFBD5CCB8B6CFC98199819A819BD9DAD8F0C7AA819CD8EE819DB4FAC1EED2D4819E819FD8ED81A0D2C7D8EFC3C781A181A281A3D1F681A4D6D9D8F281A5D8F5BCFEBCDB81A681A781A8C8CE81A9B7DD81AAB7C281ABC6F381AC81AD81AE81AF81B081B181B2D8F8D2C181B381B4CEE9BCBFB7FCB7A5D0DD81B581B681B781B881B9D6DAD3C5BBEFBBE1D8F181BA81BBC9A1CEB0B4AB81BCD8F381BDC9CBD8F6C2D7D8F781BE81BFCEB1D8F981C081C181C2B2AEB9C081C3D9A381C4B0E981C5C1E681C6C9EC81C7CBC581C8CBC6D9A481C981CA81CB81CC81CDB5E881CE81CFB5AB81D081D181D281D381D481D5CEBBB5CDD7A1D7F4D3D381D6CCE581D7BACE81D8D9A2D9DCD3E0D8FDB7F0D7F7D8FED8FAD9A1C4E381D981DAD3B6D8F4D9DD81DBD8FB81DCC5E581DD81DEC0D081DF81E0D1F0B0DB81E181E2BCD1D9A681E3D9A581E481E581E681E7D9ACD9AE81E8D9ABCAB981E981EA81EBD9A9D6B681EC81ED81EEB3DED9A881EFC0FD81F0CACC81F1D9AA81F2D9A781F381F4D9B081F581F6B6B181F781F881F9B9A981FAD2C081FB81FCCFC081FD81FEC2C28240BDC4D5ECB2E0C7C8BFEBD9AD8241D9AF8242CEEABAEE82438244824582468247C7D682488249824A824B824C824D824E824F8250B1E3825182528253B4D9B6EDD9B48254825582568257BFA182588259825AD9DEC7CEC0FED9B8825B825C825D825E825FCBD7B7FD8260D9B58261D9B7B1A3D3E1D9B98262D0C58263D9B682648265D9B18266D9B2C1A9D9B382678268BCF3D0DEB8A98269BEE3826AD9BD826B826C826D826ED9BA826FB0B3827082718272D9C28273827482758276827782788279827A827B827C827D827E8280D9C4B1B68281D9BF82828283B5B98284BEF3828582868287CCC8BAF2D2D08288D9C38289828ABDE8828BB3AB828C828D828ED9C5BEEB828FD9C6D9BBC4DF8290D9BED9C1D9C0829182928293829482958296829782988299829A829BD5AE829CD6B5829DC7E3829E829F82A082A1D9C882A282A382A4BCD9D9CA82A582A682A7D9BC82A8D9CBC6AB82A982AA82AB82AC82ADD9C982AE82AF82B082B1D7F682B2CDA382B382B482B582B682B782B882B982BABDA182BB82BC82BD82BE82BF82C0D9CC82C182C282C382C482C582C682C782C882C9C5BCCDB582CA82CB82CCD9CD82CD82CED9C7B3A5BFFE82CF82D082D182D2B8B582D382D4C0FC82D582D682D782D8B0F882D982DA82DB82DC82DD82DE82DF82E082E182E282E382E482E582E682E782E882E982EA82EB82EC82EDB4F682EED9CE82EFD9CFB4A2D9D082F082F1B4DF82F282F382F482F582F6B0C182F782F882F982FA82FB82FC82FDD9D1C9B582FE8340834183428343834483458346834783488349834A834B834C834D834E834F83508351CFF1835283538354835583568357D9D283588359835AC1C5835B835C835D835E835F836083618362836383648365D9D6C9AE8366836783688369D9D5D9D4D9D7836A836B836C836DCBDB836EBDA9836F8370837183728373C6A7837483758376837783788379837A837B837C837DD9D3D9D8837E83808381D9D9838283838384838583868387C8E583888389838A838B838C838D838E838F839083918392839383948395C0DC8396839783988399839A839B839C839D839E839F83A083A183A283A383A483A583A683A783A883A983AA83AB83AC83AD83AE83AF83B083B183B2B6F9D8A3D4CA83B3D4AAD0D6B3E4D5D783B4CFC8B9E283B5BFCB83B6C3E283B783B883B9B6D283BA83BBCDC3D9EED9F083BC83BD83BEB5B383BFB6B583C083C183C283C383C4BEA483C583C6C8EB83C783C8C8AB83C983CAB0CBB9ABC1F9D9E283CBC0BCB9B283CCB9D8D0CBB1F8C6E4BEDFB5E4D7C883CDD1F8BCE6CADE83CE83CFBCBDD9E6D8E783D083D1C4DA83D283D3B8D4C8BD83D483D5B2E1D4D983D683D783D883D9C3B083DA83DBC3E1DAA2C8DF83DCD0B483DDBEFCC5A983DE83DF83E0B9DA83E1DAA383E2D4A9DAA483E383E483E583E683E7D9FBB6AC83E883E9B7EBB1F9D9FCB3E5BEF683EABFF6D2B1C0E483EB83EC83EDB6B3D9FED9FD83EE83EFBEBB83F083F183F2C6E083F3D7BCDAA183F4C1B983F5B5F2C1E883F683F7BCF583F8B4D583F983FA83FB83FC83FD83FE844084418442C1DD8443C4FD84448445BCB8B7B284468447B7EF84488449844A844B844C844DD9EC844EC6BE844FBFADBBCB84508451B5CA8452DBC9D0D78453CDB9B0BCB3F6BBF7DBCABAAF8454D4E4B5B6B5F3D8D6C8D084558456B7D6C7D0D8D78457BFAF84588459DBBBD8D8845A845BD0CCBBAE845C845D845EEBBEC1D0C1F5D4F2B8D5B4B4845FB3F584608461C9BE846284638464C5D0846584668467C5D9C0FB8468B1F08469D8D9B9CE846AB5BD846B846CD8DA846D846ED6C6CBA2C8AFC9B2B4CCBFCC846FB9F48470D8DBD8DCB6E7BCC1CCEA847184728473847484758476CFF78477D8DDC7B084788479B9D0BDA3847A847BCCDE847CC6CA847D847E848084818482D8E08483D8DE84848485D8DF848684878488B0FE8489BEE7848ACAA3BCF4848B848C848D848EB8B1848F8490B8EE849184928493849484958496849784988499849AD8E2849BBDCB849CD8E4D8E3849D849E849F84A084A1C5FC84A284A384A484A584A684A784A8D8E584A984AAD8E684AB84AC84AD84AE84AF84B084B1C1A684B2C8B0B0ECB9A6BCD3CEF1DBBDC1D384B384B484B584B6B6AFD6FAC5ACBDD9DBBEDBBF84B784B884B9C0F8BEA2C0CD84BA84BB84BC84BD84BE84BF84C084C184C284C3DBC0CAC684C484C584C6B2AA84C784C884C9D3C284CAC3E384CBD1AB84CC84CD84CE84CFDBC284D0C0D584D184D284D3DBC384D4BFB184D584D684D784D884D984DAC4BC84DB84DC84DD84DEC7DA84DF84E084E184E284E384E484E584E684E784E884E9DBC484EA84EB84EC84ED84EE84EF84F084F1D9E8C9D784F284F384F4B9B4CEF0D4C884F584F684F784F8B0FCB4D284F9D0D984FA84FB84FC84FDD9E984FEDECBD9EB8540854185428543D8B0BBAFB1B18544B3D7D8CE85458546D4D185478548BDB3BFEF8549CFBB854A854BD8D0854C854D854EB7CB854F85508551D8D185528553855485558556855785588559855A855BC6A5C7F8D2BD855C855DD8D2C4E4855ECAAE855FC7A78560D8A68561C9FDCEE7BBDCB0EB856285638564BBAAD0AD8565B1B0D7E4D7BF8566B5A5C2F4C4CF85678568B2A98569B2B7856AB1E5DFB2D5BCBFA8C2ACD8D5C2B1856BD8D4CED4856CDAE0856DCEC0856E856FD8B4C3AED3A1CEA38570BCB4C8B4C2D18571BEEDD0B68572DAE18573857485758576C7E485778578B3A78579B6F2CCFCC0FA857A857BC0F7857CD1B9D1E1D8C7857D857E85808581858285838584B2DE85858586C0E58587BAF185888589D8C8858AD4AD858B858CCFE1D8C9858DD8CACFC3858EB3F8BEC7858F859085918592D8CB8593859485958596859785988599DBCC859A859B859C859DC8A5859E859F85A0CFD885A1C8FEB2CE85A285A385A485A585A6D3D6B2E6BCB0D3D1CBABB7B485A785A885A9B7A285AA85ABCAE585ACC8A1CADCB1E4D0F085ADC5D185AE85AF85B0DBC5B5FE85B185B2BFDAB9C5BEE4C1ED85B3DFB6DFB5D6BBBDD0D5D9B0C8B6A3BFC9CCA8DFB3CAB7D3D285B4D8CFD2B6BAC5CBBECCBE85B5DFB7B5F0DFB485B685B785B8D3F585B9B3D4B8F785BADFBA85BBBACFBCAAB5F585BCCDACC3FBBAF3C0F4CDC2CFF2DFB8CFC585BDC2C0DFB9C2F085BE85BF85C0BEFD85C1C1DFCDCCD2F7B7CDDFC185C2DFC485C385C4B7F1B0C9B6D6B7D485C5BAACCCFDBFD4CBB1C6F485C6D6A8DFC585C7CEE2B3B385C885C9CEFCB4B585CACEC7BAF085CBCEE185CCD1BD85CD85CEDFC085CF85D0B4F485D1B3CA85D2B8E6DFBB85D385D485D585D6C4C585D7DFBCDFBDDFBEC5BBDFBFDFC2D4B1DFC385D8C7BACED885D985DA85DB85DC85DDC4D885DEDFCA85DFDFCF85E0D6DC85E185E285E385E485E585E685E785E8DFC9DFDACEB685E9BAC7DFCEDFC8C5DE85EA85EBC9EBBAF4C3FC85EC85EDBED785EEDFC685EFDFCD85F0C5D885F185F285F385F4D5A6BACD85F5BECCD3BDB8C085F6D6E485F7DFC7B9BEBFA785F885F9C1FCDFCBDFCC85FADFD085FB85FC85FD85FE8640DFDBDFE58641DFD7DFD6D7C9DFE3DFE4E5EBD2A7DFD28642BFA98643D4DB8644BFC8DFD4864586468647CFCC86488649DFDD864AD1CA864BDFDEB0A7C6B7DFD3864CBAE5864DB6DFCDDBB9FED4D5864E864FDFDFCFECB0A5DFE7DFD1D1C6DFD5DFD8DFD9DFDC8650BBA98651DFE0DFE18652DFE2DFE6DFE8D3B486538654865586568657B8E7C5B6DFEAC9DAC1A8C4C486588659BFDECFF8865A865B865CD5DCDFEE865D865E865F866086618662B2B88663BADFDFEC8664DBC18665D1E48666866786688669CBF4B4BD866AB0A6866B866C866D866E866FDFF1CCC6DFF286708671DFED867286738674867586768677DFE986788679867A867BDFEB867CDFEFDFF0BBBD867D867EDFF386808681DFF48682BBA38683CADBCEA8E0A7B3AA8684E0A6868586868687E0A186888689868A868BDFFE868CCDD9DFFC868DDFFA868EBFD0D7C4868FC9CC86908691DFF8B0A186928693869486958696DFFD869786988699869ADFFBE0A2869B869C869D869E869FE0A886A086A186A286A3B7C886A486A5C6A1C9B6C0B2DFF586A686A7C5BE86A8D8C4DFF9C4F686A986AA86AB86AC86AD86AEE0A3E0A4E0A5D0A586AF86B0E0B4CCE486B1E0B186B2BFA6E0AFCEB9E0ABC9C686B386B4C0AEE0AEBAEDBAB0E0A986B586B686B7DFF686B8E0B386B986BAE0B886BB86BC86BDB4ADE0B986BE86BFCFB2BAC886C0E0B086C186C286C386C486C586C686C7D0FA86C886C986CA86CB86CC86CD86CE86CF86D0E0AC86D1D4FB86D2DFF786D3C5E786D4E0AD86D5D3F786D6E0B6E0B786D786D886D986DA86DBE0C4D0E186DC86DD86DEE0BC86DF86E0E0C9E0CA86E186E286E3E0BEE0AAC9A4E0C186E4E0B286E586E686E786E886E9CAC8E0C386EAE0B586EBCECB86ECCBC3E0CDE0C6E0C286EDE0CB86EEE0BAE0BFE0C086EF86F0E0C586F186F2E0C7E0C886F3E0CC86F4E0BB86F586F686F786F886F9CBD4E0D586FAE0D6E0D286FB86FC86FD86FE87408741E0D0BCCE87428743E0D18744B8C2D8C587458746874787488749874A874B874CD0EA874D874EC2EF874F8750E0CFE0BD875187528753E0D4E0D387548755E0D78756875787588759E0DCE0D8875A875B875CD6F6B3B0875DD7EC875ECBBB875F8760E0DA8761CEFB876287638764BAD987658766876787688769876A876B876C876D876E876F8770E0E1E0DDD2AD87718772877387748775E0E287768777E0DBE0D9E0DF87788779E0E0877A877B877C877D877EE0DE8780E0E4878187828783C6F7D8ACD4EBE0E6CAC98784878587868787E0E587888789878A878BB8C1878C878D878E878FE0E7E0E887908791879287938794879587968797E0E9E0E387988799879A879B879C879D879EBABFCCE7879F87A087A1E0EA87A287A387A487A587A687A787A887A987AA87AB87AC87AD87AE87AF87B0CFF987B187B287B387B487B587B687B787B887B987BA87BBE0EB87BC87BD87BE87BF87C087C187C2C8C287C387C487C587C6BDC087C787C887C987CA87CB87CC87CD87CE87CF87D087D187D287D3C4D287D487D587D687D787D887D987DA87DB87DCE0EC87DD87DEE0ED87DF87E0C7F4CBC487E1E0EEBBD8D8B6D2F2E0EFCDC587E2B6DA87E387E487E587E687E787E8E0F187E9D4B087EA87EBC0A7B4D187EC87EDCEA7E0F087EE87EF87F0E0F2B9CC87F187F2B9FACDBCE0F387F387F487F5C6D4E0F487F6D4B287F7C8A6E0F6E0F587F887F987FA87FB87FC87FD87FE8840884188428843884488458846884788488849E0F7884A884BCDC1884C884D884ECAA5884F885088518852D4DADBD7DBD98853DBD8B9E7DBDCDBDDB5D888548855DBDA8856885788588859885ADBDBB3A1DBDF885B885CBBF8885DD6B7885EDBE0885F886088618862BEF988638864B7BB8865DBD0CCAEBFB2BBB5D7F8BFD38866886788688869886ABFE9886B886CBCE1CCB3DBDEB0D3CEEBB7D8D7B9C6C2886D886EC0A4886FCCB98870DBE7DBE1C6BADBE38871DBE88872C5F7887388748875DBEA88768877DBE9BFC088788879887ADBE6DBE5887B887C887D887E8880B4B9C0ACC2A2DBE2DBE48881888288838884D0CDDBED88858886888788888889C0DDDBF2888A888B888C888D888E888F8890B6E28891889288938894DBF3DBD2B9B8D4ABDBEC8895BFD1DBF08896DBD18897B5E68898DBEBBFE58899889A889BDBEE889CDBF1889D889E889FDBF988A088A188A288A388A488A588A688A788A8B9A1B0A388A988AA88AB88AC88AD88AE88AFC2F188B088B1B3C7DBEF88B288B3DBF888B4C6D2DBF488B588B6DBF5DBF7DBF688B788B8DBFE88B9D3F2B2BA88BA88BB88BCDBFD88BD88BE88BF88C088C188C288C388C4DCA488C5DBFB88C688C788C888C9DBFA88CA88CB88CCDBFCC5E0BBF988CD88CEDCA388CF88D0DCA588D1CCC388D288D388D4B6D1DDC088D588D688D7DCA188D8DCA288D988DA88DBC7B588DC88DD88DEB6E988DF88E088E1DCA788E288E388E488E5DCA688E6DCA9B1A488E788E8B5CC88E988EA88EB88EC88EDBFB088EE88EF88F088F188F2D1DF88F388F488F588F6B6C288F788F888F988FA88FB88FC88FD88FE894089418942894389448945DCA88946894789488949894A894B894CCBFAEBF3894D894E894FCBDC89508951CBFE895289538954CCC189558956895789588959C8FB895A895B895C895D895E895FDCAA89608961896289638964CCEEDCAB89658966896789688969896A896B896C896D896E896F897089718972897389748975DBD38976DCAFDCAC8977BEB38978CAFB8979897A897BDCAD897C897D897E89808981898289838984C9CAC4B989858986898789888989C7BDDCAE898A898B898CD4F6D0E6898D898E898F89908991899289938994C4ABB6D589958996899789988999899A899B899C899D899E899F89A089A189A289A389A489A589A6DBD489A789A889A989AAB1DA89AB89AC89ADDBD589AE89AF89B089B189B289B389B489B589B689B789B8DBD689B989BA89BBBABE89BC89BD89BE89BF89C089C189C289C389C489C589C689C789C889C9C8C089CA89CB89CC89CD89CE89CFCABFC8C989D0D7B389D1C9F989D289D3BFC789D489D5BAF889D689D7D2BC89D889D989DA89DB89DC89DD89DE89DFE2BA89E0B4A689E189E2B1B889E389E489E589E689E7B8B489E8CFC489E989EA89EB89ECD9E7CFA6CDE289ED89EED9EDB6E089EFD2B989F089F1B9BB89F289F389F489F5E2B9E2B789F6B4F389F7CCECCCABB7F289F8D8B2D1EBBABB89F9CAA789FA89FBCDB789FC89FDD2C4BFE4BCD0B6E189FEDEC58A408A418A428A43DEC6DBBC8A44D1D98A458A46C6E6C4CEB7EE8A47B7DC8A488A49BFFCD7E08A4AC6F58A4B8A4CB1BCDEC8BDB1CCD7DECA8A4DDEC98A4E8A4F8A508A518A52B5EC8A53C9DD8A548A55B0C28A568A578A588A598A5A8A5B8A5C8A5D8A5E8A5F8A608A618A62C5AEC5AB8A63C4CC8A64BCE9CBFD8A658A668A67BAC38A688A698A6AE5F9C8E7E5FACDFD8A6BD7B1B8BEC2E88A6CC8D18A6D8A6EE5FB8A6F8A708A718A72B6CABCCB8A738A74D1FDE6A18A75C3EE8A768A778A788A79E6A48A7A8A7B8A7C8A7DE5FEE6A5CDD78A7E8A80B7C1E5FCE5FDE6A38A818A82C4DDE6A88A838A84E6A78A858A868A878A888A898A8AC3C38A8BC6DE8A8C8A8DE6AA8A8E8A8F8A908A918A928A938A94C4B78A958A968A97E6A2CABC8A988A998A9A8A9BBDE3B9C3E6A6D0D5CEAF8A9C8A9DE6A9E6B08A9ED2A68A9FBDAAE6AD8AA08AA18AA28AA38AA4E6AF8AA5C0D18AA68AA7D2CC8AA88AA98AAABCA78AAB8AAC8AAD8AAE8AAF8AB08AB18AB28AB38AB48AB58AB6E6B18AB7D2F68AB88AB98ABAD7CB8ABBCDFE8ABCCDDEC2A6E6ABE6ACBDBFE6AEE6B38ABD8ABEE6B28ABF8AC08AC18AC2E6B68AC3E6B88AC48AC58AC68AC7C4EF8AC88AC98ACAC4C88ACB8ACCBEEAC9EF8ACD8ACEE6B78ACFB6F08AD08AD18AD2C3E48AD38AD48AD58AD68AD78AD88AD9D3E9E6B48ADAE6B58ADBC8A28ADC8ADD8ADE8ADF8AE0E6BD8AE18AE28AE3E6B98AE48AE58AE68AE78AE8C6C58AE98AEACDF1E6BB8AEB8AEC8AED8AEE8AEF8AF08AF18AF28AF38AF4E6BC8AF58AF68AF78AF8BBE98AF98AFA8AFB8AFC8AFD8AFE8B40E6BE8B418B428B438B44E6BA8B458B46C0B78B478B488B498B4A8B4B8B4C8B4D8B4E8B4FD3A4E6BFC9F4E6C38B508B51E6C48B528B538B548B55D0F68B568B578B588B598B5A8B5B8B5C8B5D8B5E8B5F8B608B618B628B638B648B658B668B67C3BD8B688B698B6A8B6B8B6C8B6D8B6EC3C4E6C28B6F8B708B718B728B738B748B758B768B778B788B798B7A8B7B8B7CE6C18B7D8B7E8B808B818B828B838B84E6C7CFB18B85EBF48B868B87E6CA8B888B898B8A8B8B8B8CE6C58B8D8B8EBCDEC9A98B8F8B908B918B928B938B94BCB58B958B96CFD38B978B988B998B9A8B9BE6C88B9CE6C98B9DE6CE8B9EE6D08B9F8BA08BA1E6D18BA28BA38BA4E6CBB5D58BA5E6CC8BA68BA7E6CF8BA88BA9C4DB8BAAE6C68BAB8BAC8BAD8BAE8BAFE6CD8BB08BB18BB28BB38BB48BB58BB68BB78BB88BB98BBA8BBB8BBC8BBD8BBE8BBF8BC08BC18BC28BC38BC48BC58BC6E6D28BC78BC88BC98BCA8BCB8BCC8BCD8BCE8BCF8BD08BD18BD2E6D4E6D38BD38BD48BD58BD68BD78BD88BD98BDA8BDB8BDC8BDD8BDE8BDF8BE08BE18BE28BE38BE48BE58BE68BE78BE88BE98BEA8BEB8BECE6D58BEDD9F88BEE8BEFE6D68BF08BF18BF28BF38BF48BF58BF68BF7E6D78BF88BF98BFA8BFB8BFC8BFD8BFE8C408C418C428C438C448C458C468C47D7D3E6DD8C48E6DEBFD7D4D08C49D7D6B4E6CBEFE6DAD8C3D7CED0A28C4AC3CF8C4B8C4CE6DFBCBEB9C2E6DBD1A78C4D8C4EBAA2C2CF8C4FD8AB8C508C518C52CAEBE5EE8C53E6DC8C54B7F58C558C568C578C58C8E68C598C5AC4F58C5B8C5CE5B2C4FE8C5DCBFCE5B3D5AC8C5ED3EECAD8B0B28C5FCBCECDEA8C608C61BAEA8C628C638C64E5B58C65E5B48C66D7DAB9D9D6E6B6A8CDF0D2CBB1A6CAB58C67B3E8C9F3BFCDD0FBCAD2E5B6BBC28C688C698C6ACFDCB9AC8C6B8C6C8C6D8C6ED4D78C6F8C70BAA6D1E7CFFCBCD28C71E5B7C8DD8C728C738C74BFEDB1F6CBDE8C758C76BCC58C77BCC4D2FAC3DCBFDC8C788C798C7A8C7BB8BB8C7C8C7D8C7EC3C28C80BAAED4A28C818C828C838C848C858C868C878C888C89C7DEC4AFB2EC8C8AB9D18C8B8C8CE5BBC1C88C8D8C8ED5AF8C8F8C908C918C928C93E5BC8C94E5BE8C958C968C978C988C998C9A8C9BB4E7B6D4CBC2D1B0B5BC8C9C8C9DCAD98C9EB7E28C9F8CA0C9E48CA1BDAB8CA28CA3CEBED7F08CA48CA58CA68CA7D0A18CA8C9D98CA98CAAB6FBE6D8BCE28CABB3BE8CACC9D08CADE6D9B3A28CAE8CAF8CB08CB1DECC8CB2D3C8DECD8CB3D2A28CB48CB58CB68CB7DECE8CB88CB98CBA8CBBBECD8CBC8CBDDECF8CBE8CBF8CC0CAACD2FCB3DFE5EAC4E1BEA1CEB2C4F2BED6C6A8B2E38CC18CC2BED38CC38CC4C7FCCCEBBDECCEDD8CC58CC6CABAC6C1E5ECD0BC8CC78CC88CC9D5B98CCA8CCB8CCCE5ED8CCD8CCE8CCF8CD0CAF48CD1CDC0C2C58CD2E5EF8CD3C2C4E5F08CD48CD58CD68CD78CD88CD98CDAE5F8CDCD8CDBC9BD8CDC8CDD8CDE8CDF8CE08CE18CE2D2D9E1A88CE38CE48CE58CE6D3EC8CE7CBEAC6F18CE88CE98CEA8CEB8CECE1AC8CED8CEE8CEFE1A7E1A98CF08CF1E1AAE1AF8CF28CF3B2ED8CF4E1ABB8DAE1ADE1AEE1B0B5BAE1B18CF58CF68CF78CF88CF9E1B3E1B88CFA8CFB8CFC8CFD8CFED1D28D40E1B6E1B5C1EB8D418D428D43E1B78D44D4C08D45E1B28D46E1BAB0B68D478D488D498D4AE1B48D4BBFF98D4CE1B98D4D8D4EE1BB8D4F8D508D518D528D538D54E1BE8D558D568D578D588D598D5AE1BC8D5B8D5C8D5D8D5E8D5F8D60D6C58D618D628D638D648D658D668D67CFBF8D688D69E1BDE1BFC2CD8D6AB6EB8D6BD3F88D6C8D6DC7CD8D6E8D6FB7E58D708D718D728D738D748D758D768D778D788D79BEFE8D7A8D7B8D7C8D7D8D7E8D80E1C0E1C18D818D82E1C7B3E78D838D848D858D868D878D88C6E98D898D8A8D8B8D8C8D8DB4DE8D8ED1C28D8F8D908D918D92E1C88D938D94E1C68D958D968D978D988D99E1C58D9AE1C3E1C28D9BB1C08D9C8D9D8D9ED5B8E1C48D9F8DA08DA18DA28DA3E1CB8DA48DA58DA68DA78DA88DA98DAA8DABE1CCE1CA8DAC8DAD8DAE8DAF8DB08DB18DB28DB3EFFA8DB48DB5E1D3E1D2C7B68DB68DB78DB88DB98DBA8DBB8DBC8DBD8DBE8DBF8DC0E1C98DC18DC2E1CE8DC3E1D08DC48DC58DC68DC78DC88DC98DCA8DCB8DCC8DCD8DCEE1D48DCFE1D1E1CD8DD08DD1E1CF8DD28DD38DD48DD5E1D58DD68DD78DD88DD98DDA8DDB8DDC8DDD8DDE8DDF8DE08DE18DE2E1D68DE38DE48DE58DE68DE78DE88DE98DEA8DEB8DEC8DED8DEE8DEF8DF08DF18DF28DF38DF48DF58DF68DF78DF8E1D78DF98DFA8DFBE1D88DFC8DFD8DFE8E408E418E428E438E448E458E468E478E488E498E4A8E4B8E4C8E4D8E4E8E4F8E508E518E528E538E548E55E1DA8E568E578E588E598E5A8E5B8E5C8E5D8E5E8E5F8E608E618E62E1DB8E638E648E658E668E678E688E69CEA18E6A8E6B8E6C8E6D8E6E8E6F8E708E718E728E738E748E758E76E7DD8E77B4A8D6DD8E788E79D1B2B3B28E7A8E7BB9A4D7F3C7C9BEDEB9AE8E7CCED78E7D8E7EB2EEDBCF8E80BCBAD2D1CBC8B0CD8E818E82CFEF8E838E848E858E868E87D9E3BDED8E888E89B1D2CAD0B2BC8E8ACBA7B7AB8E8BCAA68E8C8E8D8E8ECFA38E8F8E90E0F8D5CAE0FB8E918E92E0FAC5C1CCFB8E93C1B1E0F9D6E3B2AFD6C4B5DB8E948E958E968E978E988E998E9A8E9BB4F8D6A18E9C8E9D8E9E8E9F8EA0CFAFB0EF8EA18EA2E0FC8EA38EA48EA58EA68EA7E1A1B3A38EA88EA9E0FDE0FEC3B18EAA8EAB8EAC8EADC3DD8EAEE1A2B7F98EAF8EB08EB18EB28EB38EB4BBCF8EB58EB68EB78EB88EB98EBA8EBBE1A3C4BB8EBC8EBD8EBE8EBF8EC0E1A48EC18EC2E1A58EC38EC4E1A6B4B18EC58EC68EC78EC88EC98ECA8ECB8ECC8ECD8ECE8ECF8ED08ED18ED28ED3B8C9C6BDC4EA8ED4B2A28ED5D0D28ED6E7DBBBC3D3D7D3C48ED7B9E3E2CF8ED88ED98EDAD7AF8EDBC7ECB1D38EDC8EDDB4B2E2D18EDE8EDF8EE0D0F2C2AEE2D08EE1BFE2D3A6B5D7E2D2B5EA8EE2C3EDB8FD8EE3B8AE8EE4C5D3B7CFE2D48EE58EE68EE78EE8E2D3B6C8D7F98EE98EEA8EEB8EEC8EEDCDA58EEE8EEF8EF08EF18EF2E2D88EF3E2D6CAFCBFB5D3B9E2D58EF48EF58EF68EF7E2D78EF88EF98EFA8EFB8EFC8EFD8EFE8F408F418F42C1AEC0C88F438F448F458F468F478F48E2DBE2DAC0AA8F498F4AC1CE8F4B8F4C8F4D8F4EE2DC8F4F8F508F518F528F538F548F558F568F578F588F598F5AE2DD8F5BE2DE8F5C8F5D8F5E8F5F8F608F618F628F638F64DBC88F65D1D3CDA28F668F67BDA88F688F698F6ADEC3D8A5BFAADBCDD2ECC6FAC5AA8F6B8F6C8F6DDEC48F6EB1D7DFAE8F6F8F708F71CABD8F72DFB18F73B9AD8F74D2FD8F75B8A5BAEB8F768F77B3DA8F788F798F7AB5DCD5C58F7B8F7C8F7D8F7EC3D6CFD2BBA18F80E5F3E5F28F818F82E5F48F83CDE48F84C8F58F858F868F878F888F898F8A8F8BB5AFC7BF8F8CE5F68F8D8F8E8F8FECB08F908F918F928F938F948F958F968F978F988F998F9A8F9B8F9C8F9D8F9EE5E68F9FB9E9B5B18FA0C2BCE5E8E5E7E5E98FA18FA28FA38FA4D2CD8FA58FA68FA7E1EAD0CE8FA8CDAE8FA9D1E58FAA8FABB2CAB1EB8FACB1F2C5ED8FAD8FAED5C3D3B08FAFE1DC8FB08FB18FB2E1DD8FB3D2DB8FB4B3B9B1CB8FB58FB68FB7CDF9D5F7E1DE8FB8BEB6B4FD8FB9E1DFBADCE1E0BBB2C2C9E1E18FBA8FBB8FBCD0EC8FBDCDBD8FBE8FBFE1E28FC0B5C3C5C7E1E38FC18FC2E1E48FC38FC48FC58FC6D3F98FC78FC88FC98FCA8FCB8FCCE1E58FCDD1AD8FCE8FCFE1E6CEA28FD08FD18FD28FD38FD48FD5E1E78FD6B5C28FD78FD88FD98FDAE1E8BBD58FDB8FDC8FDD8FDE8FDFD0C4E2E0B1D8D2E48FE08FE1E2E18FE28FE3BCC9C8CC8FE4E2E3ECFEECFDDFAF8FE58FE68FE7E2E2D6BECDFCC3A68FE88FE98FEAE3C38FEB8FECD6D2E2E78FED8FEEE2E88FEF8FF0D3C78FF18FF2E2ECBFEC8FF3E2EDE2E58FF48FF5B3C08FF68FF78FF8C4EE8FF98FFAE2EE8FFB8FFCD0C38FFDBAF6E2E9B7DEBBB3CCACCBCBE2E4E2E6E2EAE2EB8FFE90409041E2F790429043E2F4D4F5E2F390449045C5AD9046D5FAC5C2B2C090479048E2EF9049E2F2C1AFCBBC904A904BB5A1E2F9904C904D904EBCB1E2F1D0D4D4B9E2F5B9D6E2F6904F90509051C7D390529053905490559056E2F0905790589059905A905BD7DCEDA1905C905DE2F8905EEDA5E2FECAD1905F906090619062906390649065C1B59066BBD090679068BFD69069BAE3906A906BCBA1906C906D906EEDA6EDA3906F9070EDA29071907290739074BBD6EDA7D0F490759076EDA4BADEB6F7E3A1B6B2CCF1B9A79077CFA2C7A190789079BFD2907A907BB6F1907CE2FAE2FBE2FDE2FCC4D5E3A2907DD3C1907E90809081E3A7C7C49082908390849085CFA490869087E3A9BAB790889089908A908BE3A8908CBBDA908DE3A3908E908F9090E3A4E3AA9091E3A69092CEF2D3C690939094BBBC90959096D4C39097C4FA90989099EDA8D0FCE3A5909AC3F5909BE3ADB1AF909CE3B2909D909E909FBCC290A090A1E3ACB5BF90A290A390A490A590A690A790A890A9C7E9E3B090AA90AB90ACBEAACDEF90AD90AE90AF90B090B1BBF390B290B390B4CCE890B590B6E3AF90B7E3B190B8CFA7E3AE90B9CEA9BBDD90BA90BB90BC90BD90BEB5EBBEE5B2D2B3CD90BFB1B9E3ABB2D1B5ACB9DFB6E890C090C1CFEBE3B790C2BBCC90C390C4C8C7D0CA90C590C690C790C890C9E3B8B3EE90CA90CB90CC90CDEDA990CED3FAD3E490CF90D090D1EDAAE3B9D2E290D290D390D490D590D6E3B590D790D890D990DAD3DE90DB90DC90DD90DEB8D0E3B390DF90E0E3B6B7DF90E1E3B4C0A290E290E390E4E3BA90E590E690E790E890E990EA90EB90EC90ED90EE90EF90F090F190F290F390F490F590F690F7D4B890F890F990FA90FB90FC90FD90FE9140B4C89141E3BB9142BBC59143C9F791449145C9E5914691479148C4BD9149914A914B914C914D914E914FEDAB9150915191529153C2FD9154915591569157BBDBBFAE91589159915A915B915C915D915ECEBF915F916091619162E3BC9163BFB6916491659166916791689169916A916B916C916D916E916F9170917191729173917491759176B1EF91779178D4F79179917A917B917C917DE3BE917E9180918191829183918491859186EDAD918791889189918A918B918C918D918E918FE3BFBAA9EDAC91909191E3BD91929193919491959196919791989199919A919BE3C0919C919D919E919F91A091A1BAB691A291A391A4B6AE91A591A691A791A891A9D0B891AAB0C3EDAE91AB91AC91AD91AE91AFEDAFC0C191B0E3C191B191B291B391B491B591B691B791B891B991BA91BB91BC91BD91BE91BF91C091C1C5B391C291C391C491C591C691C791C891C991CA91CB91CC91CD91CE91CFE3C291D091D191D291D391D491D591D691D791D8DCB291D991DA91DB91DC91DD91DEEDB091DFB8EA91E0CEECEAA7D0E7CAF9C8D6CFB7B3C9CED2BDE491E191E2E3DEBBF2EAA8D5BD91E3C6DDEAA991E491E591E6EAAA91E7EAACEAAB91E8EAAEEAAD91E991EA91EB91ECBDD891EDEAAF91EEC2BE91EF91F091F191F2B4C1B4F791F391F4BBA791F591F691F791F891F9ECE6ECE5B7BFCBF9B1E291FAECE791FB91FC91FDC9C8ECE8ECE991FECAD6DED0B2C5D4FA92409241C6CBB0C7B4F2C8D3924292439244CDD092459246BFB8924792489249924A924B924C924DBFDB924E924FC7A4D6B49250C0A9DED1C9A8D1EFC5A4B0E7B3B6C8C592519252B0E292539254B7F692559256C5FA92579258B6F39259D5D2B3D0BCBC925A925B925CB3AD925D925E925F9260BEF1B0D1926192629263926492659266D2D6CAE3D7A59267CDB6B6B6BFB9D5DB9268B8A7C5D79269926A926BDED2BFD9C2D5C7C0926CBBA4B1A8926D926EC5EA926F9270C5FBCCA79271927292739274B1A7927592769277B5D692789279927AC4A8927BDED3D1BAB3E9927CC3F2927D927EB7F79280D6F4B5A3B2F0C4B4C4E9C0ADDED49281B0E8C5C4C1E09282B9D59283BEDCCDD8B0CE9284CDCFDED6BED0D7BEDED5D5D0B0DD92859286C4E292879288C2A3BCF09289D3B5C0B9C5A1B2A6D4F1928A928BC0A8CAC3DED7D5FC928CB9B0928DC8ADCBA9928EDED9BFBD928F929092919292C6B4D7A7CAB0C4C39293B3D6B9D29294929592969297D6B8EAFCB0B492989299929A929BBFE6929C929DCCF4929E929F92A092A1CDDA92A292A392A4D6BFC2CE92A5CECECCA2D0AEC4D3B5B2DED8D5F5BCB7BBD392A692A7B0A492A8C5B2B4EC92A992AA92ABD5F192AC92ADEAFD92AE92AF92B092B192B292B3DEDACDA692B492B5CDEC92B692B792B892B9CEE6DEDC92BACDB1C0A692BB92BCD7BD92BDDEDBB0C6BAB4C9D3C4F3BEE892BE92BF92C092C1B2B692C292C392C492C592C692C792C892C9C0CCCBF092CABCF1BBBBB5B792CB92CC92CDC5F592CEDEE692CF92D092D1DEE3BEDD92D292D3DEDF92D492D592D692D7B4B7BDDD92D892D9DEE0C4ED92DA92DB92DC92DDCFC692DEB5E092DF92E092E192E2B6DECADAB5F4DEE592E3D5C692E4DEE1CCCDC6FE92E5C5C592E692E792E8D2B492E9BEF292EA92EB92EC92ED92EE92EF92F0C2D392F1CCBDB3B892F2BDD392F3BFD8CDC6D1DAB4EB92F4DEE4DEDDDEE792F5EAFE92F692F7C2B0DEE292F892F9D6C0B5A792FAB2F492FBDEE892FCDEF292FD92FE934093419342DEED9343DEF193449345C8E0934693479348D7E1DEEFC3E8CCE19349B2E5934A934B934CD2BE934D934E934F9350935193529353DEEE9354DEEBCED59355B4A79356935793589359935ABFABBEBE935B935CBDD2935D935E935F9360DEE99361D4AE9362DEDE9363DEEA9364936593669367C0BF9368DEECB2F3B8E9C2A79369936ABDC1936B936C936D936E936FDEF5DEF893709371B2ABB4A493729373B4EAC9A6937493759376937793789379DEF6CBD1937AB8E3937BDEF7DEFA937C937D937E9380DEF9938193829383CCC29384B0E1B4EE93859386938793889389938AE5BA938B938C938D938E938FD0AF93909391B2EB9392EBA19393DEF493949395C9E3DEF3B0DAD2A1B1F79396CCAF939793989399939A939B939C939DDEF0939ECBA4939F93A093A1D5AA93A293A393A493A593A6DEFB93A793A893A993AA93AB93AC93AD93AEB4DD93AFC4A693B093B193B2DEFD93B393B493B593B693B793B893B993BA93BB93BCC3FEC4A1DFA193BD93BE93BF93C093C193C293C3C1CC93C4DEFCBEEF93C5C6B293C693C793C893C993CA93CB93CC93CD93CEB3C5C8F693CF93D0CBBADEFE93D193D2DFA493D393D493D593D6D7B293D793D893D993DA93DBB3B793DC93DD93DE93DFC1C393E093E1C7CBB2A5B4E993E2D7AB93E393E493E593E6C4EC93E7DFA2DFA393E8DFA593E9BAB393EA93EB93ECDFA693EDC0DE93EE93EFC9C393F093F193F293F393F493F593F6B2D9C7E693F7DFA793F8C7DC93F993FA93FB93FCDFA8EBA293FD93FE944094419442CBD3944394449445DFAA9446DFA99447B2C194489449944A944B944C944D944E944F9450945194529453945494559456945794589459945A945B945C945D945E945F9460C5CA94619462946394649465946694679468DFAB9469946A946B946C946D946E946F9470D4DC94719472947394749475C8C19476947794789479947A947B947C947D947E948094819482DFAC94839484948594869487BEF094889489DFADD6A7948A948B948C948DEAB7EBB6CAD5948ED8FCB8C4948FB9A594909491B7C5D5FE94929493949494959496B9CA94979498D0A7F4CD9499949AB5D0949B949CC3F4949DBEC8949E949F94A0EBB7B0BD94A194A2BDCC94A3C1B294A4B1D6B3A894A594A694A7B8D2C9A294A894A9B6D894AA94AB94AC94ADEBB8BEB494AE94AF94B0CAFD94B1C7C394B2D5FB94B394B4B7F394B594B694B794B894B994BA94BB94BC94BD94BE94BF94C094C194C294C3CEC494C494C594C6D5ABB1F394C794C894C9ECB3B0DF94CAECB594CB94CC94CDB6B794CEC1CF94CFF5FAD0B194D094D1D5E594D2CED394D394D4BDEFB3E294D5B8AB94D6D5B694D7EDBD94D8B6CF94D9CBB9D0C294DA94DB94DC94DD94DE94DF94E094E1B7BD94E294E3ECB6CAA994E494E594E6C5D494E7ECB9ECB8C2C3ECB794E894E994EA94EBD0FDECBA94ECECBBD7E594ED94EEECBC94EF94F094F1ECBDC6EC94F294F394F494F594F694F794F894F9CEDE94FABCC894FB94FCC8D5B5A9BEC9D6BCD4E794FD94FED1AED0F1EAB8EAB9EABABAB59540954195429543CAB1BFF595449545CDFA9546954795489549954AEAC0954BB0BAEABE954C954DC0A5954E954F9550EABB9551B2FD9552C3F7BBE8955395549555D2D7CEF4EABF955695579558EABC9559955A955BEAC3955CD0C7D3B3955D955E955F9560B4BA9561C3C1D7F29562956395649565D5D19566CAC79567EAC595689569EAC4EAC7EAC6956A956B956C956D956ED6E7956FCFD495709571EACB9572BBCE9573957495759576957795789579BDFAC9CE957A957BEACC957C957DC9B9CFFEEACAD4CEEACDEACF957E9580CDED9581958295839584EAC99585EACE95869587CEEE9588BBDE9589B3BF958A958B958C958D958EC6D5BEB0CEFA958F95909591C7E79592BEA7EAD095939594D6C7959595969597C1C095989599959AD4DD959BEAD1959C959DCFBE959E959F95A095A1EAD295A295A395A495A5CAEE95A695A795A895A9C5AFB0B595AA95AB95AC95AD95AEEAD495AF95B095B195B295B395B495B595B695B7EAD3F4DF95B895B995BA95BB95BCC4BA95BD95BE95BF95C095C1B1A995C295C395C495C5E5DF95C695C795C895C9EAD595CA95CB95CC95CD95CE95CF95D095D195D295D395D495D595D695D795D895D995DA95DB95DC95DD95DE95DF95E095E195E295E3CAEF95E4EAD6EAD7C6D895E595E695E795E895E995EA95EB95ECEAD895ED95EEEAD995EF95F095F195F295F395F4D4BB95F5C7FAD2B7B8FC95F695F7EAC295F8B2DC95F995FAC2FC95FBD4F8CCE6D7EE95FC95FD95FE9640964196429643D4C2D3D0EBC3C5F39644B7FE96459646EBD4964796489649CBB7EBDE964AC0CA964B964C964DCDFB964EB3AF964FC6DA965096519652965396549655EBFC9656C4BE9657CEB4C4A9B1BED4FD9658CAF59659D6EC965A965BC6D3B6E4965C965D965E965FBBFA96609661D0E096629663C9B19664D4D3C8A896659666B8CB9667E8BEC9BC96689669E8BB966AC0EED0D3B2C4B4E5966BE8BC966C966DD5C8966E966F967096719672B6C59673E8BDCAF8B8DCCCF5967496759676C0B496779678D1EEE8BFE8C29679967ABABC967BB1ADBDDC967CEABDE8C3967DE8C6967EE8CB9680968196829683E8CC9684CBC9B0E59685BCAB96869687B9B996889689E8C1968ACDF7968BE8CA968C968D968E968FCEF69690969196929693D5ED9694C1D6E8C49695C3B69696B9FBD6A6E8C8969796989699CAE0D4E6969AE8C0969BE8C5E8C7969CC7B9B7E3969DE8C9969EBFDDE8D2969F96A0E8D796A1E8D5BCDCBCCFE8DB96A296A396A496A596A696A796A896A9E8DE96AAE8DAB1FA96AB96AC96AD96AE96AF96B096B196B296B396B4B0D8C4B3B8CCC6E2C8BEC8E196B596B696B7E8CFE8D4E8D696B8B9F1E8D8D7F596B9C4FB96BAE8DC96BB96BCB2E996BD96BE96BFE8D196C096C1BCED96C296C3BFC2E8CDD6F996C4C1F8B2F196C596C696C796C896C996CA96CB96CCE8DF96CDCAC1E8D996CE96CF96D096D1D5A496D2B1EAD5BBE8CEE8D0B6B0E8D396D3E8DDC0B896D4CAF796D5CBA896D696D7C6DCC0F596D896D996DA96DB96DCE8E996DD96DE96DFD0A396E096E196E296E396E496E596E6E8F2D6EA96E796E896E996EA96EB96EC96EDE8E0E8E196EE96EF96F0D1F9BACBB8F996F196F2B8F1D4D4E8EF96F3E8EEE8ECB9F0CCD2E8E6CEA6BFF296F4B0B8E8F1E8F096F5D7C096F6E8E496F7CDA9C9A396F8BBB8BDDBE8EA96F996FA96FB96FC96FD96FE9740974197429743E8E2E8E3E8E5B5B5E8E7C7C5E8EBE8EDBDB0D7AE9744E8F897459746974797489749974A974B974CE8F5974DCDB0E8F6974E974F9750975197529753975497559756C1BA9757E8E89758C3B7B0F09759975A975B975C975D975E975F9760E8F4976197629763E8F7976497659766B9A3976797689769976A976B976C976D976E976F9770C9D2977197729773C3CECEE0C0E69774977597769777CBF39778CCDDD0B59779977ACAE1977BE8F3977C977D977E9780978197829783978497859786BCEC9787E8F997889789978A978B978C978DC3DE978EC6E5978FB9F79790979197929793B0F497949795D7D897969797BCAC9798C5EF9799979A979B979C979DCCC4979E979FE9A697A097A197A297A397A497A597A697A797A897A9C9AD97AAE9A2C0E297AB97AC97ADBFC397AE97AF97B0E8FEB9D797B1E8FB97B297B397B497B5E9A497B697B797B8D2CE97B997BA97BB97BC97BDE9A397BED6B2D7B597BFE9A797C0BDB797C197C297C397C497C597C697C797C897C997CA97CB97CCE8FCE8FD97CD97CE97CFE9A197D097D197D297D397D497D597D697D7CDD697D897D9D2AC97DA97DB97DCE9B297DD97DE97DF97E0E9A997E197E297E3B4AA97E4B4BB97E597E6E9AB97E797E897E997EA97EB97EC97ED97EE97EF97F097F197F297F397F497F597F697F7D0A897F897F9E9A597FA97FBB3FE97FC97FDE9ACC0E397FEE9AA98409841E9B998429843E9B89844984598469847E9AE98489849E8FA984A984BE9A8984C984D984E984F9850BFACE9B1E9BA98519852C2A5985398549855E9AF9856B8C59857E9AD9858D3DCE9B4E9B5E9B79859985A985BE9C7985C985D985E985F98609861C0C6E9C598629863E9B098649865E9BBB0F19866986798689869986A986B986C986D986E986FE9BCD5A598709871E9BE9872E9BF987398749875E9C198769877C1F198789879C8B6987A987B987CE9BD987D987E988098819882E9C29883988498859886988798889889988AE9C3988BE9B3988CE9B6988DBBB1988E988F9890E9C0989198929893989498959896BCF7989798989899E9C4E9C6989A989B989C989D989E989F98A098A198A298A398A498A5E9CA98A698A798A898A9E9CE98AA98AB98AC98AD98AE98AF98B098B198B298B3B2DB98B4E9C898B598B698B798B898B998BA98BB98BC98BD98BEB7AE98BF98C098C198C298C398C498C598C698C798C898C998CAE9CBE9CC98CB98CC98CD98CE98CF98D0D5C198D1C4A398D298D398D498D598D698D7E9D898D8BAE198D998DA98DB98DCE9C998DDD3A398DE98DF98E0E9D498E198E298E398E498E598E698E7E9D7E9D098E898E998EA98EB98ECE9CF98ED98EEC7C198EF98F098F198F298F398F498F598F6E9D298F798F898F998FA98FB98FC98FDE9D9B3C898FEE9D399409941994299439944CFF0994599469947E9CD99489949994A994B994C994D994E994F995099519952B3F79953995499559956995799589959E9D6995A995BE9DA995C995D995ECCB4995F99609961CFAD99629963996499659966996799689969996AE9D5996BE9DCE9DB996C996D996E996F9970E9DE99719972997399749975997699779978E9D19979997A997B997C997D997E99809981E9DD9982E9DFC3CA9983998499859986998799889989998A998B998C998D998E998F9990999199929993999499959996999799989999999A999B999C999D999E999F99A099A199A299A399A499A599A699A799A899A999AA99AB99AC99AD99AE99AF99B099B199B299B399B499B599B699B799B899B999BA99BB99BC99BD99BE99BF99C099C199C299C399C499C599C699C799C899C999CA99CB99CC99CD99CE99CF99D099D199D299D399D499D599D699D799D899D999DA99DB99DC99DD99DE99DF99E099E199E299E399E499E599E699E799E899E999EA99EB99EC99ED99EE99EF99F099F199F299F399F499F5C7B7B4CEBBB6D0C0ECA399F699F7C5B799F899F999FA99FB99FC99FD99FE9A409A419A42D3FB9A439A449A459A46ECA49A47ECA5C6DB9A489A499A4ABFEE9A4B9A4C9A4D9A4EECA69A4F9A50ECA7D0AA9A51C7B89A529A53B8E89A549A559A569A579A589A599A5A9A5B9A5C9A5D9A5E9A5FECA89A609A619A629A639A649A659A669A67D6B9D5FDB4CBB2BDCEE4C6E79A689A69CDE19A6A9A6B9A6C9A6D9A6E9A6F9A709A719A729A739A749A759A769A77B4F59A78CBC0BCDF9A799A7A9A7B9A7CE9E2E9E3D1EAE9E59A7DB4F9E9E49A7ED1B3CAE2B2D09A80E9E89A819A829A839A84E9E6E9E79A859A86D6B39A879A889A89E9E9E9EA9A8A9A8B9A8C9A8D9A8EE9EB9A8F9A909A919A929A939A949A959A96E9EC9A979A989A999A9A9A9B9A9C9A9D9A9EECAFC5B9B6CE9A9FD2F39AA09AA19AA29AA39AA49AA59AA6B5EE9AA7BBD9ECB19AA89AA9D2E39AAA9AAB9AAC9AAD9AAECEE39AAFC4B89AB0C3BF9AB19AB2B6BED8B9B1C8B1CFB1D1C5FE9AB3B1D09AB4C3AB9AB59AB69AB79AB89AB9D5B19ABA9ABB9ABC9ABD9ABE9ABF9AC09AC1EBA4BAC19AC29AC39AC4CCBA9AC59AC69AC7EBA59AC8EBA79AC99ACA9ACBEBA89ACC9ACD9ACEEBA69ACF9AD09AD19AD29AD39AD49AD5EBA9EBABEBAA9AD69AD79AD89AD99ADAEBAC9ADBCACFD8B5C3F19ADCC3A5C6F8EBADC4CA9ADDEBAEEBAFEBB0B7D59ADE9ADF9AE0B7FA9AE1EBB1C7E29AE2EBB39AE3BAA4D1F5B0B1EBB2EBB49AE49AE59AE6B5AAC2C8C7E89AE7EBB59AE8CBAEE3DF9AE99AEAD3C09AEB9AEC9AED9AEED9DB9AEF9AF0CDA1D6ADC7F39AF19AF29AF3D9E0BBE39AF4BABAE3E29AF59AF69AF79AF89AF9CFAB9AFA9AFB9AFCE3E0C9C79AFDBAB99AFE9B409B41D1B4E3E1C8EAB9AFBDADB3D8CEDB9B429B43CCC09B449B459B46E3E8E3E9CDF49B479B489B499B4A9B4BCCAD9B4CBCB39B4DE3EA9B4EE3EB9B4F9B50D0DA9B519B529B53C6FBB7DA9B549B55C7DFD2CACED69B56E3E4E3EC9B57C9F2B3C19B589B59E3E79B5A9B5BC6E3E3E59B5C9B5DEDB3E3E69B5E9B5F9B609B61C9B39B62C5E69B639B649B65B9B59B66C3BB9B67E3E3C5BDC1A4C2D9B2D79B68E3EDBBA6C4AD9B69E3F0BEDA9B6A9B6BE3FBE3F5BAD39B6C9B6D9B6E9B6FB7D0D3CD9B70D6CED5D3B9C1D5B4D1D89B719B729B739B74D0B9C7F69B759B769B77C8AAB2B49B78C3DA9B799B7A9B7BE3EE9B7C9B7DE3FCE3EFB7A8E3F7E3F49B7E9B809B81B7BA9B829B83C5A29B84E3F6C5DDB2A8C6FC9B85C4E09B869B87D7A29B88C0E1E3F99B899B8AE3FAE3FDCCA9E3F39B8BD3BE9B8CB1C3EDB4E3F1E3F29B8DE3F8D0BAC6C3D4F3E3FE9B8E9B8FBDE09B909B91E4A79B929B93E4A69B949B959B96D1F3E4A39B97E4A99B989B999B9AC8F79B9B9B9C9B9D9B9ECFB49B9FE4A8E4AEC2E59BA09BA1B6B49BA29BA39BA49BA59BA69BA7BDF29BA8E4A29BA99BAABAE9E4AA9BAB9BACE4AC9BAD9BAEB6FDD6DEE4B29BAFE4AD9BB09BB19BB2E4A19BB3BBEECDDDC7A2C5C99BB49BB5C1F79BB6E4A49BB7C7B3BDACBDBDE4A59BB8D7C7B2E29BB9E4ABBCC3E4AF9BBABBEBE4B0C5A8E4B19BBB9BBC9BBD9BBED5E3BFA39BBFE4BA9BC0E4B79BC1E4BB9BC29BC3E4BD9BC49BC5C6D69BC69BC7BAC6C0CB9BC89BC99BCAB8A1E4B49BCB9BCC9BCD9BCED4A19BCF9BD0BAA3BDFE9BD19BD29BD3E4BC9BD49BD59BD69BD79BD8CDBF9BD99BDAC4F99BDB9BDCCFFBC9E69BDD9BDED3BF9BDFCFD19BE09BE1E4B39BE2E4B8E4B9CCE99BE39BE49BE59BE69BE7CCCE9BE8C0D4E4B5C1B0E4B6CED09BE9BBC1B5D39BEAC8F3BDA7D5C7C9ACB8A2E4CA9BEB9BECE4CCD1C49BED9BEED2BA9BEF9BF0BAAD9BF19BF2BAD49BF39BF49BF59BF69BF79BF8E4C3B5ED9BF99BFA9BFBD7CDE4C0CFFDE4BF9BFC9BFD9BFEC1DCCCCA9C409C419C429C43CAE79C449C459C469C47C4D79C48CCD4E4C89C499C4A9C4BE4C7E4C19C4CE4C4B5AD9C4D9C4ED3D99C4FE4C69C509C519C529C53D2F9B4E39C54BBB49C559C56C9EE9C57B4BE9C589C599C5ABBEC9C5BD1CD9C5CCCEDEDB59C5D9C5E9C5F9C609C619C629C639C64C7E59C659C669C679C68D4A89C69E4CBD7D5E4C29C6ABDA5E4C59C6B9C6CD3E69C6DE4C9C9F89C6E9C6FE4BE9C709C71D3E59C729C73C7FEB6C99C74D4FCB2B3E4D79C759C769C77CEC29C78E4CD9C79CEBC9C7AB8DB9C7B9C7CE4D69C7DBFCA9C7E9C809C81D3CE9C82C3EC9C839C849C859C869C879C889C899C8AC5C8E4D89C8B9C8C9C8D9C8E9C8F9C909C919C92CDC4E4CF9C939C949C959C96E4D4E4D59C97BAFE9C98CFE69C999C9AD5BF9C9B9C9C9C9DE4D29C9E9C9F9CA09CA19CA29CA39CA49CA59CA69CA79CA8E4D09CA99CAAE4CE9CAB9CAC9CAD9CAE9CAF9CB09CB19CB29CB39CB49CB59CB69CB79CB89CB9CDE5CAAA9CBA9CBB9CBCC0A39CBDBDA6E4D39CBE9CBFB8C89CC09CC19CC29CC39CC4E4E7D4B49CC59CC69CC79CC89CC99CCA9CCBE4DB9CCC9CCD9CCEC1EF9CCF9CD0E4E99CD19CD2D2E79CD39CD4E4DF9CD5E4E09CD69CD7CFAA9CD89CD99CDA9CDBCBDD9CDCE4DAE4D19CDDE4E59CDEC8DCE4E39CDF9CE0C4E7E4E29CE1E4E19CE29CE39CE4B3FCE4E89CE59CE69CE79CE8B5E19CE99CEA9CEBD7CC9CEC9CED9CEEE4E69CEFBBAC9CF0D7D2CCCFEBF89CF1E4E49CF29CF3B9F69CF49CF59CF6D6CDE4D9E4DCC2FAE4DE9CF7C2CBC0C4C2D09CF8B1F5CCB29CF99CFA9CFB9CFC9CFD9CFE9D409D419D429D43B5CE9D449D459D469D47E4EF9D489D499D4A9D4B9D4C9D4D9D4E9D4FC6AF9D509D519D52C6E19D539D54E4F59D559D569D579D589D59C2A99D5A9D5B9D5CC0ECD1DDE4EE9D5D9D5E9D5F9D609D619D629D639D649D659D66C4AE9D679D689D69E4ED9D6A9D6B9D6C9D6DE4F6E4F4C2FE9D6EE4DD9D6FE4F09D70CAFE9D71D5C49D729D73E4F19D749D759D769D779D789D799D7AD1FA9D7B9D7C9D7D9D7E9D809D819D82E4EBE4EC9D839D849D85E4F29D86CEAB9D879D889D899D8A9D8B9D8C9D8D9D8E9D8F9D90C5CB9D919D929D93C7B19D94C2BA9D959D969D97E4EA9D989D999D9AC1CA9D9B9D9C9D9D9D9E9D9F9DA0CCB6B3B19DA19DA29DA3E4FB9DA4E4F39DA59DA69DA7E4FA9DA8E4FD9DA9E4FC9DAA9DAB9DAC9DAD9DAE9DAF9DB0B3CE9DB19DB29DB3B3BAE4F79DB49DB5E4F9E4F8C5EC9DB69DB79DB89DB99DBA9DBB9DBC9DBD9DBE9DBF9DC09DC19DC2C0BD9DC39DC49DC59DC6D4E89DC79DC89DC99DCA9DCBE5A29DCC9DCD9DCE9DCF9DD09DD19DD29DD39DD49DD59DD6B0C49DD79DD8E5A49DD99DDAE5A39DDB9DDC9DDD9DDE9DDF9DE0BCA49DE1E5A59DE29DE39DE49DE59DE69DE7E5A19DE89DE99DEA9DEB9DEC9DED9DEEE4FEB1F49DEF9DF09DF19DF29DF39DF49DF59DF69DF79DF89DF9E5A89DFAE5A9E5A69DFB9DFC9DFD9DFE9E409E419E429E439E449E459E469E47E5A7E5AA9E489E499E4A9E4B9E4C9E4D9E4E9E4F9E509E519E529E539E549E559E569E579E589E599E5A9E5B9E5C9E5D9E5E9E5F9E609E619E629E639E649E659E669E679E68C6D99E699E6A9E6B9E6C9E6D9E6E9E6F9E70E5ABE5AD9E719E729E739E749E759E769E77E5AC9E789E799E7A9E7B9E7C9E7D9E7E9E809E819E829E839E849E859E869E879E889E89E5AF9E8A9E8B9E8CE5AE9E8D9E8E9E8F9E909E919E929E939E949E959E969E979E989E999E9A9E9B9E9C9E9D9E9EB9E09E9F9EA0E5B09EA19EA29EA39EA49EA59EA69EA79EA89EA99EAA9EAB9EAC9EAD9EAEE5B19EAF9EB09EB19EB29EB39EB49EB59EB69EB79EB89EB99EBABBF0ECE1C3F09EBBB5C6BBD29EBC9EBD9EBE9EBFC1E9D4EE9EC0BEC49EC19EC29EC3D7C69EC4D4D6B2D3ECBE9EC59EC69EC79EC8EAC19EC99ECA9ECBC2AFB4B69ECC9ECD9ECED1D79ECF9ED09ED1B3B49ED2C8B2BFBBECC09ED39ED4D6CB9ED59ED6ECBFECC19ED79ED89ED99EDA9EDB9EDC9EDD9EDE9EDF9EE09EE19EE29EE3ECC5BEE6CCBFC5DABEBC9EE4ECC69EE5B1FE9EE69EE79EE8ECC4D5A8B5E39EE9ECC2C1B6B3E39EEA9EEBECC3CBB8C0C3CCFE9EEC9EED9EEE9EEFC1D29EF0ECC89EF19EF29EF39EF49EF59EF69EF79EF89EF99EFA9EFB9EFC9EFDBAE6C0D39EFED6F29F409F419F42D1CC9F439F449F459F46BFBE9F47B7B3C9D5ECC7BBE29F48CCCCBDFDC8C89F49CFA99F4A9F4B9F4C9F4D9F4E9F4F9F50CDE99F51C5EB9F529F539F54B7E99F559F569F579F589F599F5A9F5B9F5C9F5D9F5E9F5FD1C9BAB89F609F619F629F639F64ECC99F659F66ECCA9F67BBC0ECCB9F68ECE2B1BAB7D99F699F6A9F6B9F6C9F6D9F6E9F6F9F709F719F729F73BDB99F749F759F769F779F789F799F7A9F7BECCCD1E6ECCD9F7C9F7D9F7E9F80C8BB9F819F829F839F849F859F869F879F889F899F8A9F8B9F8C9F8D9F8EECD19F8F9F909F919F92ECD39F93BBCD9F94BCE59F959F969F979F989F999F9A9F9B9F9C9F9D9F9E9F9F9FA09FA1ECCF9FA2C9B79FA39FA49FA59FA69FA7C3BA9FA8ECE3D5D5ECD09FA99FAA9FAB9FAC9FADD6F39FAE9FAF9FB0ECD2ECCE9FB19FB29FB39FB4ECD49FB5ECD59FB69FB7C9BF9FB89FB99FBA9FBB9FBC9FBDCFA89FBE9FBF9FC09FC19FC2D0DC9FC39FC49FC59FC6D1AC9FC79FC89FC99FCAC8DB9FCB9FCC9FCDECD6CEF59FCE9FCF9FD09FD19FD2CAECECDA9FD39FD49FD59FD69FD79FD89FD9ECD99FDA9FDB9FDCB0BE9FDD9FDE9FDF9FE09FE19FE2ECD79FE3ECD89FE49FE59FE6ECE49FE79FE89FE99FEA9FEB9FEC9FED9FEE9FEFC8BC9FF09FF19FF29FF39FF49FF59FF69FF79FF89FF9C1C79FFA9FFB9FFC9FFD9FFEECDCD1E0A040A041A042A043A044A045A046A047A048A049ECDBA04AA04BA04CA04DD4EFA04EECDDA04FA050A051A052A053A054DBC6A055A056A057A058A059A05AA05BA05CA05DA05EECDEA05FA060A061A062A063A064A065A066A067A068A069A06AB1ACA06BA06CA06DA06EA06FA070A071A072A073A074A075A076A077A078A079A07AA07BA07CA07DA07EA080A081ECDFA082A083A084A085A086A087A088A089A08AA08BECE0A08CD7A6A08DC5C0A08EA08FA090EBBCB0AEA091A092A093BEF4B8B8D2AFB0D6B5F9A094D8B3A095CBACA096E3DDA097A098A099A09AA09BA09CA09DC6ACB0E6A09EA09FA0A0C5C6EBB9A0A1A0A2A0A3A0A4EBBAA0A5A0A6A0A7EBBBA0A8A0A9D1C0A0AAC5A3A0ABEAF2A0ACC4B2A0ADC4B5C0CEA0AEA0AFA0B0EAF3C4C1A0B1CEEFA0B2A0B3A0B4A0B5EAF0EAF4A0B6A0B7C9FCA0B8A0B9C7A3A0BAA0BBA0BCCCD8CEFEA0BDA0BEA0BFEAF5EAF6CFACC0E7A0C0A0C1EAF7A0C2A0C3A0C4A0C5A0C6B6BFEAF8A0C7EAF9A0C8EAFAA0C9A0CAEAFBA0CBA0CCA0CDA0CEA0CFA0D0A0D1A0D2A0D3A0D4A0D5A0D6EAF1A0D7A0D8A0D9A0DAA0DBA0DCA0DDA0DEA0DFA0E0A0E1A0E2C8AEE1EBA0E3B7B8E1ECA0E4A0E5A0E6E1EDA0E7D7B4E1EEE1EFD3CCA0E8A0E9A0EAA0EBA0ECA0EDA0EEE1F1BFF1E1F0B5D2A0EFA0F0A0F1B1B7A0F2A0F3A0F4A0F5E1F3E1F2A0F6BAFCA0F7E1F4A0F8A0F9A0FAA0FBB9B7A0FCBED1A0FDA0FEAA40AA41C4FCAA42BADDBDC6AA43AA44AA45AA46AA47AA48E1F5E1F7AA49AA4AB6C0CFC1CAA8E1F6D5F8D3FCE1F8E1FCE1F9AA4BAA4CE1FAC0EAAA4DE1FEE2A1C0C7AA4EAA4FAA50AA51E1FBAA52E1FDAA53AA54AA55AA56AA57AA58E2A5AA59AA5AAA5BC1D4AA5CAA5DAA5EAA5FE2A3AA60E2A8B2FEE2A2AA61AA62AA63C3CDB2C2E2A7E2A6AA64AA65E2A4E2A9AA66AA67E2ABAA68AA69AA6AD0C9D6EDC3A8E2ACAA6BCFD7AA6CAA6DE2AEAA6EAA6FBAEFAA70AA71E9E0E2ADE2AAAA72AA73AA74AA75BBABD4B3AA76AA77AA78AA79AA7AAA7BAA7CAA7DAA7EAA80AA81AA82AA83E2B0AA84AA85E2AFAA86E9E1AA87AA88AA89AA8AE2B1AA8BAA8CAA8DAA8EAA8FAA90AA91AA92E2B2AA93AA94AA95AA96AA97AA98AA99AA9AAA9BAA9CAA9DE2B3CCA1AA9EE2B4AA9FAAA0AB40AB41AB42AB43AB44AB45AB46AB47AB48AB49AB4AAB4BE2B5AB4CAB4DAB4EAB4FAB50D0FEAB51AB52C2CAAB53D3F1AB54CDF5AB55AB56E7E0AB57AB58E7E1AB59AB5AAB5BAB5CBEC1AB5DAB5EAB5FAB60C2EAAB61AB62AB63E7E4AB64AB65E7E3AB66AB67AB68AB69AB6AAB6BCDE6AB6CC3B5AB6DAB6EE7E2BBB7CFD6AB6FC1E1E7E9AB70AB71AB72E7E8AB73AB74E7F4B2A3AB75AB76AB77AB78E7EAAB79E7E6AB7AAB7BAB7CAB7DAB7EE7ECE7EBC9BAAB80AB81D5E4AB82E7E5B7A9E7E7AB83AB84AB85AB86AB87AB88AB89E7EEAB8AAB8BAB8CAB8DE7F3AB8ED6E9AB8FAB90AB91AB92E7EDAB93E7F2AB94E7F1AB95AB96AB97B0E0AB98AB99AB9AAB9BE7F5AB9CAB9DAB9EAB9FABA0AC40AC41AC42AC43AC44AC45AC46AC47AC48AC49AC4AC7F2AC4BC0C5C0EDAC4CAC4DC1F0E7F0AC4EAC4FAC50AC51E7F6CBF6AC52AC53AC54AC55AC56AC57AC58AC59AC5AE8A2E8A1AC5BAC5CAC5DAC5EAC5FAC60D7C1AC61AC62E7FAE7F9AC63E7FBAC64E7F7AC65E7FEAC66E7FDAC67E7FCAC68AC69C1D5C7D9C5FDC5C3AC6AAC6BAC6CAC6DAC6EC7EDAC6FAC70AC71AC72E8A3AC73AC74AC75AC76AC77AC78AC79AC7AAC7BAC7CAC7DAC7EAC80AC81AC82AC83AC84AC85AC86E8A6AC87E8A5AC88E8A7BAF7E7F8E8A4AC89C8F0C9AAAC8AAC8BAC8CAC8DAC8EAC8FAC90AC91AC92AC93AC94AC95AC96E8A9AC97AC98B9E5AC99AC9AAC9BAC9CAC9DD1FEE8A8AC9EAC9FACA0AD40AD41AD42E8AAAD43E8ADE8AEAD44C1A7AD45AD46AD47E8AFAD48AD49AD4AE8B0AD4BAD4CE8ACAD4DE8B4AD4EAD4FAD50AD51AD52AD53AD54AD55AD56AD57AD58E8ABAD59E8B1AD5AAD5BAD5CAD5DAD5EAD5FAD60AD61E8B5E8B2E8B3AD62AD63AD64AD65AD66AD67AD68AD69AD6AAD6BAD6CAD6DAD6EAD6FAD70AD71E8B7AD72AD73AD74AD75AD76AD77AD78AD79AD7AAD7BAD7CAD7DAD7EAD80AD81AD82AD83AD84AD85AD86AD87AD88AD89E8B6AD8AAD8BAD8CAD8DAD8EAD8FAD90AD91AD92B9CFAD93F0ACAD94F0ADAD95C6B0B0EAC8BFAD96CDDFAD97AD98AD99AD9AAD9BAD9CAD9DCECDEAB1AD9EAD9FADA0AE40EAB2AE41C6BFB4C9AE42AE43AE44AE45AE46AE47AE48EAB3AE49AE4AAE4BAE4CD5E7AE4DAE4EAE4FAE50AE51AE52AE53AE54DDF9AE55EAB4AE56EAB5AE57EAB6AE58AE59AE5AAE5BB8CADFB0C9F5AE5CCCF0AE5DAE5EC9FAAE5FAE60AE61AE62AE63C9FBAE64AE65D3C3CBA6AE66B8A6F0AEB1C2AE67E5B8CCEFD3C9BCD7C9EAAE68B5E7AE69C4D0B5E9AE6AEEAEBBADAE6BAE6CE7DEAE6DEEAFAE6EAE6FAE70AE71B3A9AE72AE73EEB2AE74AE75EEB1BDE7AE76EEB0CEB7AE77AE78AE79AE7AC5CFAE7BAE7CAE7DAE7EC1F4DBCEEEB3D0F3AE80AE81AE82AE83AE84AE85AE86AE87C2D4C6E8AE88AE89AE8AB7ACAE8BAE8CAE8DAE8EAE8FAE90AE91EEB4AE92B3EBAE93AE94AE95BBFBEEB5AE96AE97AE98AE99AE9AE7DCAE9BAE9CAE9DEEB6AE9EAE9FBDAEAEA0AF40AF41AF42F1E2AF43AF44AF45CAE8AF46D2C9F0DAAF47F0DBAF48F0DCC1C6AF49B8EDBECEAF4AAF4BF0DEAF4CC5B1F0DDD1F1AF4DF0E0B0CCBDEAAF4EAF4FAF50AF51AF52D2DFF0DFAF53B4AFB7E8F0E6F0E5C6A3F0E1F0E2B4C3AF54AF55F0E3D5EEAF56AF57CCDBBED2BCB2AF58AF59AF5AF0E8F0E7F0E4B2A1AF5BD6A2D3B8BEB7C8ACAF5CAF5DF0EAAF5EAF5FAF60AF61D1F7AF62D6CCBADBF0E9AF63B6BBAF64AF65CDB4AF66AF67C6A6AF68AF69AF6AC1A1F0EBF0EEAF6BF0EDF0F0F0ECAF6CBBBEF0EFAF6DAF6EAF6FAF70CCB5F0F2AF71AF72B3D5AF73AF74AF75AF76B1D4AF77AF78F0F3AF79AF7AF0F4F0F6B4E1AF7BF0F1AF7CF0F7AF7DAF7EAF80AF81F0FAAF82F0F8AF83AF84AF85F0F5AF86AF87AF88AF89F0FDAF8AF0F9F0FCF0FEAF8BF1A1AF8CAF8DAF8ECEC1F1A4AF8FF1A3AF90C1F6F0FBCADDAF91AF92B4F1B1F1CCB1AF93F1A6AF94AF95F1A7AF96AF97F1ACD5CEF1A9AF98AF99C8B3AF9AAF9BAF9CF1A2AF9DF1ABF1A8F1A5AF9EAF9FF1AAAFA0B040B041B042B043B044B045B046B0A9F1ADB047B048B049B04AB04BB04CF1AFB04DF1B1B04EB04FB050B051B052F1B0B053F1AEB054B055B056B057D1A2B058B059B05AB05BB05CB05DB05EF1B2B05FB060B061F1B3B062B063B064B065B066B067B068B069B9EFB06AB06BB5C7B06CB0D7B0D9B06DB06EB06FD4EDB070B5C4B071BDD4BBCAF0A7B072B073B8DEB074B075F0A8B076B077B0A8B078F0A9B079B07ACDEEB07BB07CF0AAB07DB07EB080B081B082B083B084B085B086B087F0ABB088B089B08AB08BB08CB08DB08EB08FB090C6A4B091B092D6E5F1E4B093F1E5B094B095B096B097B098B099B09AB09BB09CB09DC3F3B09EB09FD3DBB0A0B140D6D1C5E8B141D3AFB142D2E6B143B144EEC1B0BBD5B5D1CEBCE0BAD0B145BFF8B146B8C7B5C1C5CCB147B148CAA2B149B14AB14BC3CBB14CB14DB14EB14FB150EEC2B151B152B153B154B155B156B157B158C4BFB6A2B159EDECC3A4B15AD6B1B15BB15CB15DCFE0EDEFB15EB15FC5CEB160B6DCB161B162CAA1B163B164EDEDB165B166EDF0EDF1C3BCB167BFB4B168EDEEB169B16AB16BB16CB16DB16EB16FB170B171B172B173EDF4EDF2B174B175B176B177D5E6C3DFB178EDF3B179B17AB17BEDF6B17CD5A3D1A3B17DB17EB180EDF5B181C3D0B182B183B184B185B186EDF7BFF4BEECEDF8B187CCF7B188D1DBB189B18AB18BD7C5D5F6B18CEDFCB18DB18EB18FEDFBB190B191B192B193B194B195B196B197EDF9EDFAB198B199B19AB19BB19CB19DB19EB19FEDFDBEA6B1A0B240B241B242B243CBAFEEA1B6BDB244EEA2C4C0B245EDFEB246B247BDDEB2C7B248B249B24AB24BB24CB24DB24EB24FB250B251B252B253B6C3B254B255B256EEA5D8BAEEA3EEA6B257B258B259C3E9B3F2B25AB25BB25CB25DB25EB25FEEA7EEA4CFB9B260B261EEA8C2F7B262B263B264B265B266B267B268B269B26AB26BB26CB26DEEA9EEAAB26EDEABB26FB270C6B3B271C7C6B272D6F5B5C9B273CBB2B274B275B276EEABB277B278CDABB279EEACB27AB27BB27CB27DB27ED5B0B280EEADB281F6C4B282B283B284B285B286B287B288B289B28AB28BB28CB28DB28EDBC7B28FB290B291B292B293B294B295B296B297B4A3B298B299B29AC3ACF1E6B29BB29CB29DB29EB29FCAB8D2D3B2A0D6AAB340EFF2B341BED8B342BDC3EFF3B6CCB0ABB343B344B345B346CAAFB347B348EDB6B349EDB7B34AB34BB34CB34DCEF9B7AFBFF3EDB8C2EBC9B0B34EB34FB350B351B352B353EDB9B354B355C6F6BFB3B356B357B358EDBCC5F8B359D1D0B35AD7A9EDBAEDBBB35BD1E2B35CEDBFEDC0B35DEDC4B35EB35FB360EDC8B361EDC6EDCED5E8B362EDC9B363B364EDC7EDBEB365B366C5E9B367B368B369C6C6B36AB36BC9E9D4D2EDC1EDC2EDC3EDC5B36CC0F9B36DB4A1B36EB36FB370B371B9E8B372EDD0B373B374B375B376EDD1B377EDCAB378EDCFB379CEF8B37AB37BCBB6EDCCEDCDB37CB37DB37EB380B381CFF5B382B383B384B385B386B387B388B389B38AB38BB38CB38DEDD2C1F2D3B2EDCBC8B7B38EB38FB390B391B392B393B394B395BCEFB396B397B398B399C5F0B39AB39BB39CB39DB39EB39FB3A0B440B441B442EDD6B443B5EFB444B445C2B5B0ADCBE9B446B447B1AEB448EDD4B449B44AB44BCDEBB5E2B44CEDD5EDD3EDD7B44DB44EB5FAB44FEDD8B450EDD9B451EDDCB452B1CCB453B454B455B456B457B458B459B45AC5F6BCEEEDDACCBCB2EAB45BB45CB45DB45EEDDBB45FB460B461B462C4EBB463B464B4C5B465B466B467B0F5B468B469B46AEDDFC0DAB4E8B46BB46CB46DB46EC5CDB46FB470B471EDDDBFC4B472B473B474EDDEB475B476B477B478B479B47AB47BB47CB47DB47EB480B481B482B483C4A5B484B485B486EDE0B487B488B489B48AB48BEDE1B48CEDE3B48DB48EC1D7B48FB490BBC7B491B492B493B494B495B496BDB8B497B498B499EDE2B49AB49BB49CB49DB49EB49FB4A0B540B541B542B543B544B545EDE4B546B547B548B549B54AB54BB54CB54DB54EB54FEDE6B550B551B552B553B554EDE5B555B556B557B558B559B55AB55BB55CB55DB55EB55FB560B561B562B563EDE7B564B565B566B567B568CABEECEAC0F1B569C9E7B56AECEBC6EEB56BB56CB56DB56EECECB56FC6EDECEDB570B571B572B573B574B575B576B577B578ECF0B579B57AD7E6ECF3B57BB57CECF1ECEEECEFD7A3C9F1CBEEECF4B57DECF2B57EB580CFE9B581ECF6C6B1B582B583B584B585BCC0B586ECF5B587B588B589B58AB58BB58CB58DB5BBBBF6B58EECF7B58FB590B591B592B593D9F7BDFBB594B595C2BBECF8B596B597B598B599ECF9B59AB59BB59CB59DB8A3B59EB59FB5A0B640B641B642B643B644B645B646ECFAB647B648B649B64AB64BB64CB64DB64EB64FB650B651B652ECFBB653B654B655B656B657B658B659B65AB65BB65CB65DECFCB65EB65FB660B661B662D3EDD8AEC0EBB663C7DDBACCB664D0E3CBBDB665CDBAB666B667B8D1B668B669B1FCB66AC7EFB66BD6D6B66CB66DB66EBFC6C3EBB66FB670EFF5B671B672C3D8B673B674B675B676B677B678D7E2B679B67AB67BEFF7B3D3B67CC7D8D1EDB67DD6C8B67EEFF8B680EFF6B681BBFDB3C6B682B683B684B685B686B687B688BDD5B689B68AD2C6B68BBBE0B68CB68DCFA1B68EEFFCEFFBB68FB690EFF9B691B692B693B694B3CCB695C9D4CBB0B696B697B698B699B69AEFFEB69BB69CB0DEB69DB69ED6C9B69FB6A0B740EFFDB741B3EDB742B743F6D5B744B745B746B747B748B749B74AB74BB74CB74DB74EB74FB750B751B752CEC8B753B754B755F0A2B756F0A1B757B5BEBCDABBFCB758B8E5B759B75AB75BB75CB75DB75EC4C2B75FB760B761B762B763B764B765B766B767B768F0A3B769B76AB76BB76CB76DCBEBB76EB76FB770B771B772B773B774B775B776B777B778B779B77AB77BB77CB77DB77EB780B781B782B783B784B785B786F0A6B787B788B789D1A8B78ABEBFC7EEF1B6F1B7BFD5B78BB78CB78DB78EB4A9F1B8CDBBB78FC7D4D5ADB790F1B9B791F1BAB792B793B794B795C7CFB796B797B798D2A4D6CFB799B79AF1BBBDD1B4B0BEBDB79BB79CB79DB4DCCED1B79EBFDFF1BDB79FB7A0B840B841BFFAF1BCB842F1BFB843B844B845F1BEF1C0B846B847B848B849B84AF1C1B84BB84CB84DB84EB84FB850B851B852B853B854B855C1FEB856B857B858B859B85AB85BB85CB85DB85EB85FB860C1A2B861B862B863B864B865B866B867B868B869B86ACAFAB86BB86CD5BEB86DB86EB86FB870BEBABEB9D5C2B871B872BFA2B873CDAFF1B5B874B875B876B877B878B879BDDFB87AB6CBB87BB87CB87DB87EB880B881B882B883B884D6F1F3C3B885B886F3C4B887B8CDB888B889B88AF3C6F3C7B88BB0CAB88CF3C5B88DF3C9CBF1B88EB88FB890F3CBB891D0A6B892B893B1CAF3C8B894B895B896F3CFB897B5D1B898B899F3D7B89AF3D2B89BB89CB89DF3D4F3D3B7FBB89EB1BFB89FF3CEF3CAB5DAB8A0F3D0B940B941F3D1B942F3D5B943B944B945B946F3CDB947BCE3B948C1FDB949F3D6B94AB94BB94CB94DB94EB94FF3DAB950F3CCB951B5C8B952BDEEF3DCB953B954B7A4BFF0D6FECDB2B955B4F0B956B2DFB957F3D8B958F3D9C9B8B959F3DDB95AB95BF3DEB95CF3E1B95DB95EB95FB960B961B962B963B964B965B966B967F3DFB968B969F3E3F3E2B96AB96BF3DBB96CBFEAB96DB3EFB96EF3E0B96FB970C7A9B971BCF2B972B973B974B975F3EBB976B977B978B979B97AB97BB97CB9BFB97DB97EF3E4B980B981B982B2ADBBFEB983CBE3B984B985B986B987F3EDF3E9B988B989B98AB9DCF3EEB98BB98CB98DF3E5F3E6F3EAC2E1F3ECF3EFF3E8BCFDB98EB98FB990CFE4B991B992F3F0B993B994B995F3E7B996B997B998B999B99AB99BB99CB99DF3F2B99EB99FB9A0BA40D7ADC6AABA41BA42BA43BA44F3F3BA45BA46BA47BA48F3F1BA49C2A8BA4ABA4BBA4CBA4DBA4EB8DDF3F5BA4FBA50F3F4BA51BA52BA53B4DBBA54BA55BA56F3F6F3F7BA57BA58BA59F3F8BA5ABA5BBA5CC0BABA5DBA5EC0E9BA5FBA60BA61BA62BA63C5F1BA64BA65BA66BA67F3FBBA68F3FABA69BA6ABA6BBA6CBA6DBA6EBA6FBA70B4D8BA71BA72BA73F3FEF3F9BA74BA75F3FCBA76BA77BA78BA79BA7ABA7BF3FDBA7CBA7DBA7EBA80BA81BA82BA83BA84F4A1BA85BA86BA87BA88BA89BA8AF4A3BBC9BA8BBA8CF4A2BA8DBA8EBA8FBA90BA91BA92BA93BA94BA95BA96BA97BA98BA99F4A4BA9ABA9BBA9CBA9DBA9EBA9FB2BEF4A6F4A5BAA0BB40BB41BB42BB43BB44BB45BB46BB47BB48BB49BCAEBB4ABB4BBB4CBB4DBB4EBB4FBB50BB51BB52BB53BB54BB55BB56BB57BB58BB59BB5ABB5BBB5CBB5DBB5EBB5FBB60BB61BB62BB63BB64BB65BB66BB67BB68BB69BB6ABB6BBB6CBB6DBB6EC3D7D9E1BB6FBB70BB71BB72BB73BB74C0E0F4CCD7D1BB75BB76BB77BB78BB79BB7ABB7BBB7CBB7DBB7EBB80B7DBBB81BB82BB83BB84BB85BB86BB87F4CEC1A3BB88BB89C6C9BB8AB4D6D5B3BB8BBB8CBB8DF4D0F4CFF4D1CBDABB8EBB8FF4D2BB90D4C1D6E0BB91BB92BB93BB94B7E0BB95BB96BB97C1B8BB98BB99C1BBF4D3BEACBB9ABB9BBB9CBB9DBB9EB4E2BB9FBBA0F4D4F4D5BEABBC40BC41F4D6BC42BC43BC44F4DBBC45F4D7F4DABC46BAFDBC47F4D8F4D9BC48BC49BC4ABC4BBC4CBC4DBC4EB8E2CCC7F4DCBC4FB2DABC50BC51C3D3BC52BC53D4E3BFB7BC54BC55BC56BC57BC58BC59BC5AF4DDBC5BBC5CBC5DBC5EBC5FBC60C5B4BC61BC62BC63BC64BC65BC66BC67BC68F4E9BC69BC6ACFB5BC6BBC6CBC6DBC6EBC6FBC70BC71BC72BC73BC74BC75BC76BC77BC78CEC9BC79BC7ABC7BBC7CBC7DBC7EBC80BC81BC82BC83BC84BC85BC86BC87BC88BC89BC8ABC8BBC8CBC8DBC8ECBD8BC8FCBF7BC90BC91BC92BC93BDF4BC94BC95BC96D7CFBC97BC98BC99C0DBBC9ABC9BBC9CBC9DBC9EBC9FBCA0BD40BD41BD42BD43BD44BD45BD46BD47BD48BD49BD4ABD4BBD4CBD4DBD4EBD4FBD50BD51BD52BD53BD54BD55BD56BD57BD58BD59BD5ABD5BBD5CBD5DBD5EBD5FBD60BD61BD62BD63BD64BD65BD66BD67BD68BD69BD6ABD6BBD6CBD6DBD6EBD6FBD70BD71BD72BD73BD74BD75BD76D0F5BD77BD78BD79BD7ABD7BBD7CBD7DBD7EF4EABD80BD81BD82BD83BD84BD85BD86BD87BD88BD89BD8ABD8BBD8CBD8DBD8EBD8FBD90BD91BD92BD93BD94BD95BD96BD97BD98BD99BD9ABD9BBD9CBD9DBD9EBD9FBDA0BE40BE41BE42BE43BE44BE45BE46BE47BE48BE49BE4ABE4BBE4CF4EBBE4DBE4EBE4FBE50BE51BE52BE53F4ECBE54BE55BE56BE57BE58BE59BE5ABE5BBE5CBE5DBE5EBE5FBE60BE61BE62BE63BE64BE65BE66BE67BE68BE69BE6ABE6BBE6CBE6DBE6EBE6FBE70BE71BE72BE73BE74BE75BE76BE77BE78BE79BE7ABE7BBE7CBE7DBE7EBE80BE81BE82BE83BE84BE85BE86BE87BE88BE89BE8ABE8BBE8CBE8DBE8EBE8FBE90BE91BE92BE93BE94BE95BE96BE97BE98BE99BE9ABE9BBE9CBE9DBE9EBE9FBEA0BF40BF41BF42BF43BF44BF45BF46BF47BF48BF49BF4ABF4BBF4CBF4DBF4EBF4FBF50BF51BF52BF53BF54BF55BF56BF57BF58BF59BF5ABF5BBF5CBF5DBF5EBF5FBF60BF61BF62BF63BF64BF65BF66BF67BF68BF69BF6ABF6BBF6CBF6DBF6EBF6FBF70BF71BF72BF73BF74BF75BF76BF77BF78BF79BF7ABF7BBF7CBF7DBF7EBF80F7E3BF81BF82BF83BF84BF85B7B1BF86BF87BF88BF89BF8AF4EDBF8BBF8CBF8DBF8EBF8FBF90BF91BF92BF93BF94BF95BF96BF97BF98BF99BF9ABF9BBF9CBF9DBF9EBF9FBFA0C040C041C042C043C044C045C046C047C048C049C04AC04BC04CC04DC04EC04FC050C051C052C053C054C055C056C057C058C059C05AC05BC05CC05DC05EC05FC060C061C062C063D7EBC064C065C066C067C068C069C06AC06BC06CC06DC06EC06FC070C071C072C073C074C075C076C077C078C079C07AC07BF4EEC07CC07DC07EE6F9BEC0E6FABAECE6FBCFCBE6FCD4BCBCB6E6FDE6FEBCCDC8D2CEB3E7A1C080B4BFE7A2C9B4B8D9C4C9C081D7DDC2DAB7D7D6BDCEC6B7C4C082C083C5A6E7A3CFDFE7A4E7A5E7A6C1B7D7E9C9F0CFB8D6AFD6D5E7A7B0EDE7A8E7A9C9DCD2EFBEADE7AAB0F3C8DEBDE1E7ABC8C6C084E7ACBBE6B8F8D1A4E7ADC2E7BEF8BDCACDB3E7AEE7AFBEEED0E5C085CBE7CCD0BCCCE7B0BCA8D0F7E7B1C086D0F8E7B2E7B3B4C2E7B4E7B5C9FECEACC3E0E7B7B1C1B3F1C087E7B8E7B9D7DBD5C0E7BAC2CCD7BAE7BBE7BCE7BDBCEAC3E5C0C2E7BEE7BFBCA9C088E7C0E7C1E7B6B6D0E7C2C089E7C3E7C4BBBAB5DEC2C6B1E0E7C5D4B5E7C6B8BFE7C8E7C7B7ECC08AE7C9B2F8E7CAE7CBE7CCE7CDE7CEE7CFE7D0D3A7CBF5E7D1E7D2E7D3E7D4C9C9E7D5E7D6E7D7E7D8E7D9BDC9E7DAF3BEC08BB8D7C08CC8B1C08DC08EC08FC090C091C092C093F3BFC094F3C0F3C1C095C096C097C098C099C09AC09BC09CC09DC09EB9DECDF8C09FC0A0D8E8BAB1C140C2DEEEB7C141B7A3C142C143C144C145EEB9C146EEB8B0D5C147C148C149C14AC14BEEBBD5D6D7EFC14CC14DC14ED6C3C14FC150EEBDCAF0C151EEBCC152C153C154C155EEBEC156C157C158C159EEC0C15AC15BEEBFC15CC15DC15EC15FC160C161C162C163D1F2C164C7BCC165C3C0C166C167C168C169C16AB8E1C16BC16CC16DC16EC16FC1E7C170C171F4C6D0DFF4C7C172CFDBC173C174C8BAC175C176F4C8C177C178C179C17AC17BC17CC17DF4C9F4CAC17EF4CBC180C181C182C183C184D9FAB8FEC185C186E5F1D3F0C187F4E0C188CECCC189C18AC18BB3E1C18CC18DC18EC18FF1B4C190D2EEC191F4E1C192C193C194C195C196CFE8F4E2C197C198C7CCC199C19AC19BC19CC19DC19EB5D4B4E4F4E4C19FC1A0C240F4E3F4E5C241C242F4E6C243C244C245C246F4E7C247BAB2B0BFC248F4E8C249C24AC24BC24CC24DC24EC24FB7ADD2EDC250C251C252D2ABC0CFC253BFBCEBA3D5DFEAC8C254C255C256C257F1F3B6F8CBA3C258C259C4CDC25AF1E7C25BF1E8B8FBF1E9BAC4D4C5B0D2C25CC25DF1EAC25EC25FC260F1EBC261F1ECC262C263F1EDF1EEF1EFF1F1F1F0C5D5C264C265C266C267C268C269F1F2C26AB6FAC26BF1F4D2AEDEC7CBCAC26CC26DB3DCC26EB5A2C26FB9A2C270C271C4F4F1F5C272C273F1F6C274C275C276C1C4C1FBD6B0F1F7C277C278C279C27AF1F8C27BC1AAC27CC27DC27EC6B8C280BEDBC281C282C283C284C285C286C287C288C289C28AC28BC28CC28DC28EF1F9B4CFC28FC290C291C292C293C294F1FAC295C296C297C298C299C29AC29BC29CC29DC29EC29FC2A0C340EDB2EDB1C341C342CBE0D2DEC343CBC1D5D8C344C8E2C345C0DFBCA1C346C347C348C349C34AC34BEBC1C34CC34DD0A4C34ED6E2C34FB6C7B8D8EBC0B8CEC350EBBFB3A6B9C9D6ABC351B7F4B7CAC352C353C354BCE7B7BEEBC6C355EBC7B0B9BFCFC356EBC5D3FDC357EBC8C358C359EBC9C35AC35BB7CEC35CEBC2EBC4C9F6D6D7D5CDD0B2EBCFCEB8EBD0C35DB5A8C35EC35FC360C361C362B1B3EBD2CCA5C363C364C365C366C367C368C369C5D6EBD3C36AEBD1C5DFEBCECAA4EBD5B0FBC36BC36CBAFAC36DC36ED8B7F1E3C36FEBCAEBCBEBCCEBCDEBD6E6C0EBD9C370BFE8D2C8EBD7EBDCB8ECEBD8C371BDBAC372D0D8C373B0B7C374EBDDC4DCC375C376C377C378D6ACC379C37AC37BB4E0C37CC37DC2F6BCB9C37EC380EBDAEBDBD4E0C6EAC4D4EBDFC5A7D9F5C381B2B1C382EBE4C383BDC5C384C385C386EBE2C387C388C389C38AC38BC38CC38DC38EC38FC390C391C392C393EBE3C394C395B8ACC396CDD1EBE5C397C398C399EBE1C39AC1B3C39BC39CC39DC39EC39FC6A2C3A0C440C441C442C443C444C445CCF3C446EBE6C447C0B0D2B8EBE7C448C449C44AB8AFB8ADC44BEBE8C7BBCDF3C44CC44DC44EEBEAEBEBC44FC450C451C452C453EBEDC454C455C456C457D0C8C458EBF2C459EBEEC45AC45BC45CEBF1C8F9C45DD1FCEBECC45EC45FEBE9C460C461C462C463B8B9CFD9C4E5EBEFEBF0CCDACDC8B0F2C464EBF6C465C466C467C468C469EBF5C46AB2B2C46BC46CC46DC46EB8E0C46FEBF7C470C471C472C473C474C475B1ECC476C477CCC5C4A4CFA5C478C479C47AC47BC47CEBF9C47DC47EECA2C480C5F2C481EBFAC482C483C484C485C486C487C488C489C9C5C48AC48BC48CC48DC48EC48FE2DFEBFEC490C491C492C493CDCEECA1B1DBD3B7C494C495D2DCC496C497C498EBFDC499EBFBC49AC49BC49CC49DC49EC49FC4A0C540C541C542C543C544C545C546C547C548C549C54AC54BC54CC54DC54EB3BCC54FC550C551EAB0C552C553D7D4C554F4ABB3F4C555C556C557C558C559D6C1D6C2C55AC55BC55CC55DC55EC55FD5E9BECAC560F4A7C561D2A8F4A8F4A9C562F4AABECBD3DFC563C564C565C566C567C9E0C9E1C568C569F3C2C56ACAE6C56BCCF2C56CC56DC56EC56FC570C571E2B6CBB4C572CEE8D6DBC573F4ADF4AEF4AFC574C575C576C577F4B2C578BABDF4B3B0E3F4B0C579F4B1BDA2B2D5C57AF4B6F4B7B6E6B2B0CFCFF4B4B4ACC57BF4B5C57CC57DF4B8C57EC580C581C582C583F4B9C584C585CDA7C586F4BAC587F4BBC588C589C58AF4BCC58BC58CC58DC58EC58FC590C591C592CBD2C593F4BDC594C595C596C597F4BEC598C599C59AC59BC59CC59DC59EC59FF4BFC5A0C640C641C642C643F4DEC1BCBCE8C644C9ABD1DEE5F5C645C646C647C648DCB3D2D5C649C64ADCB4B0ACDCB5C64BC64CBDDAC64DDCB9C64EC64FC650D8C2C651DCB7D3F3C652C9D6DCBADCB6C653DCBBC3A2C654C655C656C657DCBCDCC5DCBDC658C659CEDFD6A5C65ADCCFC65BDCCDC65CC65DDCD2BDE6C2ABC65EDCB8DCCBDCCEDCBEB7D2B0C5DCC7D0BEDCC1BBA8C65FB7BCDCCCC660C661DCC6DCBFC7DBC662C663C664D1BFDCC0C665C666DCCAC667C668DCD0C669C66ACEADDCC2C66BDCC3DCC8DCC9B2D4DCD1CBD5C66CD4B7DCDBDCDFCCA6DCE6C66DC3E7DCDCC66EC66FBFC1DCD9C670B0FAB9B6DCE5DCD3C671DCC4DCD6C8F4BFE0C672C673C674C675C9BBC676C677C678B1BDC679D3A2C67AC67BDCDAC67CC67DDCD5C67EC6BBC680DCDEC681C682C683C684C685D7C2C3AFB7B6C7D1C3A9DCE2DCD8DCEBDCD4C686C687DCDDC688BEA5DCD7C689DCE0C68AC68BDCE3DCE4C68CDCF8C68DC68EDCE1DDA2DCE7C68FC690C691C692C693C694C695C696C697C698BCEBB4C4C699C69AC3A3B2E7DCFAC69BDCF2C69CDCEFC69DDCFCDCEED2F0B2E8C69EC8D7C8E3DCFBC69FDCEDC6A0C740C741DCF7C742C743DCF5C744C745BEA3DCF4C746B2DDC747C748C749C74AC74BDCF3BCF6DCE8BBC4C74CC0F3C74DC74EC74FC750C751BCD4DCE9DCEAC752DCF1DCF6DCF9B5B4C753C8D9BBE7DCFEDCFDD3ABDDA1DDA3DDA5D2F1DDA4DDA6DDA7D2A9C754C755C756C757C758C759C75ABAC9DDA9C75BC75CDDB6DDB1DDB4C75DC75EC75FC760C761C762C763DDB0C6CEC764C765C0F2C766C767C768C769C9AFC76AC76BC76CDCECDDAEC76DC76EC76FC770DDB7C771C772DCF0DDAFC773DDB8C774DDACC775C776C777C778C779C77AC77BDDB9DDB3DDADC4AAC77CC77DC77EC780DDA8C0B3C1ABDDAADDABC781DDB2BBF1DDB5D3A8DDBAC782DDBBC3A7C783C784DDD2DDBCC785C786C787DDD1C788B9BDC789C78ABED5C78BBEFAC78CC78DBACAC78EC78FC790C791DDCAC792DDC5C793DDBFC794C795C796B2CBDDC3C797DDCBB2A4DDD5C798C799C79ADDBEC79BC79CC79DC6D0DDD0C79EC79FC7A0C840C841DDD4C1E2B7C6C842C843C844C845C846DDCEDDCFC847C848C849DDC4C84AC84BC84CDDBDC84DDDCDCCD1C84EDDC9C84FC850C851C852DDC2C3C8C6BCCEAEDDCCC853DDC8C854C855C856C857C858C859DDC1C85AC85BC85CDDC6C2DCC85DC85EC85FC860C861C862D3A9D3AADDD3CFF4C8F8C863C864C865C866C867C868C869C86ADDE6C86BC86CC86DC86EC86FC870DDC7C871C872C873DDE0C2E4C874C875C876C877C878C879C87AC87BDDE1C87CC87DC87EC880C881C882C883C884C885C886DDD7C887C888C889C88AC88BD6F8C88CDDD9DDD8B8F0DDD6C88DC88EC88FC890C6CFC891B6ADC892C893C894C895C896DDE2C897BAF9D4E1DDE7C898C899C89AB4D0C89BDDDAC89CBFFBDDE3C89DDDDFC89EDDDDC89FC8A0C940C941C942C943C944B5D9C945C946C947C948DDDBDDDCDDDEC949BDAFDDE4C94ADDE5C94BC94CC94DC94EC94FC950C951C952DDF5C953C3C9C954C955CBE2C956C957C958C959DDF2C95AC95BC95CC95DC95EC95FC960C961C962C963C964C965C966D8E1C967C968C6D1C969DDF4C96AC96BC96CD5F4DDF3DDF0C96DC96EDDECC96FDDEFC970DDE8C971C972D0EEC973C974C975C976C8D8DDEEC977C978DDE9C979C97ADDEACBF2C97BDDEDC97CC97DB1CDC97EC980C981C982C983C984C0B6C985BCBBDDF1C986C987DDF7C988DDF6DDEBC989C98AC98BC98CC98DC5EEC98EC98FC990DDFBC991C992C993C994C995C996C997C998C999C99AC99BDEA4C99CC99DDEA3C99EC99FC9A0CA40CA41CA42CA43CA44CA45CA46CA47CA48DDF8CA49CA4ACA4BCA4CC3EFCA4DC2FBCA4ECA4FCA50D5E1CA51CA52CEB5CA53CA54CA55CA56DDFDCA57B2CCCA58CA59CA5ACA5BCA5CCA5DCA5ECA5FCA60C4E8CADFCA61CA62CA63CA64CA65CA66CA67CA68CA69CA6AC7BEDDFADDFCDDFEDEA2B0AAB1CECA6BCA6CCA6DCA6ECA6FDEACCA70CA71CA72CA73DEA6BDB6C8EFCA74CA75CA76CA77CA78CA79CA7ACA7BCA7CCA7DCA7EDEA1CA80CA81DEA5CA82CA83CA84CA85DEA9CA86CA87CA88CA89CA8ADEA8CA8BCA8CCA8DDEA7CA8ECA8FCA90CA91CA92CA93CA94CA95CA96DEADCA97D4CCCA98CA99CA9ACA9BDEB3DEAADEAECA9CCA9DC0D9CA9ECA9FCAA0CB40CB41B1A1DEB6CB42DEB1CB43CB44CB45CB46CB47CB48CB49DEB2CB4ACB4BCB4CCB4DCB4ECB4FCB50CB51CB52CB53CB54D1A6DEB5CB55CB56CB57CB58CB59CB5ACB5BDEAFCB5CCB5DCB5EDEB0CB5FD0BDCB60CB61CB62DEB4CAEDDEB9CB63CB64CB65CB66CB67CB68DEB8CB69DEB7CB6ACB6BCB6CCB6DCB6ECB6FCB70DEBBCB71CB72CB73CB74CB75CB76CB77BDE5CB78CB79CB7ACB7BCB7CB2D8C3EACB7DCB7EDEBACB80C5BACB81CB82CB83CB84CB85CB86DEBCCB87CB88CB89CB8ACB8BCB8CCB8DCCD9CB8ECB8FCB90CB91B7AACB92CB93CB94CB95CB96CB97CB98CB99CB9ACB9BCB9CCB9DCB9ECB9FCBA0CC40CC41D4E5CC42CC43CC44DEBDCC45CC46CC47CC48CC49DEBFCC4ACC4BCC4CCC4DCC4ECC4FCC50CC51CC52CC53CC54C4A2CC55CC56CC57CC58DEC1CC59CC5ACC5BCC5CCC5DCC5ECC5FCC60CC61CC62CC63CC64CC65CC66CC67CC68DEBECC69DEC0CC6ACC6BCC6CCC6DCC6ECC6FCC70CC71CC72CC73CC74CC75CC76CC77D5BACC78CC79CC7ADEC2CC7BCC7CCC7DCC7ECC80CC81CC82CC83CC84CC85CC86CC87CC88CC89CC8ACC8BF2AEBBA2C2B2C5B0C2C7CC8CCC8DF2AFCC8ECC8FCC90CC91CC92D0E9CC93CC94CC95D3DDCC96CC97CC98EBBDCC99CC9ACC9BCC9CCC9DCC9ECC9FCCA0B3E6F2B0CD40F2B1CD41CD42CAADCD43CD44CD45CD46CD47CD48CD49BAE7F2B3F2B5F2B4CBE4CFBAF2B2CAB4D2CFC2ECCD4ACD4BCD4CCD4DCD4ECD4FCD50CEC3F2B8B0F6F2B7CD51CD52CD53CD54CD55F2BECD56B2CFCD57CD58CD59CD5ACD5BCD5CD1C1F2BACD5DCD5ECD5FCD60CD61F2BCD4E9CD62CD63F2BBF2B6F2BFF2BDCD64F2B9CD65CD66F2C7F2C4F2C6CD67CD68F2CAF2C2F2C0CD69CD6ACD6BF2C5CD6CCD6DCD6ECD6FCD70D6FBCD71CD72CD73F2C1CD74C7F9C9DFCD75F2C8B9C6B5B0CD76CD77F2C3F2C9F2D0F2D6CD78CD79BBD7CD7ACD7BCD7CF2D5CDDCCD7DD6EBCD7ECD80F2D2F2D4CD81CD82CD83CD84B8F2CD85CD86CD87CD88F2CBCD89CD8ACD8BF2CEC2F9CD8CD5DDF2CCF2CDF2CFF2D3CD8DCD8ECD8FF2D9D3BCCD90CD91CD92CD93B6EACD94CAF1CD95B7E4F2D7CD96CD97CD98F2D8F2DAF2DDF2DBCD99CD9AF2DCCD9BCD9CCD9DCD9ED1D1F2D1CD9FCDC9CDA0CECFD6A9CE40F2E3CE41C3DBCE42F2E0CE43CE44C0AFF2ECF2DECE45F2E1CE46CE47CE48F2E8CE49CE4ACE4BCE4CF2E2CE4DCE4EF2E7CE4FCE50F2E6CE51CE52F2E9CE53CE54CE55F2DFCE56CE57F2E4F2EACE58CE59CE5ACE5BCE5CCE5DCE5ED3ACF2E5B2F5CE5FCE60F2F2CE61D0ABCE62CE63CE64CE65F2F5CE66CE67CE68BBC8CE69F2F9CE6ACE6BCE6CCE6DCE6ECE6FF2F0CE70CE71F2F6F2F8F2FACE72CE73CE74CE75CE76CE77CE78CE79F2F3CE7AF2F1CE7BCE7CCE7DBAFBCE7EB5FBCE80CE81CE82CE83F2EFF2F7F2EDF2EECE84CE85CE86F2EBF3A6CE87F3A3CE88CE89F3A2CE8ACE8BF2F4CE8CC8DACE8DCE8ECE8FCE90CE91F2FBCE92CE93CE94F3A5CE95CE96CE97CE98CE99CE9ACE9BC3F8CE9CCE9DCE9ECE9FCEA0CF40CF41CF42F2FDCF43CF44F3A7F3A9F3A4CF45F2FCCF46CF47CF48F3ABCF49F3AACF4ACF4BCF4CCF4DC2DDCF4ECF4FF3AECF50CF51F3B0CF52CF53CF54CF55CF56F3A1CF57CF58CF59F3B1F3ACCF5ACF5BCF5CCF5DCF5EF3AFF2FEF3ADCF5FCF60CF61CF62CF63CF64CF65F3B2CF66CF67CF68CF69F3B4CF6ACF6BCF6CCF6DF3A8CF6ECF6FCF70CF71F3B3CF72CF73CF74F3B5CF75CF76CF77CF78CF79CF7ACF7BCF7CCF7DCF7ED0B7CF80CF81CF82CF83F3B8CF84CF85CF86CF87D9F9CF88CF89CF8ACF8BCF8CCF8DF3B9CF8ECF8FCF90CF91CF92CF93CF94CF95F3B7CF96C8E4F3B6CF97CF98CF99CF9AF3BACF9BCF9CCF9DCF9ECF9FF3BBB4C0CFA0D040D041D042D043D044D045D046D047D048D049D04AD04BD04CD04DEEC3D04ED04FD050D051D052D053F3BCD054D055F3BDD056D057D058D1AAD059D05AD05BF4ACD0C6D05CD05DD05ED05FD060D061D0D0D1DCD062D063D064D065D066D067CFCED068D069BDD6D06AD1C3D06BD06CD06DD06ED06FD070D071BAE2E1E9D2C2F1C2B2B9D072D073B1EDF1C3D074C9C0B3C4D075D9F2D076CBA5D077F1C4D078D079D07AD07BD6D4D07CD07DD07ED080D081F1C5F4C0F1C6D082D4ACF1C7D083B0C0F4C1D084D085F4C2D086D087B4FCD088C5DBD089D08AD08BD08CCCBBD08DD08ED08FD0E4D090D091D092D093D094CDE0D095D096D097D098D099F1C8D09AD9F3D09BD09CD09DD09ED09FD0A0B1BBD140CFAED141D142D143B8A4D144D145D146D147D148F1CAD149D14AD14BD14CF1CBD14DD14ED14FD150B2C3C1D1D151D152D7B0F1C9D153D154F1CCD155D156D157D158F1CED159D15AD15BD9F6D15CD2E1D4A3D15DD15EF4C3C8B9D15FD160D161D162D163F4C4D164D165F1CDF1CFBFE3F1D0D166D167F1D4D168D169D16AD16BD16CD16DD16EF1D6F1D1D16FC9D1C5E1D170D171D172C2E3B9FCD173D174F1D3D175F1D5D176D177D178B9D3D179D17AD17BD17CD17DD17ED180F1DBD181D182D183D184D185BAD6D186B0FDF1D9D187D188D189D18AD18BF1D8F1D2F1DAD18CD18DD18ED18FD190F1D7D191D192D193C8ECD194D195D196D197CDCAF1DDD198D199D19AD19BE5BDD19CD19DD19EF1DCD19FF1DED1A0D240D241D242D243D244D245D246D247D248F1DFD249D24ACFE5D24BD24CD24DD24ED24FD250D251D252D253D254D255D256D257D258D259D25AD25BD25CD25DD25ED25FD260D261D262D263F4C5BDF3D264D265D266D267D268D269F1E0D26AD26BD26CD26DD26ED26FD270D271D272D273D274D275D276D277D278D279D27AD27BD27CD27DF1E1D27ED280D281CEF7D282D2AAD283F1FBD284D285B8B2D286D287D288D289D28AD28BD28CD28DD28ED28FD290D291D292D293D294D295D296D297D298D299D29AD29BD29CD29DD29ED29FD2A0D340D341D342D343D344D345D346D347D348D349D34AD34BD34CD34DD34ED34FD350D351D352D353D354D355D356D357D358D359D35AD35BD35CD35DD35EBCFBB9DBD35FB9E6C3D9CAD3EAE8C0C0BEF5EAE9EAEAEAEBD360EAECEAEDEAEEEAEFBDC7D361D362D363F5FBD364D365D366F5FDD367F5FED368F5FCD369D36AD36BD36CBDE2D36DF6A1B4A5D36ED36FD370D371F6A2D372D373D374F6A3D375D376D377ECB2D378D379D37AD37BD37CD37DD37ED380D381D382D383D384D1D4D385D386D387D388D389D38AD9EAD38BD38CD38DD38ED38FD390D391D392D393D394D395D396D397D398D399D39AD39BD39CD39DD39ED39FD3A0D440D441D442D443D444D445D446D447D448D449D44AD44BD44CD44DD44ED44FD450D451D452D453D454D455D456D457D458D459D45AD45BD45CD45DD45ED45FF6A4D460D461D462D463D464D465D466D467D468EEBAD469D46AD46BD46CD46DD46ED46FD470D471D472D473D474D475D476D477D478D479D47AD47BD47CD47DD47ED480D481D482D483D484D485D486D487D488D489D48AD48BD48CD48DD48ED48FD490D491D492D493D494D495D496D497D498D499D5B2D49AD49BD49CD49DD49ED49FD4A0D540D541D542D543D544D545D546D547D3FECCDCD548D549D54AD54BD54CD54DD54ED54FCAC4D550D551D552D553D554D555D556D557D558D559D55AD55BD55CD55DD55ED55FD560D561D562D563D564D565D566D567D568D569D56AD56BD56CD56DD56ED56FD570D571D572D573D574D575D576D577D578D579D57AD57BD57CD57DD57ED580D581D582D583D584D585D586D587D588D589D58AD58BD58CD58DD58ED58FD590D591D592D593D594D595D596D597D598D599D59AD59BD59CD59DD59ED59FD5A0D640D641D642D643D644D645D646D647D648D649D64AD64BD64CD64DD64ED64FD650D651D652D653D654D655D656D657D658D659D65AD65BD65CD65DD65ED65FD660D661D662E5C0D663D664D665D666D667D668D669D66AD66BD66CD66DD66ED66FD670D671D672D673D674D675D676D677D678D679D67AD67BD67CD67DD67ED680D681F6A5D682D683D684D685D686D687D688D689D68AD68BD68CD68DD68ED68FD690D691D692D693D694D695D696D697D698D699D69AD69BD69CD69DD69ED69FD6A0D740D741D742D743D744D745D746D747D748D749D74AD74BD74CD74DD74ED74FD750D751D752D753D754D755D756D757D758D759D75AD75BD75CD75DD75ED75FBEAFD760D761D762D763D764C6A9D765D766D767D768D769D76AD76BD76CD76DD76ED76FD770D771D772D773D774D775D776D777D778D779D77AD77BD77CD77DD77ED780D781D782D783D784D785D786D787D788D789D78AD78BD78CD78DD78ED78FD790D791D792D793D794D795D796D797D798DAA5BCC6B6A9B8BCC8CFBCA5DAA6DAA7CCD6C8C3DAA8C6FDD799D1B5D2E9D1B6BCC7D79ABDB2BBE4DAA9DAAAD1C8DAABD0EDB6EFC2DBD79BCBCFB7EDC9E8B7C3BEF7D6A4DAACDAADC6C0D7E7CAB6D79CD5A9CBDFD5EFDAAED6DFB4CADAB0DAAFD79DD2EBDAB1DAB2DAB3CAD4DAB4CAABDAB5DAB6B3CFD6EFDAB7BBB0B5AEDAB8DAB9B9EED1AFD2E8DABAB8C3CFEAB2EFDABBDABCD79EBDEBCEDCD3EFDABDCEF3DABED3D5BBE5DABFCBB5CBD0DAC0C7EBD6EEDAC1C5B5B6C1DAC2B7CCBFCEDAC3DAC4CBADDAC5B5F7DAC6C1C2D7BBDAC7CCB8D79FD2EAC4B1DAC8B5FDBBD1DAC9D0B3DACADACBCEBDDACCDACDDACEB2F7DAD1DACFD1E8DAD0C3D5DAD2D7A0DAD3DAD4DAD5D0BBD2A5B0F9DAD6C7ABDAD7BDF7C3A1DAD8DAD9C3FDCCB7DADADADBC0BEC6D7DADCDADDC7B4DADEDADFB9C8D840D841D842D843D844D845D846D847D848BBEDD849D84AD84BD84CB6B9F4F8D84DF4F9D84ED84FCDE3D850D851D852D853D854D855D856D857F5B9D858D859D85AD85BEBE0D85CD85DD85ED85FD860D861CFF3BBBFD862D863D864D865D866D867D868BAC0D4A5D869D86AD86BD86CD86DD86ED86FE1D9D870D871D872D873F5F4B1AAB2F2D874D875D876D877D878D879D87AF5F5D87BD87CF5F7D87DD87ED880BAD1F5F6D881C3B2D882D883D884D885D886D887D888F5F9D889D88AD88BF5F8D88CD88DD88ED88FD890D891D892D893D894D895D896D897D898D899D89AD89BD89CD89DD89ED89FD8A0D940D941D942D943D944D945D946D947D948D949D94AD94BD94CD94DD94ED94FD950D951D952D953D954D955D956D957D958D959D95AD95BD95CD95DD95ED95FD960D961D962D963D964D965D966D967D968D969D96AD96BD96CD96DD96ED96FD970D971D972D973D974D975D976D977D978D979D97AD97BD97CD97DD97ED980D981D982D983D984D985D986D987D988D989D98AD98BD98CD98DD98ED98FD990D991D992D993D994D995D996D997D998D999D99AD99BD99CD99DD99ED99FD9A0DA40DA41DA42DA43DA44DA45DA46DA47DA48DA49DA4ADA4BDA4CDA4DDA4EB1B4D5EAB8BADA4FB9B1B2C6D4F0CFCDB0DCD5CBBBF5D6CAB7B7CCB0C6B6B1E1B9BAD6FCB9E1B7A1BCFAEADAEADBCCF9B9F3EADCB4FBC3B3B7D1BAD8EADDD4F4EADEBCD6BBDFEADFC1DEC2B8D4DFD7CAEAE0EAE1EAE4EAE2EAE3C9DEB8B3B6C4EAE5CAEAC9CDB4CDDA50DA51E2D9C5E2EAE6C0B5DA52D7B8EAE7D7ACC8FCD8D3D8CDD4DEDA53D4F9C9C4D3AEB8D3B3E0DA54C9E2F4F6DA55DA56DA57BAD5DA58F4F7DA59DA5AD7DFDA5BDA5CF4F1B8B0D5D4B8CFC6F0DA5DDA5EDA5FDA60DA61DA62DA63DA64DA65B3C3DA66DA67F4F2B3ACDA68DA69DA6ADA6BD4BDC7F7DA6CDA6DDA6EDA6FDA70F4F4DA71DA72F4F3DA73DA74DA75DA76DA77DA78DA79DA7ADA7BDA7CCCCBDA7DDA7EDA80C8A4DA81DA82DA83DA84DA85DA86DA87DA88DA89DA8ADA8BDA8CDA8DF4F5DA8ED7E3C5BFF5C0DA8FDA90F5BBDA91F5C3DA92F5C2DA93D6BAF5C1DA94DA95DA96D4BEF5C4DA97F5CCDA98DA99DA9ADA9BB0CFB5F8DA9CF5C9F5CADA9DC5DCDA9EDA9FDAA0DB40F5C5F5C6DB41DB42F5C7F5CBDB43BEE0F5C8B8FADB44DB45DB46F5D0F5D3DB47DB48DB49BFE7DB4AB9F2F5BCF5CDDB4BDB4CC2B7DB4DDB4EDB4FCCF8DB50BCF9DB51F5CEF5CFF5D1B6E5F5D2DB52F5D5DB53DB54DB55DB56DB57DB58DB59F5BDDB5ADB5BDB5CF5D4D3BBDB5DB3ECDB5EDB5FCCA4DB60DB61DB62DB63F5D6DB64DB65DB66DB67DB68DB69DB6ADB6BF5D7BEE1F5D8DB6CDB6DCCDFF5DBDB6EDB6FDB70DB71DB72B2C8D7D9DB73F5D9DB74F5DAF5DCDB75F5E2DB76DB77DB78F5E0DB79DB7ADB7BF5DFF5DDDB7CDB7DF5E1DB7EDB80F5DEF5E4F5E5DB81CCE3DB82DB83E5BFB5B8F5E3F5E8CCA3DB84DB85DB86DB87DB88F5E6F5E7DB89DB8ADB8BDB8CDB8DDB8EF5BEDB8FDB90DB91DB92DB93DB94DB95DB96DB97DB98DB99DB9AB1C4DB9BDB9CF5BFDB9DDB9EB5C5B2E4DB9FF5ECF5E9DBA0B6D7DC40F5EDDC41F5EADC42DC43DC44DC45DC46F5EBDC47DC48B4DADC49D4EADC4ADC4BDC4CF5EEDC4DB3F9DC4EDC4FDC50DC51DC52DC53DC54F5EFF5F1DC55DC56DC57F5F0DC58DC59DC5ADC5BDC5CDC5DDC5EF5F2DC5FF5F3DC60DC61DC62DC63DC64DC65DC66DC67DC68DC69DC6ADC6BC9EDB9AADC6CDC6DC7FBDC6EDC6FB6E3DC70DC71DC72DC73DC74DC75DC76CCC9DC77DC78DC79DC7ADC7BDC7CDC7DDC7EDC80DC81DC82DC83DC84DC85DC86DC87DC88DC89DC8AEAA6DC8BDC8CDC8DDC8EDC8FDC90DC91DC92DC93DC94DC95DC96DC97DC98DC99DC9ADC9BDC9CDC9DDC9EDC9FDCA0DD40DD41DD42DD43DD44DD45DD46DD47DD48DD49DD4ADD4BDD4CDD4DDD4EDD4FDD50DD51DD52DD53DD54DD55DD56DD57DD58DD59DD5ADD5BDD5CDD5DDD5EDD5FDD60DD61DD62DD63DD64DD65DD66DD67DD68DD69DD6ADD6BDD6CDD6DDD6EDD6FDD70DD71DD72DD73DD74DD75DD76DD77DD78DD79DD7ADD7BDD7CDD7DDD7EDD80DD81DD82DD83DD84DD85DD86DD87DD88DD89DD8ADD8BDD8CDD8DDD8EDD8FDD90DD91DD92DD93DD94DD95DD96DD97DD98DD99DD9ADD9BDD9CDD9DDD9EDD9FDDA0DE40DE41DE42DE43DE44DE45DE46DE47DE48DE49DE4ADE4BDE4CDE4DDE4EDE4FDE50DE51DE52DE53DE54DE55DE56DE57DE58DE59DE5ADE5BDE5CDE5DDE5EDE5FDE60B3B5D4FEB9ECD0F9DE61E9EDD7AAE9EEC2D6C8EDBAE4E9EFE9F0E9F1D6E1E9F2E9F3E9F5E9F4E9F6E9F7C7E1E9F8D4D8E9F9BDCEDE62E9FAE9FBBDCFE9FCB8A8C1BEE9FDB1B2BBD4B9F5E9FEDE63EAA1EAA2EAA3B7F8BCADDE64CAE4E0CED4AFCFBDD5B7EAA4D5DEEAA5D0C1B9BCDE65B4C7B1D9DE66DE67DE68C0B1DE69DE6ADE6BDE6CB1E6B1E7DE6DB1E8DE6EDE6FDE70DE71B3BDC8E8DE72DE73DE74DE75E5C1DE76DE77B1DFDE78DE79DE7AC1C9B4EFDE7BDE7CC7A8D3D8DE7DC6F9D1B8DE7EB9FDC2F5DE80DE81DE82DE83DE84D3ADDE85D4CBBDFCDE86E5C2B7B5E5C3DE87DE88BBB9D5E2DE89BDF8D4B6CEA5C1ACB3D9DE8ADE8BCCF6DE8CE5C6E5C4E5C8DE8DE5CAE5C7B5CFC6C8DE8EB5FCE5C5DE8FCAF6DE90DE91E5C9DE92DE93DE94C3D4B1C5BCA3DE95DE96DE97D7B7DE98DE99CDCBCBCDCACACCD3E5CCE5CBC4E6DE9ADE9BD1A1D1B7E5CDDE9CE5D0DE9DCDB8D6F0E5CFB5DDDE9ECDBEDE9FE5D1B6BADEA0DF40CDA8B9E4DF41CAC5B3D1CBD9D4ECE5D2B7EADF42DF43DF44E5CEDF45DF46DF47DF48DF49DF4AE5D5B4FEE5D6DF4BDF4CDF4DDF4EDF4FE5D3E5D4DF50D2DDDF51DF52C2DFB1C6DF53D3E2DF54DF55B6DDCBECDF56E5D7DF57DF58D3F6DF59DF5ADF5BDF5CDF5DB1E9DF5EB6F4E5DAE5D8E5D9B5C0DF5FDF60DF61D2C5E5DCDF62DF63E5DEDF64DF65DF66DF67DF68DF69E5DDC7B2DF6AD2A3DF6BDF6CE5DBDF6DDF6EDF6FDF70D4E2D5DADF71DF72DF73DF74DF75E5E0D7F1DF76DF77DF78DF79DF7ADF7BDF7CE5E1DF7DB1DCD1FBDF7EE5E2E5E4DF80DF81DF82DF83E5E3DF84DF85E5E5DF86DF87DF88DF89DF8AD2D8DF8BB5CBDF8CE7DFDF8DDAF5DF8EDAF8DF8FDAF6DF90DAF7DF91DF92DF93DAFAD0CFC4C7DF94DF95B0EEDF96DF97DF98D0B0DF99DAF9DF9AD3CABAAADBA2C7F1DF9BDAFCDAFBC9DBDAFDDF9CDBA1D7DEDAFEC1DADF9DDF9EDBA5DF9FDFA0D3F4E040E041DBA7DBA4E042DBA8E043E044BDBCE045E046E047C0C9DBA3DBA6D6A3E048DBA9E049E04AE04BDBADE04CE04DE04EDBAEDBACBAC2E04FE050E051BFA4DBABE052E053E054DBAAD4C7B2BFE055E056DBAFE057B9F9E058DBB0E059E05AE05BE05CB3BBE05DE05EE05FB5A6E060E061E062E063B6BCDBB1E064E065E066B6F5E067DBB2E068E069E06AE06BE06CE06DE06EE06FE070E071E072E073E074E075E076E077E078E079E07AE07BB1C9E07CE07DE07EE080DBB4E081E082E083DBB3DBB5E084E085E086E087E088E089E08AE08BE08CE08DE08EDBB7E08FDBB6E090E091E092E093E094E095E096DBB8E097E098E099E09AE09BE09CE09DE09EE09FDBB9E0A0E140DBBAE141E142D3CFF4FAC7F5D7C3C5E4F4FCF4FDF4FBE143BEC6E144E145E146E147D0EFE148E149B7D3E14AE14BD4CDCCAAE14CE14DF5A2F5A1BAA8F4FECBD6E14EE14FE150F5A4C0D2E151B3EAE152CDAAF5A5F5A3BDB4F5A8E153F5A9BDCDC3B8BFE1CBE1F5AAE154E155E156F5A6F5A7C4F0E157E158E159E15AE15BF5ACE15CB4BCE15DD7EDE15EB4D7F5ABF5AEE15FE160F5ADF5AFD0D1E161E162E163E164E165E166E167C3D1C8A9E168E169E16AE16BE16CE16DF5B0F5B1E16EE16FE170E171E172E173F5B2E174E175F5B3F5B4F5B5E176E177E178E179F5B7F5B6E17AE17BE17CE17DF5B8E17EE180E181E182E183E184E185E186E187E188E189E18AB2C9E18BD3D4CACDE18CC0EFD6D8D2B0C1BFE18DBDF0E18EE18FE190E191E192E193E194E195E196E197B8AAE198E199E19AE19BE19CE19DE19EE19FE1A0E240E241E242E243E244E245E246E247E248E249E24AE24BE24CE24DE24EE24FE250E251E252E253E254E255E256E257E258E259E25AE25BE25CE25DE25EE25FE260E261E262E263E264E265E266E267E268E269E26AE26BE26CE26DE26EE26FE270E271E272E273E274E275E276E277E278E279E27AE27BE27CE27DE27EE280E281E282E283E284E285E286E287E288E289E28AE28BE28CE28DE28EE28FE290E291E292E293E294E295E296E297E298E299E29AE29BE29CE29DE29EE29FE2A0E340E341E342E343E344E345E346E347E348E349E34AE34BE34CE34DE34EE34FE350E351E352E353E354E355E356E357E358E359E35AE35BE35CE35DE35EE35FE360E361E362E363E364E365E366E367E368E369E36AE36BE36CE36DBCF8E36EE36FE370E371E372E373E374E375E376E377E378E379E37AE37BE37CE37DE37EE380E381E382E383E384E385E386E387F6C6E388E389E38AE38BE38CE38DE38EE38FE390E391E392E393E394E395E396E397E398E399E39AE39BE39CE39DE39EE39FE3A0E440E441E442E443E444E445F6C7E446E447E448E449E44AE44BE44CE44DE44EE44FE450E451E452E453E454E455E456E457E458E459E45AE45BE45CE45DE45EF6C8E45FE460E461E462E463E464E465E466E467E468E469E46AE46BE46CE46DE46EE46FE470E471E472E473E474E475E476E477E478E479E47AE47BE47CE47DE47EE480E481E482E483E484E485E486E487E488E489E48AE48BE48CE48DE48EE48FE490E491E492E493E494E495E496E497E498E499E49AE49BE49CE49DE49EE49FE4A0E540E541E542E543E544E545E546E547E548E549E54AE54BE54CE54DE54EE54FE550E551E552E553E554E555E556E557E558E559E55AE55BE55CE55DE55EE55FE560E561E562E563E564E565E566E567E568E569E56AE56BE56CE56DE56EE56FE570E571E572E573F6C9E574E575E576E577E578E579E57AE57BE57CE57DE57EE580E581E582E583E584E585E586E587E588E589E58AE58BE58CE58DE58EE58FE590E591E592E593E594E595E596E597E598E599E59AE59BE59CE59DE59EE59FF6CAE5A0E640E641E642E643E644E645E646E647E648E649E64AE64BE64CE64DE64EE64FE650E651E652E653E654E655E656E657E658E659E65AE65BE65CE65DE65EE65FE660E661E662F6CCE663E664E665E666E667E668E669E66AE66BE66CE66DE66EE66FE670E671E672E673E674E675E676E677E678E679E67AE67BE67CE67DE67EE680E681E682E683E684E685E686E687E688E689E68AE68BE68CE68DE68EE68FE690E691E692E693E694E695E696E697E698E699E69AE69BE69CE69DF6CBE69EE69FE6A0E740E741E742E743E744E745E746E747F7E9E748E749E74AE74BE74CE74DE74EE74FE750E751E752E753E754E755E756E757E758E759E75AE75BE75CE75DE75EE75FE760E761E762E763E764E765E766E767E768E769E76AE76BE76CE76DE76EE76FE770E771E772E773E774E775E776E777E778E779E77AE77BE77CE77DE77EE780E781E782E783E784E785E786E787E788E789E78AE78BE78CE78DE78EE78FE790E791E792E793E794E795E796E797E798E799E79AE79BE79CE79DE79EE79FE7A0E840E841E842E843E844E845E846E847E848E849E84AE84BE84CE84DE84EF6CDE84FE850E851E852E853E854E855E856E857E858E859E85AE85BE85CE85DE85EE85FE860E861E862E863E864E865E866E867E868E869E86AE86BE86CE86DE86EE86FE870E871E872E873E874E875E876E877E878E879E87AF6CEE87BE87CE87DE87EE880E881E882E883E884E885E886E887E888E889E88AE88BE88CE88DE88EE88FE890E891E892E893E894EEC4EEC5EEC6D5EBB6A4EEC8EEC7EEC9EECAC7A5EECBEECCE895B7B0B5F6EECDEECFE896EECEE897B8C6EED0EED1EED2B6DBB3AED6D3C4C6B1B5B8D6EED3EED4D4BFC7D5BEFBCED9B9B3EED6EED5EED8EED7C5A5EED9EEDAC7AEEEDBC7AFEEDCB2A7EEDDEEDEEEDFEEE0EEE1D7EAEEE2EEE3BCD8EEE4D3CBCCFAB2ACC1E5EEE5C7A6C3ADE898EEE6EEE7EEE8EEE9EEEAEEEBEEECE899EEEDEEEEEEEFE89AE89BEEF0EEF1EEF2EEF4EEF3E89CEEF5CDADC2C1EEF6EEF7EEF8D5A1EEF9CFB3EEFAEEFBE89DEEFCEEFDEFA1EEFEEFA2B8F5C3FAEFA3EFA4BDC2D2BFB2F9EFA5EFA6EFA7D2F8EFA8D6FDEFA9C6CCE89EEFAAEFABC1B4EFACCFFACBF8EFAEEFADB3FAB9F8EFAFEFB0D0E2EFB1EFB2B7E6D0BFEFB3EFB4EFB5C8F1CCE0EFB6EFB7EFB8EFB9EFBAD5E0EFBBB4EDC3AAEFBCE89FEFBDEFBEEFBFE8A0CEFDEFC0C2E0B4B8D7B6BDF5E940CFC7EFC3EFC1EFC2EFC4B6A7BCFCBEE2C3CCEFC5EFC6E941EFC7EFCFEFC8EFC9EFCAC7C2EFF1B6CDEFCBE942EFCCEFCDB6C6C3BEEFCEE943EFD0EFD1EFD2D5F2E944EFD3C4F7E945EFD4C4F8EFD5EFD6B8E4B0F7EFD7EFD8EFD9E946EFDAEFDBEFDCEFDDE947EFDEBEB5EFE1EFDFEFE0E948EFE2EFE3C1CDEFE4EFE5EFE6EFE7EFE8EFE9EFEAEFEBEFECC0D8E949EFEDC1ADEFEEEFEFEFF0E94AE94BCFE2E94CE94DE94EE94FE950E951E952E953B3A4E954E955E956E957E958E959E95AE95BE95CE95DE95EE95FE960E961E962E963E964E965E966E967E968E969E96AE96BE96CE96DE96EE96FE970E971E972E973E974E975E976E977E978E979E97AE97BE97CE97DE97EE980E981E982E983E984E985E986E987E988E989E98AE98BE98CE98DE98EE98FE990E991E992E993E994E995E996E997E998E999E99AE99BE99CE99DE99EE99FE9A0EA40EA41EA42EA43EA44EA45EA46EA47EA48EA49EA4AEA4BEA4CEA4DEA4EEA4FEA50EA51EA52EA53EA54EA55EA56EA57EA58EA59EA5AEA5BC3C5E3C5C9C1E3C6EA5CB1D5CECAB4B3C8F2E3C7CFD0E3C8BCE4E3C9E3CAC3C6D5A2C4D6B9EBCEC5E3CBC3F6E3CCEA5DB7A7B8F3BAD2E3CDE3CED4C4E3CFEA5EE3D0D1CBE3D1E3D2E3D3E3D4D1D6E3D5B2FBC0BBE3D6EA5FC0ABE3D7E3D8E3D9EA60E3DAE3DBEA61B8B7DAE2EA62B6D3EA63DAE4DAE3EA64EA65EA66EA67EA68EA69EA6ADAE6EA6BEA6CEA6DC8EEEA6EEA6FDAE5B7C0D1F4D2F5D5F3BDD7EA70EA71EA72EA73D7E8DAE8DAE7EA74B0A2CDD3EA75DAE9EA76B8BDBCCAC2BDC2A4B3C2DAEAEA77C2AAC4B0BDB5EA78EA79CFDEEA7AEA7BEA7CDAEBC9C2EA7DEA7EEA80EA81EA82B1DDEA83EA84EA85DAECEA86B6B8D4BAEA87B3FDEA88EA89DAEDD4C9CFD5C5E3EA8ADAEEEA8BEA8CEA8DEA8EEA8FDAEFEA90DAF0C1EACCD5CFDDEA91EA92EA93EA94EA95EA96EA97EA98EA99EA9AEA9BEA9CEA9DD3E7C2A1EA9EDAF1EA9FEAA0CBE5EB40DAF2EB41CBE6D2FEEB42EB43EB44B8F4EB45EB46DAF3B0AFCFB6EB47EB48D5CFEB49EB4AEB4BEB4CEB4DEB4EEB4FEB50EB51EB52CBEDEB53EB54EB55EB56EB57EB58EB59EB5ADAF4EB5BEB5CE3C4EB5DEB5EC1A5EB5FEB60F6BFEB61EB62F6C0F6C1C4D1EB63C8B8D1E3EB64EB65D0DBD1C5BCAFB9CDEB66EFF4EB67EB68B4C6D3BAF6C2B3FBEB69EB6AF6C3EB6BEB6CB5F1EB6DEB6EEB6FEB70EB71EB72EB73EB74EB75EB76F6C5EB77EB78EB79EB7AEB7BEB7CEB7DD3EAF6A7D1A9EB7EEB80EB81EB82F6A9EB83EB84EB85F6A8EB86EB87C1E3C0D7EB88B1A2EB89EB8AEB8BEB8CCEEDEB8DD0E8F6ABEB8EEB8FCFF6EB90F6AAD5F0F6ACC3B9EB91EB92EB93BBF4F6AEF6ADEB94EB95EB96C4DEEB97EB98C1D8EB99EB9AEB9BEB9CEB9DCBAAEB9ECFBCEB9FEBA0EC40EC41EC42EC43EC44EC45EC46EC47EC48F6AFEC49EC4AF6B0EC4BEC4CF6B1EC4DC2B6EC4EEC4FEC50EC51EC52B0D4C5F9EC53EC54EC55EC56F6B2EC57EC58EC59EC5AEC5BEC5CEC5DEC5EEC5FEC60EC61EC62EC63EC64EC65EC66EC67EC68EC69C7E0F6A6EC6AEC6BBEB8EC6CEC6DBEB2EC6EB5E5EC6FEC70B7C7EC71BFBFC3D2C3E6EC72EC73D8CCEC74EC75EC76B8EFEC77EC78EC79EC7AEC7BEC7CEC7DEC7EEC80BDF9D1A5EC81B0D0EC82EC83EC84EC85EC86F7B0EC87EC88EC89EC8AEC8BEC8CEC8DEC8EF7B1EC8FEC90EC91EC92EC93D0ACEC94B0B0EC95EC96EC97F7B2F7B3EC98F7B4EC99EC9AEC9BC7CAEC9CEC9DEC9EEC9FECA0ED40ED41BECFED42ED43F7B7ED44ED45ED46ED47ED48ED49ED4AF7B6ED4BB1DEED4CF7B5ED4DED4EF7B8ED4FF7B9ED50ED51ED52ED53ED54ED55ED56ED57ED58ED59ED5AED5BED5CED5DED5EED5FED60ED61ED62ED63ED64ED65ED66ED67ED68ED69ED6AED6BED6CED6DED6EED6FED70ED71ED72ED73ED74ED75ED76ED77ED78ED79ED7AED7BED7CED7DED7EED80ED81CEA4C8CDED82BAABE8B8E8B9E8BABEC2ED83ED84ED85ED86ED87D2F4ED88D4CFC9D8ED89ED8AED8BED8CED8DED8EED8FED90ED91ED92ED93ED94ED95ED96ED97ED98ED99ED9AED9BED9CED9DED9EED9FEDA0EE40EE41EE42EE43EE44EE45EE46EE47EE48EE49EE4AEE4BEE4CEE4DEE4EEE4FEE50EE51EE52EE53EE54EE55EE56EE57EE58EE59EE5AEE5BEE5CEE5DEE5EEE5FEE60EE61EE62EE63EE64EE65EE66EE67EE68EE69EE6AEE6BEE6CEE6DEE6EEE6FEE70EE71EE72EE73EE74EE75EE76EE77EE78EE79EE7AEE7BEE7CEE7DEE7EEE80EE81EE82EE83EE84EE85EE86EE87EE88EE89EE8AEE8BEE8CEE8DEE8EEE8FEE90EE91EE92EE93EE94EE95EE96EE97EE98EE99EE9AEE9BEE9CEE9DEE9EEE9FEEA0EF40EF41EF42EF43EF44EF45D2B3B6A5C7EAF1FCCFEECBB3D0EBE7EFCDE7B9CBB6D9F1FDB0E4CBCCF1FED4A4C2ADC1ECC6C4BEB1F2A1BCD5EF46F2A2F2A3EF47F2A4D2C3C6B5EF48CDC7F2A5EF49D3B1BFC5CCE2EF4AF2A6F2A7D1D5B6EEF2A8F2A9B5DFF2AAF2ABEF4BB2FCF2ACF2ADC8A7EF4CEF4DEF4EEF4FEF50EF51EF52EF53EF54EF55EF56EF57EF58EF59EF5AEF5BEF5CEF5DEF5EEF5FEF60EF61EF62EF63EF64EF65EF66EF67EF68EF69EF6AEF6BEF6CEF6DEF6EEF6FEF70EF71B7E7EF72EF73ECA9ECAAECABEF74ECACEF75EF76C6AEECADECAEEF77EF78EF79B7C9CAB3EF7AEF7BEF7CEF7DEF7EEF80EF81E2B8F7CFEF82EF83EF84EF85EF86EF87EF88EF89EF8AEF8BEF8CEF8DEF8EEF8FEF90EF91EF92EF93EF94EF95EF96EF97EF98EF99EF9AEF9BEF9CEF9DEF9EEF9FEFA0F040F041F042F043F044F7D0F045F046B2CDF047F048F049F04AF04BF04CF04DF04EF04FF050F051F052F053F054F055F056F057F058F059F05AF05BF05CF05DF05EF05FF060F061F062F063F7D1F064F065F066F067F068F069F06AF06BF06CF06DF06EF06FF070F071F072F073F074F075F076F077F078F079F07AF07BF07CF07DF07EF080F081F082F083F084F085F086F087F088F089F7D3F7D2F08AF08BF08CF08DF08EF08FF090F091F092F093F094F095F096E2BBF097BCA2F098E2BCE2BDE2BEE2BFE2C0E2C1B7B9D2FBBDA4CACEB1A5CBC7F099E2C2B6FCC8C4E2C3F09AF09BBDC8F09CB1FDE2C4F09DB6F6E2C5C4D9F09EF09FE2C6CFDAB9DDE2C7C0A1F0A0E2C8B2F6F140E2C9F141C1F3E2CAE2CBC2F8E2CCE2CDE2CECAD7D8B8D9E5CFE3F142F143F144F145F146F147F148F149F14AF14BF14CF0A5F14DF14EDCB0F14FF150F151F152F153F154F155F156F157F158F159F15AF15BF15CF15DF15EF15FF160F161F162F163F164F165F166F167F168F169F16AF16BF16CF16DF16EF16FF170F171F172F173F174F175F176F177F178F179F17AF17BF17CF17DF17EF180F181F182F183F184F185F186F187F188F189F18AF18BF18CF18DF18EF18FF190F191F192F193F194F195F196F197F198F199F19AF19BF19CF19DF19EF19FF1A0F240F241F242F243F244F245F246F247F248F249F24AF24BF24CF24DF24EF24FF250F251F252F253F254F255F256F257F258F259F25AF25BF25CF25DF25EF25FF260F261F262F263F264F265F266F267F268F269F26AF26BF26CF26DF26EF26FF270F271F272F273F274F275F276F277F278F279F27AF27BF27CF27DF27EF280F281F282F283F284F285F286F287F288F289F28AF28BF28CF28DF28EF28FF290F291F292F293F294F295F296F297F298F299F29AF29BF29CF29DF29EF29FF2A0F340F341F342F343F344F345F346F347F348F349F34AF34BF34CF34DF34EF34FF350F351C2EDD4A6CDD4D1B1B3DBC7FDF352B2B5C2BFE6E0CABBE6E1E6E2BED4E6E3D7A4CDD5E6E5BCDDE6E4E6E6E6E7C2EEF353BDBEE6E8C2E6BAA7E6E9F354E6EAB3D2D1E9F355F356BFA5E6EBC6EFE6ECE6EDF357F358E6EEC6ADE6EFF359C9A7E6F0E6F1E6F2E5B9E6F3E6F4C2E2E6F5E6F6D6E8E6F7F35AE6F8B9C7F35BF35CF35DF35EF35FF360F361F7BBF7BAF362F363F364F365F7BEF7BCBAA1F366F7BFF367F7C0F368F369F36AF7C2F7C1F7C4F36BF36CF7C3F36DF36EF36FF370F371F7C5F7C6F372F373F374F375F7C7F376CBE8F377F378F379F37AB8DFF37BF37CF37DF37EF380F381F7D4F382F7D5F383F384F385F386F7D6F387F388F389F38AF7D8F38BF7DAF38CF7D7F38DF38EF38FF390F391F392F393F394F395F7DBF396F7D9F397F398F399F39AF39BF39CF39DD7D7F39EF39FF3A0F440F7DCF441F442F443F444F445F446F7DDF447F448F449F7DEF44AF44BF44CF44DF44EF44FF450F451F452F453F454F7DFF455F456F457F7E0F458F459F45AF45BF45CF45DF45EF45FF460F461F462DBCBF463F464D8AAF465F466F467F468F469F46AF46BF46CE5F7B9EDF46DF46EF46FF470BFFDBBEAF7C9C6C7F7C8F471F7CAF7CCF7CBF472F473F474F7CDF475CEBAF476F7CEF477F478C4A7F479F47AF47BF47CF47DF47EF480F481F482F483F484F485F486F487F488F489F48AF48BF48CF48DF48EF48FF490F491F492F493F494F495F496F497F498F499F49AF49BF49CF49DF49EF49FF4A0F540F541F542F543F544F545F546F547F548F549F54AF54BF54CF54DF54EF54FF550F551F552F553F554F555F556F557F558F559F55AF55BF55CF55DF55EF55FF560F561F562F563F564F565F566F567F568F569F56AF56BF56CF56DF56EF56FF570F571F572F573F574F575F576F577F578F579F57AF57BF57CF57DF57EF580F581F582F583F584F585F586F587F588F589F58AF58BF58CF58DF58EF58FF590F591F592F593F594F595F596F597F598F599F59AF59BF59CF59DF59EF59FF5A0F640F641F642F643F644F645F646F647F648F649F64AF64BF64CF64DF64EF64FF650F651F652F653F654F655F656F657F658F659F65AF65BF65CF65DF65EF65FF660F661F662F663F664F665F666F667F668F669F66AF66BF66CF66DF66EF66FF670F671F672F673F674F675F676F677F678F679F67AF67BF67CF67DF67EF680F681F682F683F684F685F686F687F688F689F68AF68BF68CF68DF68EF68FF690F691F692F693F694F695F696F697F698F699F69AF69BF69CF69DF69EF69FF6A0F740F741F742F743F744F745F746F747F748F749F74AF74BF74CF74DF74EF74FF750F751F752F753F754F755F756F757F758F759F75AF75BF75CF75DF75EF75FF760F761F762F763F764F765F766F767F768F769F76AF76BF76CF76DF76EF76FF770F771F772F773F774F775F776F777F778F779F77AF77BF77CF77DF77EF780D3E3F781F782F6CFF783C2B3F6D0F784F785F6D1F6D2F6D3F6D4F786F787F6D6F788B1ABF6D7F789F6D8F6D9F6DAF78AF6DBF6DCF78BF78CF78DF78EF6DDF6DECFCAF78FF6DFF6E0F6E1F6E2F6E3F6E4C0F0F6E5F6E6F6E7F6E8F6E9F790F6EAF791F6EBF6ECF792F6EDF6EEF6EFF6F0F6F1F6F2F6F3F6F4BEA8F793F6F5F6F6F6F7F6F8F794F795F796F797F798C8FAF6F9F6FAF6FBF6FCF799F79AF6FDF6FEF7A1F7A2F7A3F7A4F7A5F79BF79CF7A6F7A7F7A8B1EEF7A9F7AAF7ABF79DF79EF7ACF7ADC1DBF7AEF79FF7A0F7AFF840F841F842F843F844F845F846F847F848F849F84AF84BF84CF84DF84EF84FF850F851F852F853F854F855F856F857F858F859F85AF85BF85CF85DF85EF85FF860F861F862F863F864F865F866F867F868F869F86AF86BF86CF86DF86EF86FF870F871F872F873F874F875F876F877F878F879F87AF87BF87CF87DF87EF880F881F882F883F884F885F886F887F888F889F88AF88BF88CF88DF88EF88FF890F891F892F893F894F895F896F897F898F899F89AF89BF89CF89DF89EF89FF8A0F940F941F942F943F944F945F946F947F948F949F94AF94BF94CF94DF94EF94FF950F951F952F953F954F955F956F957F958F959F95AF95BF95CF95DF95EF95FF960F961F962F963F964F965F966F967F968F969F96AF96BF96CF96DF96EF96FF970F971F972F973F974F975F976F977F978F979F97AF97BF97CF97DF97EF980F981F982F983F984F985F986F987F988F989F98AF98BF98CF98DF98EF98FF990F991F992F993F994F995F996F997F998F999F99AF99BF99CF99DF99EF99FF9A0FA40FA41FA42FA43FA44FA45FA46FA47FA48FA49FA4AFA4BFA4CFA4DFA4EFA4FFA50FA51FA52FA53FA54FA55FA56FA57FA58FA59FA5AFA5BFA5CFA5DFA5EFA5FFA60FA61FA62FA63FA64FA65FA66FA67FA68FA69FA6AFA6BFA6CFA6DFA6EFA6FFA70FA71FA72FA73FA74FA75FA76FA77FA78FA79FA7AFA7BFA7CFA7DFA7EFA80FA81FA82FA83FA84FA85FA86FA87FA88FA89FA8AFA8BFA8CFA8DFA8EFA8FFA90FA91FA92FA93FA94FA95FA96FA97FA98FA99FA9AFA9BFA9CFA9DFA9EFA9FFAA0FB40FB41FB42FB43FB44FB45FB46FB47FB48FB49FB4AFB4BFB4CFB4DFB4EFB4FFB50FB51FB52FB53FB54FB55FB56FB57FB58FB59FB5AFB5BC4F1F0AFBCA6F0B0C3F9FB5CC5B8D1BBFB5DF0B1F0B2F0B3F0B4F0B5D1BCFB5ED1ECFB5FF0B7F0B6D4A7FB60CDD2F0B8F0BAF0B9F0BBF0BCFB61FB62B8EBF0BDBAE8FB63F0BEF0BFBEE9F0C0B6ECF0C1F0C2F0C3F0C4C8B5F0C5F0C6FB64F0C7C5F4FB65F0C8FB66FB67FB68F0C9FB69F0CAF7BDFB6AF0CBF0CCF0CDFB6BF0CEFB6CFB6DFB6EFB6FF0CFBAD7FB70F0D0F0D1F0D2F0D3F0D4F0D5F0D6F0D8FB71FB72D3A5F0D7FB73F0D9FB74FB75FB76FB77FB78FB79FB7AFB7BFB7CFB7DF5BAC2B9FB7EFB80F7E4FB81FB82FB83FB84F7E5F7E6FB85FB86F7E7FB87FB88FB89FB8AFB8BFB8CF7E8C2B4FB8DFB8EFB8FFB90FB91FB92FB93FB94FB95F7EAFB96F7EBFB97FB98FB99FB9AFB9BFB9CC2F3FB9DFB9EFB9FFBA0FC40FC41FC42FC43FC44FC45FC46FC47FC48F4F0FC49FC4AFC4BF4EFFC4CFC4DC2E9FC4EF7E1F7E2FC4FFC50FC51FC52FC53BBC6FC54FC55FC56FC57D9E4FC58FC59FC5ACAF2C0E8F0A4FC5BBADAFC5CFC5DC7ADFC5EFC5FFC60C4ACFC61FC62F7ECF7EDF7EEFC63F7F0F7EFFC64F7F1FC65FC66F7F4FC67F7F3FC68F7F2F7F5FC69FC6AFC6BFC6CF7F6FC6DFC6EFC6FFC70FC71FC72FC73FC74FC75EDE9FC76EDEAEDEBFC77F6BCFC78FC79FC7AFC7BFC7CFC7DFC7EFC80FC81FC82FC83FC84F6BDFC85F6BEB6A6FC86D8BEFC87FC88B9C4FC89FC8AFC8BD8BBFC8CDCB1FC8DFC8EFC8FFC90FC91FC92CAF3FC93F7F7FC94FC95FC96FC97FC98FC99FC9AFC9BFC9CF7F8FC9DFC9EF7F9FC9FFCA0FD40FD41FD42FD43FD44F7FBFD45F7FAFD46B1C7FD47F7FCF7FDFD48FD49FD4AFD4BFD4CF7FEFD4DFD4EFD4FFD50FD51FD52FD53FD54FD55FD56FD57C6EBECB4FD58FD59FD5AFD5BFD5CFD5DFD5EFD5FFD60FD61FD62FD63FD64FD65FD66FD67FD68FD69FD6AFD6BFD6CFD6DFD6EFD6FFD70FD71FD72FD73FD74FD75FD76FD77FD78FD79FD7AFD7BFD7CFD7DFD7EFD80FD81FD82FD83FD84FD85B3DDF6B3FD86FD87F6B4C1E4F6B5F6B6F6B7F6B8F6B9F6BAC8A3F6BBFD88FD89FD8AFD8BFD8CFD8DFD8EFD8FFD90FD91FD92FD93C1FAB9A8EDE8FD94FD95FD96B9EAD9DFFD97FD98FD99FD9AFD9'; for (var i = 0; i < str.length; i++) { var c = str.charAt(i), code = str.charCodeAt(i); if (c == " ") strOut += "+"; else if (code >= 19968 && code <= 40869) { var index = code - 19968; strOut += "%" + z.substr(index * 4, 2) + "%" + z.substr(index * 4 + 2, 2); } else { strOut += "%" + str.charCodeAt(i).toString(16); } } return strOut; }, /* 改变图片大小 */ scale: function (img, w, h) { var ow = img.width, oh = img.height; if (ow >= oh) { img.width = w * ow / oh; img.height = h; img.style.marginLeft = '-' + parseInt((img.width - w) / 2) + 'px'; } else { img.width = w; img.height = h * oh / ow; img.style.marginTop = '-' + parseInt((img.height - h) / 2) + 'px'; } }, getImageData: function(){ var _this = this, key = $G('searchTxt').value, type = $G('searchType').value, keepOriginName = editor.options.keepOriginName ? "1" : "0", url = "http://image.baidu.com/i?ct=201326592&cl=2&lm=-1&st=-1&tn=baiduimagejson&istype=2&rn=32&fm=index&pv=&word=" + _this.encodeToGb2312(key) + type + "&keeporiginname=" + keepOriginName + "&" + +new Date; $G('searchListUl').innerHTML = lang.searchLoading; ajax.request(url, { 'dataType': 'jsonp', 'charset': 'GB18030', 'onsuccess':function(json){ var list = []; if(json && json.data) { for(var i = 0; i < json.data.length; i++) { if(json.data[i].objURL) { list.push({ title: json.data[i].fromPageTitleEnc, src: json.data[i].objURL, url: json.data[i].fromURL }); } } } _this.setList(list); }, 'onerror':function(){ $G('searchListUl').innerHTML = lang.searchRetry; } }); }, /* 添加图片到列表界面上 */ setList: function (list) { var i, item, p, img, link, _this = this, listUl = $G('searchListUl'); listUl.innerHTML = ''; if(list.length) { for (i = 0; i < list.length; i++) { item = document.createElement('li'); p = document.createElement('p'); img = document.createElement('img'); link = document.createElement('a'); img.onload = function () { _this.scale(this, 113, 113); }; img.width = 113; img.setAttribute('src', list[i].src); link.href = list[i].url; link.target = '_blank'; link.title = list[i].title; link.innerHTML = list[i].title; p.appendChild(img); item.appendChild(p); item.appendChild(link); listUl.appendChild(item); } } else { listUl.innerHTML = lang.searchRetry; } }, getInsertList: function () { var child, src, align = getAlign(), list = [], items = $G('searchListUl').children; for(var i = 0; i < items.length; i++) { child = items[i].firstChild && items[i].firstChild.firstChild; if(child.tagName && child.tagName.toLowerCase() == 'img' && domUtils.hasClass(items[i], 'selected')) { src = child.src; list.push({ src: src, _src: src, alt: src.substr(src.lastIndexOf('/') + 1), floatStyle: align }); } } return list; } }; })(); ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/insertframe/insertframe.html ================================================
    px
    px
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/internal.js ================================================ (function () { var parent = window.parent; //dialog对象 dialog = parent.$EDITORUI[window.frameElement.id.replace( /_iframe$/, '' )]; //当前打开dialog的编辑器实例 editor = dialog.editor; UE = parent.UE; domUtils = UE.dom.domUtils; utils = UE.utils; browser = UE.browser; ajax = UE.ajax; $G = function ( id ) { return document.getElementById( id ) }; //focus元素 $focus = function ( node ) { setTimeout( function () { if ( browser.ie ) { var r = node.createTextRange(); r.collapse( false ); r.select(); } else { node.focus() } }, 0 ) }; utils.loadFile(document,{ href:editor.options.themePath + editor.options.theme + "/dialogbase.css?cache="+Math.random(), tag:"link", type:"text/css", rel:"stylesheet" }); lang = editor.getLang(dialog.className.split( "-" )[2]); if(lang){ domUtils.on(window,'load',function () { var langImgPath = editor.options.langPath + editor.options.lang + "/images/"; //针对静态资源 for ( var i in lang["static"] ) { var dom = $G( i ); if(!dom) continue; var tagName = dom.tagName, content = lang["static"][i]; if(content.src){ //clone content = utils.extend({},content,false); content.src = langImgPath + content.src; } if(content.style){ content = utils.extend({},content,false); content.style = content.style.replace(/url\s*\(/g,"url(" + langImgPath) } switch ( tagName.toLowerCase() ) { case "var": dom.parentNode.replaceChild( document.createTextNode( content ), dom ); break; case "select": var ops = dom.options; for ( var j = 0, oj; oj = ops[j]; ) { oj.innerHTML = content.options[j++]; } for ( var p in content ) { p != "options" && dom.setAttribute( p, content[p] ); } break; default : domUtils.setAttributes( dom, content); } } } ); } })(); ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/link/link.html ================================================
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/map/map.html ================================================
    : :
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/map/show.html ================================================ 百度地图API自定义地图
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/music/music.css ================================================ .wrapper{margin: 5px 10px;} .searchBar{height:30px;padding:7px 0 3px;text-align:center;} .searchBtn{font-size:13px;height:24px;} .resultBar{width:460px;margin:5px auto;border: 1px solid #CCC;border-radius: 5px;box-shadow: 2px 2px 5px #D3D6DA;overflow: hidden;} .listPanel{overflow: hidden;} .panelon{display:block;} .paneloff{display:none} .page{width:220px;margin:20px auto;overflow: hidden;} .pageon{float:right;width:24px;line-height:24px;height:24px;margin-right: 5px;background: none;border: none;color: #000;font-weight: bold;text-align:center} .pageoff{float:right;width:24px;line-height:24px;height:24px;cursor:pointer;background-color: #fff; border: 1px solid #E7ECF0;color: #2D64B3;margin-right: 5px;text-decoration: none;text-align:center;} .m-box{width:460px;} .m-m{float: left;line-height: 20px;height: 20px;} .m-h{height:24px;line-height:24px;padding-left: 46px;background-color:#FAFAFA;border-bottom: 1px solid #DAD8D8;font-weight: bold;font-size: 12px;color: #333;} .m-l{float:left;width:40px; } .m-t{float:left;width:140px;} .m-s{float:left;width:110px;} .m-z{float:left;width:100px;} .m-try-t{float: left;width: 60px;;} .m-try{float:left;width:20px;height:20px;background:url('http://static.tieba.baidu.com/tb/editor/images/try_music.gif') no-repeat ;} .m-trying{float:left;width:20px;height:20px;background:url('http://static.tieba.baidu.com/tb/editor/images/stop_music.gif') no-repeat ;} .loading{width:95px;height:7px;font-size:7px;margin:60px auto;background:url(http://static.tieba.baidu.com/tb/editor/images/loading.gif) no-repeat} .empty{width:300px;height:40px;padding:2px;margin:50px auto;line-height:40px; color:#006699;text-align:center;} ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/music/music.html ================================================ 插入音乐
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/music/music.js ================================================ function Music() { this.init(); } (function () { var pages = [], panels = [], selectedItem = null; Music.prototype = { total:70, pageSize:10, dataUrl:"http://tingapi.ting.baidu.com/v1/restserver/ting?method=baidu.ting.search.common", playerUrl:"http://box.baidu.com/widget/flash/bdspacesong.swf", init:function () { var me = this; domUtils.on($G("J_searchName"), "keyup", function (event) { var e = window.event || event; if (e.keyCode == 13) { me.dosearch(); } }); domUtils.on($G("J_searchBtn"), "click", function () { me.dosearch(); }); }, callback:function (data) { var me = this; me.data = data.song_list; setTimeout(function () { $G('J_resultBar').innerHTML = me._renderTemplate(data.song_list); }, 300); }, dosearch:function () { var me = this; selectedItem = null; var key = $G('J_searchName').value; if (utils.trim(key) == "")return false; key = encodeURIComponent(key); me._sent(key); }, doselect:function (i) { var me = this; if (typeof i == 'object') { selectedItem = i; } else if (typeof i == 'number') { selectedItem = me.data[i]; } }, onpageclick:function (id) { var me = this; for (var i = 0; i < pages.length; i++) { $G(pages[i]).className = 'pageoff'; $G(panels[i]).className = 'paneloff'; } $G('page' + id).className = 'pageon'; $G('panel' + id).className = 'panelon'; }, listenTest:function (elem) { var me = this, view = $G('J_preview'), is_play_action = (elem.className == 'm-try'), old_trying = me._getTryingElem(); if (old_trying) { old_trying.className = 'm-try'; view.innerHTML = ''; } if (is_play_action) { elem.className = 'm-trying'; view.innerHTML = me._buildMusicHtml(me._getUrl(true)); } }, _sent:function (param) { var me = this; $G('J_resultBar').innerHTML = '
    '; utils.loadFile(document, { src:me.dataUrl + '&query=' + param + '&page_size=' + me.total + '&callback=music.callback&.r=' + Math.random(), tag:"script", type:"text/javascript", defer:"defer" }); }, _removeHtml:function (str) { var reg = /<\s*\/?\s*[^>]*\s*>/gi; return str.replace(reg, ""); }, _getUrl:function (isTryListen) { var me = this; var param = 'from=tiebasongwidget&url=&name=' + encodeURIComponent(me._removeHtml(selectedItem.title)) + '&artist=' + encodeURIComponent(me._removeHtml(selectedItem.author)) + '&extra=' + encodeURIComponent(me._removeHtml(selectedItem.album_title)) + '&autoPlay='+isTryListen+'' + '&loop=true'; return me.playerUrl + "?" + param; }, _getTryingElem:function () { var s = $G('J_listPanel').getElementsByTagName('span'); for (var i = 0; i < s.length; i++) { if (s[i].className == 'm-trying') return s[i]; } return null; }, _buildMusicHtml:function (playerUrl) { var html = ' 12) return s.substring(0, 5) + '...'; if (!s) s = " "; return s; }, _rebuildData:function (data) { var me = this, newData = [], d = me.pageSize, itembox; for (var i = 0; i < data.length; i++) { if ((i + d) % d == 0) { itembox = []; newData.push(itembox) } itembox.push(data[i]); } return newData; }, _renderTemplate:function (data) { var me = this; if (data.length == 0)return '
    ' + lang.emptyTxt + '
    '; data = me._rebuildData(data); var s = [], p = [], t = []; s.push('
    '); p.push('
    '); for (var i = 0, tmpList; tmpList = data[i++];) { panels.push('panel' + i); pages.push('page' + i); if (i == 1) { s.push('
    '); if (data.length != 1) { t.push('
    ' + (i ) + '
    '); } } else { s.push('
    '); t.push('
    ' + (i ) + '
    '); } s.push('
    '); s.push('
    ' + lang.chapter + '' + lang.singer + '' + lang.special + '' + lang.listenTest + '
    '); for (var j = 0, tmpObj; tmpObj = tmpList[j++];) { s.push(''); } s.push('
    '); s.push('
    '); } t.reverse(); p.push(t.join('')); s.push('
    '); p.push('
    '); return s.join('') + p.join(''); }, exec:function () { var me = this; if (selectedItem == null) return; $G('J_preview').innerHTML = ""; editor.execCommand('music', { url:me._getUrl(false), width:400, height:95 }); } }; })(); ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/preview/preview.html ================================================
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/scrawl/scrawl.css ================================================ /*common */ body{margin: 0;} table{width:100%;} table td{padding:2px 4px;vertical-align: middle;} a{text-decoration: none;} em{font-style: normal;} .border_style1{border: 1px solid #ccc;border-radius: 5px;box-shadow:2px 2px 5px #d3d6da;} /*module */ .main{margin: 8px;overflow: hidden;} .hot{float:left;height:335px;} .drawBoard{position: relative; cursor: crosshair;} .brushBorad{position: absolute;left:0;top:0;z-index: 998;} .picBoard{border: none;text-align: center;line-height: 300px;cursor: default;} .operateBar{margin-top:10px;font-size:12px;text-align: center;} .operateBar span{margin-left: 10px;} .drawToolbar{float:right;width:110px;height:300px;overflow: hidden;} .colorBar{margin-top:10px;font-size: 12px;text-align: center;} .colorBar a{display:block;width: 10px;height: 10px;border:1px solid #1006F1;border-radius: 3px; box-shadow:2px 2px 5px #d3d6da;opacity: 0.3} .sectionBar{margin-top:15px;font-size: 12px;text-align: center;} .sectionBar a{display:inline-block;width:10px;height:12px;color: #888;text-indent: -999px;opacity: 0.3} .size1{background: url('images/size.png') 1px center no-repeat ;} .size2{background: url('images/size.png') -10px center no-repeat;} .size3{background: url('images/size.png') -22px center no-repeat;} .size4{background: url('images/size.png') -35px center no-repeat;} .addImgH{position: relative;} .addImgH_form{position: absolute;left: 18px;top: -1px;width: 75px;height: 21px;opacity: 0;cursor: pointer;} .addImgH_form input{width: 100%;} /*scrawl遮罩层 */ .maskLayerNull{display: none;} .maskLayer{position: absolute;top:0;left:0;width: 100%; height: 100%;opacity: 0.7; background-color: #fff;text-align:center;font-weight:bold;line-height:300px;z-index: 1000;} /*btn state */ .previousStepH .icon{display: inline-block;width:16px;height:16px;background-image: url('images/undoH.png');cursor: pointer;} .previousStepH .text{color:#888;cursor:pointer;} .previousStep .icon{display: inline-block;width:16px;height:16px;background-image: url('images/undo.png');cursor:default;} .previousStep .text{color:#ccc;cursor:default;} .nextStepH .icon{display: inline-block;width:16px;height:16px;background-image: url('images/redoH.png');cursor: pointer;} .nextStepH .text{color:#888;cursor:pointer;} .nextStep .icon{display: inline-block;width:16px;height:16px;background-image: url('images/redo.png');cursor:default;} .nextStep .text{color:#ccc;cursor:default;} .clearBoardH .icon{display: inline-block;width:16px;height:16px;background-image: url('images/emptyH.png');cursor: pointer;} .clearBoardH .text{color:#888;cursor:pointer;} .clearBoard .icon{display: inline-block;width:16px;height:16px;background-image: url('images/empty.png');cursor:default;} .clearBoard .text{color:#ccc;cursor:default;} .scaleBoardH .icon{display: inline-block;width:16px;height:16px;background-image: url('images/scaleH.png');cursor: pointer;} .scaleBoardH .text{color:#888;cursor:pointer;} .scaleBoard .icon{display: inline-block;width:16px;height:16px;background-image: url('images/scale.png');cursor:default;} .scaleBoard .text{color:#ccc;cursor:default;} .removeImgH .icon{display: inline-block;width:16px;height:16px;background-image: url('images/delimgH.png');cursor: pointer;} .removeImgH .text{color:#888;cursor:pointer;} .removeImg .icon{display: inline-block;width:16px;height:16px;background-image: url('images/delimg.png');cursor:default;} .removeImg .text{color:#ccc;cursor:default;} .addImgH .icon{vertical-align:top;display: inline-block;width:16px;height:16px;background-image: url('images/addimg.png')} .addImgH .text{color:#888;cursor:pointer;} /*icon */ .brushIcon{display: inline-block;width:16px;height:16px;background-image: url('images/brush.png')} .eraserIcon{display: inline-block;width:16px;height:16px;background-image: url('images/eraser.png')} ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/scrawl/scrawl.html ================================================
    1 3 5 7
    1 3 5 7
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/scrawl/scrawl.js ================================================ /** * Created with JetBrains PhpStorm. * User: xuheng * Date: 12-5-22 * Time: 上午11:38 * To change this template use File | Settings | File Templates. */ var scrawl = function (options) { options && this.initOptions(options); }; (function () { var canvas = $G("J_brushBoard"), context = canvas.getContext('2d'), drawStep = [], //undo redo存储 drawStepIndex = 0; //undo redo指针 scrawl.prototype = { isScrawl:false, //是否涂鸦 brushWidth:-1, //画笔粗细 brushColor:"", //画笔颜色 initOptions:function (options) { var me = this; me.originalState(options);//初始页面状态 me._buildToolbarColor(options.colorList);//动态生成颜色选择集合 me._addBoardListener(options.saveNum);//添加画板处理 me._addOPerateListener(options.saveNum);//添加undo redo clearBoard处理 me._addColorBarListener();//添加颜色选择处理 me._addBrushBarListener();//添加画笔大小处理 me._addEraserBarListener();//添加橡皮大小处理 me._addAddImgListener();//添加增添背景图片处理 me._addRemoveImgListenter();//删除背景图片处理 me._addScalePicListenter();//添加缩放处理 me._addClearSelectionListenter();//添加清楚选中状态处理 me._originalColorSelect(options.drawBrushColor);//初始化颜色选中 me._originalBrushSelect(options.drawBrushSize);//初始化画笔选中 me._clearSelection();//清楚选中状态 }, originalState:function (options) { var me = this; me.brushWidth = options.drawBrushSize;//同步画笔粗细 me.brushColor = options.drawBrushColor;//同步画笔颜色 context.lineWidth = me.brushWidth;//初始画笔大小 context.strokeStyle = me.brushColor;//初始画笔颜色 context.fillStyle = "transparent";//初始画布背景颜色 context.lineCap = "round";//去除锯齿 context.fill(); }, _buildToolbarColor:function (colorList) { var tmp = null, arr = []; arr.push(""); for (var i = 0, color; color = colorList[i++];) { if ((i - 1) % 5 == 0) { if (i != 1) { arr.push(""); } arr.push(""); } tmp = '#' + color; arr.push(""); } arr.push("
    "); $G("J_colorBar").innerHTML = arr.join(""); }, _addBoardListener:function (saveNum) { var me = this, margin = 0, startX = -1, startY = -1, isMouseDown = false, isMouseMove = false, isMouseUp = false, buttonPress = 0, button, flag = ''; margin = parseInt(domUtils.getComputedStyle($G("J_wrap"), "margin-left")); drawStep.push(context.getImageData(0, 0, context.canvas.width, context.canvas.height)); drawStepIndex += 1; domUtils.on(canvas, ["mousedown", "mousemove", "mouseup", "mouseout"], function (e) { button = browser.webkit ? e.which : buttonPress; switch (e.type) { case 'mousedown': buttonPress = 1; flag = 1; isMouseDown = true; isMouseUp = false; isMouseMove = false; me.isScrawl = true; startX = e.clientX - margin;//10为外边距总和 startY = e.clientY - margin; context.beginPath(); break; case 'mousemove' : if (!flag && button == 0) { return; } if (!flag && button) { startX = e.clientX - margin;//10为外边距总和 startY = e.clientY - margin; context.beginPath(); flag = 1; } if (isMouseUp || !isMouseDown) { return; } var endX = e.clientX - margin, endY = e.clientY - margin; context.moveTo(startX, startY); context.lineTo(endX, endY); context.stroke(); startX = endX; startY = endY; isMouseMove = true; break; case 'mouseup': buttonPress = 0; if (!isMouseDown)return; if (!isMouseMove) { context.arc(startX, startY, context.lineWidth, 0, Math.PI * 2, false); context.fillStyle = context.strokeStyle; context.fill(); } context.closePath(); me._saveOPerate(saveNum); isMouseDown = false; isMouseMove = false; isMouseUp = true; startX = -1; startY = -1; break; case 'mouseout': flag = ''; buttonPress = 0; if (button == 1) return; context.closePath(); break; } }); }, _addOPerateListener:function (saveNum) { var me = this; domUtils.on($G("J_previousStep"), "click", function () { if (drawStepIndex > 1) { drawStepIndex -= 1; context.clearRect(0, 0, context.canvas.width, context.canvas.height); context.putImageData(drawStep[drawStepIndex - 1], 0, 0); me.btn2Highlight("J_nextStep"); drawStepIndex == 1 && me.btn2disable("J_previousStep"); } }); domUtils.on($G("J_nextStep"), "click", function () { if (drawStepIndex > 0 && drawStepIndex < drawStep.length) { context.clearRect(0, 0, context.canvas.width, context.canvas.height); context.putImageData(drawStep[drawStepIndex], 0, 0); drawStepIndex += 1; me.btn2Highlight("J_previousStep"); drawStepIndex == drawStep.length && me.btn2disable("J_nextStep"); } }); domUtils.on($G("J_clearBoard"), "click", function () { context.clearRect(0, 0, context.canvas.width, context.canvas.height); drawStep = []; me._saveOPerate(saveNum); drawStepIndex = 1; me.isScrawl = false; me.btn2disable("J_previousStep"); me.btn2disable("J_nextStep"); me.btn2disable("J_clearBoard"); }); }, _addColorBarListener:function () { var me = this; domUtils.on($G("J_colorBar"), "click", function (e) { var target = me.getTarget(e), color = target.title; if (!!color) { me._addColorSelect(target); me.brushColor = color; context.globalCompositeOperation = "source-over"; context.lineWidth = me.brushWidth; context.strokeStyle = color; } }); }, _addBrushBarListener:function () { var me = this; domUtils.on($G("J_brushBar"), "click", function (e) { var target = me.getTarget(e), size = browser.ie ? target.innerText : target.text; if (!!size) { me._addBESelect(target); context.globalCompositeOperation = "source-over"; context.lineWidth = parseInt(size); context.strokeStyle = me.brushColor; me.brushWidth = context.lineWidth; } }); }, _addEraserBarListener:function () { var me = this; domUtils.on($G("J_eraserBar"), "click", function (e) { var target = me.getTarget(e), size = browser.ie ? target.innerText : target.text; if (!!size) { me._addBESelect(target); context.lineWidth = parseInt(size); context.globalCompositeOperation = "destination-out"; context.strokeStyle = "#FFF"; } }); }, _addAddImgListener:function () { var file = $G("J_imgTxt"); if (!window.FileReader) { $G("J_addImg").style.display = 'none'; $G("J_removeImg").style.display = 'none'; $G("J_sacleBoard").style.display = 'none'; } domUtils.on(file, "change", function (e) { var frm = file.parentNode; addMaskLayer(lang.backgroundUploading); var target = e.target || e.srcElement, reader = new FileReader(); reader.onload = function(evt){ var target = evt.target || evt.srcElement; ue_callback(target.result, 'SUCCESS'); }; reader.readAsDataURL(target.files[0]); frm.reset(); }); }, _addRemoveImgListenter:function () { var me = this; domUtils.on($G("J_removeImg"), "click", function () { $G("J_picBoard").innerHTML = ""; me.btn2disable("J_removeImg"); me.btn2disable("J_sacleBoard"); }); }, _addScalePicListenter:function () { domUtils.on($G("J_sacleBoard"), "click", function () { var picBoard = $G("J_picBoard"), scaleCon = $G("J_scaleCon"), img = picBoard.children[0]; if (img) { if (!scaleCon) { picBoard.style.cssText = "position:relative;z-index:999;"+picBoard.style.cssText; img.style.cssText = "position: absolute;top:" + (canvas.height - img.height) / 2 + "px;left:" + (canvas.width - img.width) / 2 + "px;"; var scale = new ScaleBoy(); picBoard.appendChild(scale.init()); scale.startScale(img); } else { if (scaleCon.style.visibility == "visible") { scaleCon.style.visibility = "hidden"; picBoard.style.position = ""; picBoard.style.zIndex = ""; } else { scaleCon.style.visibility = "visible"; picBoard.style.cssText += "position:relative;z-index:999"; } } } }); }, _addClearSelectionListenter:function () { var doc = document; domUtils.on(doc, 'mousemove', function (e) { if (browser.ie && browser.version < 11) doc.selection.clear(); else window.getSelection().removeAllRanges(); }); }, _clearSelection:function () { var list = ["J_operateBar", "J_colorBar", "J_brushBar", "J_eraserBar", "J_picBoard"]; for (var i = 0, group; group = list[i++];) { domUtils.unSelectable($G(group)); } }, _saveOPerate:function (saveNum) { var me = this; if (drawStep.length <= saveNum) { if(drawStepIndex"); } scale.innerHTML = arr.join(""); return scale; } var rect = [ //[left, top, width, height] [1, 1, -1, -1], [0, 1, 0, -1], [0, 1, 1, -1], [1, 0, -1, 0], [0, 0, 1, 0], [1, 0, -1, 1], [0, 0, 0, 1], [0, 0, 1, 1] ]; ScaleBoy.prototype = { init:function () { _appendStyle(); var me = this, scale = me.dom = _getDom(); me.scaleMousemove.fp = me; domUtils.on(scale, 'mousedown', function (e) { var target = e.target || e.srcElement; me.start = {x:e.clientX, y:e.clientY}; if (target.className.indexOf('hand') != -1) { me.dir = target.className.replace('hand', ''); } domUtils.on(document.body, 'mousemove', me.scaleMousemove); e.stopPropagation ? e.stopPropagation() : e.cancelBubble = true; }); domUtils.on(document.body, 'mouseup', function (e) { if (me.start) { domUtils.un(document.body, 'mousemove', me.scaleMousemove); if (me.moved) { me.updateScaledElement({position:{x:scale.style.left, y:scale.style.top}, size:{w:scale.style.width, h:scale.style.height}}); } delete me.start; delete me.moved; delete me.dir; } }); return scale; }, startScale:function (objElement) { var me = this, Idom = me.dom; Idom.style.cssText = 'visibility:visible;top:' + objElement.style.top + ';left:' + objElement.style.left + ';width:' + objElement.offsetWidth + 'px;height:' + objElement.offsetHeight + 'px;'; me.scalingElement = objElement; }, updateScaledElement:function (objStyle) { var cur = this.scalingElement, pos = objStyle.position, size = objStyle.size; if (pos) { typeof pos.x != 'undefined' && (cur.style.left = pos.x); typeof pos.y != 'undefined' && (cur.style.top = pos.y); } if (size) { size.w && (cur.style.width = size.w); size.h && (cur.style.height = size.h); } }, updateStyleByDir:function (dir, offset) { var me = this, dom = me.dom, tmp; rect['def'] = [1, 1, 0, 0]; if (rect[dir][0] != 0) { tmp = parseInt(dom.style.left) + offset.x; dom.style.left = me._validScaledProp('left', tmp) + 'px'; } if (rect[dir][1] != 0) { tmp = parseInt(dom.style.top) + offset.y; dom.style.top = me._validScaledProp('top', tmp) + 'px'; } if (rect[dir][2] != 0) { tmp = dom.clientWidth + rect[dir][2] * offset.x; dom.style.width = me._validScaledProp('width', tmp) + 'px'; } if (rect[dir][3] != 0) { tmp = dom.clientHeight + rect[dir][3] * offset.y; dom.style.height = me._validScaledProp('height', tmp) + 'px'; } if (dir === 'def') { me.updateScaledElement({position:{x:dom.style.left, y:dom.style.top}}); } }, scaleMousemove:function (e) { var me = arguments.callee.fp, start = me.start, dir = me.dir || 'def', offset = {x:e.clientX - start.x, y:e.clientY - start.y}; me.updateStyleByDir(dir, offset); arguments.callee.fp.start = {x:e.clientX, y:e.clientY}; arguments.callee.fp.moved = 1; }, _validScaledProp:function (prop, value) { var ele = this.dom, wrap = $G("J_picBoard"); value = isNaN(value) ? 0 : value; switch (prop) { case 'left': return value < 0 ? 0 : (value + ele.clientWidth) > wrap.clientWidth ? wrap.clientWidth - ele.clientWidth : value; case 'top': return value < 0 ? 0 : (value + ele.clientHeight) > wrap.clientHeight ? wrap.clientHeight - ele.clientHeight : value; case 'width': return value <= 0 ? 1 : (value + ele.offsetLeft) > wrap.clientWidth ? wrap.clientWidth - ele.offsetLeft : value; case 'height': return value <= 0 ? 1 : (value + ele.offsetTop) > wrap.clientHeight ? wrap.clientHeight - ele.offsetTop : value; } } }; })(); //后台回调 function ue_callback(url, state) { var doc = document, picBorard = $G("J_picBoard"), img = doc.createElement("img"); //图片缩放 function scale(img, max, oWidth, oHeight) { var width = 0, height = 0, percent, ow = img.width || oWidth, oh = img.height || oHeight; if (ow > max || oh > max) { if (ow >= oh) { if (width = ow - max) { percent = (width / ow).toFixed(2); img.height = oh - oh * percent; img.width = max; } } else { if (height = oh - max) { percent = (height / oh).toFixed(2); img.width = ow - ow * percent; img.height = max; } } } } //移除遮罩层 removeMaskLayer(); //状态响应 if (state == "SUCCESS") { picBorard.innerHTML = ""; img.onload = function () { scale(this, 300); picBorard.appendChild(img); var obj = new scrawl(); obj.btn2Highlight("J_removeImg"); //trace 2457 obj.btn2Highlight("J_sacleBoard"); }; img.src = url; } else { alert(state); } } //去掉遮罩层 function removeMaskLayer() { var maskLayer = $G("J_maskLayer"); maskLayer.className = "maskLayerNull"; maskLayer.innerHTML = ""; dialog.buttons[0].setDisabled(false); } //添加遮罩层 function addMaskLayer(html) { var maskLayer = $G("J_maskLayer"); dialog.buttons[0].setDisabled(true); maskLayer.className = "maskLayer"; maskLayer.innerHTML = html; } //执行确认按钮方法 function exec(scrawlObj) { if (scrawlObj.isScrawl) { addMaskLayer(lang.scrawlUpLoading); var base64 = scrawlObj.getCanvasData(); if (!!base64) { var options = { timeout:100000, onsuccess:function (xhr) { if (!scrawlObj.isCancelScrawl) { var responseObj; responseObj = eval("(" + xhr.responseText + ")"); if (responseObj.state == "SUCCESS") { var imgObj = {}, url = editor.options.scrawlUrlPrefix + responseObj.url; imgObj.src = url; imgObj._src = url; imgObj.alt = responseObj.original || ''; imgObj.title = responseObj.title || ''; editor.execCommand("insertImage", imgObj); dialog.close(); } else { alert(responseObj.state); } } }, onerror:function () { alert(lang.imageError); dialog.close(); } }; options[editor.getOpt('scrawlFieldName')] = base64; var actionUrl = editor.getActionUrl(editor.getOpt('scrawlActionName')), params = utils.serializeParam(editor.queryCommandValue('serverparam')) || '', url = utils.formatUrl(actionUrl + (actionUrl.indexOf('?') == -1 ? '?':'&') + params); ajax.request(url, options); } } else { addMaskLayer(lang.noScarwl + "   "); } } ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/searchreplace/searchreplace.html ================================================
    :
     
    :
    :
     
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/searchreplace/searchreplace.js ================================================ /** * Created with JetBrains PhpStorm. * User: xuheng * Date: 12-9-26 * Time: 下午12:29 * To change this template use File | Settings | File Templates. */ //清空上次查选的痕迹 editor.firstForSR = 0; editor.currentRangeForSR = null; //给tab注册切换事件 /** * tab点击处理事件 * @param tabHeads * @param tabBodys * @param obj */ function clickHandler( tabHeads,tabBodys,obj ) { //head样式更改 for ( var k = 0, len = tabHeads.length; k < len; k++ ) { tabHeads[k].className = ""; } obj.className = "focus"; //body显隐 var tabSrc = obj.getAttribute( "tabSrc" ); for ( var j = 0, length = tabBodys.length; j < length; j++ ) { var body = tabBodys[j], id = body.getAttribute( "id" ); if ( id != tabSrc ) { body.style.zIndex = 1; } else { body.style.zIndex = 200; } } } /** * TAB切换 * @param tabParentId tab的父节点ID或者对象本身 */ function switchTab( tabParentId ) { var tabElements = $G( tabParentId ).children, tabHeads = tabElements[0].children, tabBodys = tabElements[1].children; for ( var i = 0, length = tabHeads.length; i < length; i++ ) { var head = tabHeads[i]; if ( head.className === "focus" )clickHandler(tabHeads,tabBodys, head ); head.onclick = function () { clickHandler(tabHeads,tabBodys,this); } } } $G('searchtab').onmousedown = function(){ $G('search-msg').innerHTML = ''; $G('replace-msg').innerHTML = '' } //是否区分大小写 function getMatchCase(id) { return $G(id).checked ? true : false; } //查找 $G("nextFindBtn").onclick = function (txt, dir, mcase) { var findtxt = $G("findtxt").value, obj; if (!findtxt) { return false; } obj = { searchStr:findtxt, dir:1, casesensitive:getMatchCase("matchCase") }; if (!frCommond(obj)) { var bk = editor.selection.getRange().createBookmark(); $G('search-msg').innerHTML = lang.getEnd; editor.selection.getRange().moveToBookmark(bk).select(); } }; $G("nextReplaceBtn").onclick = function (txt, dir, mcase) { var findtxt = $G("findtxt1").value, obj; if (!findtxt) { return false; } obj = { searchStr:findtxt, dir:1, casesensitive:getMatchCase("matchCase1") }; frCommond(obj); }; $G("preFindBtn").onclick = function (txt, dir, mcase) { var findtxt = $G("findtxt").value, obj; if (!findtxt) { return false; } obj = { searchStr:findtxt, dir:-1, casesensitive:getMatchCase("matchCase") }; if (!frCommond(obj)) { $G('search-msg').innerHTML = lang.getStart; } }; $G("preReplaceBtn").onclick = function (txt, dir, mcase) { var findtxt = $G("findtxt1").value, obj; if (!findtxt) { return false; } obj = { searchStr:findtxt, dir:-1, casesensitive:getMatchCase("matchCase1") }; frCommond(obj); }; //替换 $G("repalceBtn").onclick = function () { var findtxt = $G("findtxt1").value.replace(/^\s|\s$/g, ""), obj, replacetxt = $G("replacetxt").value.replace(/^\s|\s$/g, ""); if (!findtxt) { return false; } if (findtxt == replacetxt || (!getMatchCase("matchCase1") && findtxt.toLowerCase() == replacetxt.toLowerCase())) { return false; } obj = { searchStr:findtxt, dir:1, casesensitive:getMatchCase("matchCase1"), replaceStr:replacetxt }; frCommond(obj); }; //全部替换 $G("repalceAllBtn").onclick = function () { var findtxt = $G("findtxt1").value.replace(/^\s|\s$/g, ""), obj, replacetxt = $G("replacetxt").value.replace(/^\s|\s$/g, ""); if (!findtxt) { return false; } if (findtxt == replacetxt || (!getMatchCase("matchCase1") && findtxt.toLowerCase() == replacetxt.toLowerCase())) { return false; } obj = { searchStr:findtxt, casesensitive:getMatchCase("matchCase1"), replaceStr:replacetxt, all:true }; var num = frCommond(obj); if (num) { $G('replace-msg').innerHTML = lang.countMsg.replace("{#count}", num); } }; //执行 var frCommond = function (obj) { return editor.execCommand("searchreplace", obj); }; switchTab("searchtab"); ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/snapscreen/snapscreen.html ================================================

    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/spechars/spechars.html ================================================
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/spechars/spechars.js ================================================ /** * Created with JetBrains PhpStorm. * User: xuheng * Date: 12-9-26 * Time: 下午1:09 * To change this template use File | Settings | File Templates. */ var charsContent = [ { name:"tsfh", title:lang.tsfh, content:toArray("、,。,·,ˉ,ˇ,¨,〃,々,—,~,‖,…,‘,’,“,”,〔,〕,〈,〉,《,》,「,」,『,』,〖,〗,【,】,±,×,÷,∶,∧,∨,∑,∏,∪,∩,∈,∷,√,⊥,∥,∠,⌒,⊙,∫,∮,≡,≌,≈,∽,∝,≠,≮,≯,≤,≥,∞,∵,∴,♂,♀,°,′,″,℃,$,¤,¢,£,‰,§,№,☆,★,○,●,◎,◇,◆,□,■,△,▲,※,→,←,↑,↓,〓,〡,〢,〣,〤,〥,〦,〧,〨,〩,㊣,㎎,㎏,㎜,㎝,㎞,㎡,㏄,㏎,㏑,㏒,㏕,︰,¬,¦,℡,ˊ,ˋ,˙,–,―,‥,‵,℅,℉,↖,↗,↘,↙,∕,∟,∣,≒,≦,≧,⊿,═,║,╒,╓,╔,╕,╖,╗,╘,╙,╚,╛,╜,╝,╞,╟,╠,╡,╢,╣,╤,╥,╦,╧,╨,╩,╪,╫,╬,╭,╮,╯,╰,╱,╲,╳,▁,▂,▃,▄,▅,▆,▇,�,█,▉,▊,▋,▌,▍,▎,▏,▓,▔,▕,▼,▽,◢,◣,◤,◥,☉,⊕,〒,〝,〞")}, { name:"lmsz", title:lang.lmsz, content:toArray("ⅰ,ⅱ,ⅲ,ⅳ,ⅴ,ⅵ,ⅶ,ⅷ,ⅸ,ⅹ,Ⅰ,Ⅱ,Ⅲ,Ⅳ,Ⅴ,Ⅵ,Ⅶ,Ⅷ,Ⅸ,Ⅹ,Ⅺ,Ⅻ")}, { name:"szfh", title:lang.szfh, content:toArray("⒈,⒉,⒊,⒋,⒌,⒍,⒎,⒏,⒐,⒑,⒒,⒓,⒔,⒕,⒖,⒗,⒘,⒙,⒚,⒛,⑴,⑵,⑶,⑷,⑸,⑹,⑺,⑻,⑼,⑽,⑾,⑿,⒀,⒁,⒂,⒃,⒄,⒅,⒆,⒇,①,②,③,④,⑤,⑥,⑦,⑧,⑨,⑩,㈠,㈡,㈢,㈣,㈤,㈥,㈦,㈧,㈨,㈩")}, { name:"rwfh", title:lang.rwfh, content:toArray("ぁ,あ,ぃ,い,ぅ,う,ぇ,え,ぉ,お,か,が,き,ぎ,く,ぐ,け,げ,こ,ご,さ,ざ,し,じ,す,ず,せ,ぜ,そ,ぞ,た,だ,ち,ぢ,っ,つ,づ,て,で,と,ど,な,に,ぬ,ね,の,は,ば,ぱ,ひ,び,ぴ,ふ,ぶ,ぷ,へ,べ,ぺ,ほ,ぼ,ぽ,ま,み,む,め,も,ゃ,や,ゅ,ゆ,ょ,よ,ら,り,る,れ,ろ,ゎ,わ,ゐ,ゑ,を,ん,ァ,ア,ィ,イ,ゥ,ウ,ェ,エ,ォ,オ,カ,ガ,キ,ギ,ク,グ,ケ,ゲ,コ,ゴ,サ,ザ,シ,ジ,ス,ズ,セ,ゼ,ソ,ゾ,タ,ダ,チ,ヂ,ッ,ツ,ヅ,テ,デ,ト,ド,ナ,ニ,ヌ,ネ,ノ,ハ,バ,パ,ヒ,ビ,ピ,フ,ブ,プ,ヘ,ベ,ペ,ホ,ボ,ポ,マ,ミ,ム,メ,モ,ャ,ヤ,ュ,ユ,ョ,ヨ,ラ,リ,ル,レ,ロ,ヮ,ワ,ヰ,ヱ,ヲ,ン,ヴ,ヵ,ヶ")}, { name:"xlzm", title:lang.xlzm, content:toArray("Α,Β,Γ,Δ,Ε,Ζ,Η,Θ,Ι,Κ,Λ,Μ,Ν,Ξ,Ο,Π,Ρ,Σ,Τ,Υ,Φ,Χ,Ψ,Ω,α,β,γ,δ,ε,ζ,η,θ,ι,κ,λ,μ,ν,ξ,ο,π,ρ,σ,τ,υ,φ,χ,ψ,ω")}, { name:"ewzm", title:lang.ewzm, content:toArray("А,Б,В,Г,Д,Е,Ё,Ж,З,И,Й,К,Л,М,Н,О,П,Р,С,Т,У,Ф,Х,Ц,Ч,Ш,Щ,Ъ,Ы,Ь,Э,Ю,Я,а,б,в,г,д,е,ё,ж,з,и,й,к,л,м,н,о,п,р,с,т,у,ф,х,ц,ч,ш,щ,ъ,ы,ь,э,ю,я")}, { name:"pyzm", title:lang.pyzm, content:toArray("ā,á,ǎ,à,ē,é,ě,è,ī,í,ǐ,ì,ō,ó,ǒ,ò,ū,ú,ǔ,ù,ǖ,ǘ,ǚ,ǜ,ü")}, { name:"yyyb", title:lang.yyyb, content:toArray("i:,i,e,æ,ʌ,ə:,ə,u:,u,ɔ:,ɔ,a:,ei,ai,ɔi,əu,au,iə,εə,uə,p,t,k,b,d,g,f,s,ʃ,θ,h,v,z,ʒ,ð,tʃ,tr,ts,dʒ,dr,dz,m,n,ŋ,l,r,w,j,")}, { name:"zyzf", title:lang.zyzf, content:toArray("ㄅ,ㄆ,ㄇ,ㄈ,ㄉ,ㄊ,ㄋ,ㄌ,ㄍ,ㄎ,ㄏ,ㄐ,ㄑ,ㄒ,ㄓ,ㄔ,ㄕ,ㄖ,ㄗ,ㄘ,ㄙ,ㄚ,ㄛ,ㄜ,ㄝ,ㄞ,ㄟ,ㄠ,ㄡ,ㄢ,ㄣ,ㄤ,ㄥ,ㄦ,ㄧ,ㄨ")} ]; (function createTab(content) { for (var i = 0, ci; ci = content[i++];) { var span = document.createElement("span"); span.setAttribute("tabSrc", ci.name); span.innerHTML = ci.title; if (i == 1)span.className = "focus"; domUtils.on(span, "click", function () { var tmps = $G("tabHeads").children; for (var k = 0, sk; sk = tmps[k++];) { sk.className = ""; } tmps = $G("tabBodys").children; for (var k = 0, sk; sk = tmps[k++];) { sk.style.display = "none"; } this.className = "focus"; $G(this.getAttribute("tabSrc")).style.display = ""; }); $G("tabHeads").appendChild(span); domUtils.insertAfter(span, document.createTextNode("\n")); var div = document.createElement("div"); div.id = ci.name; div.style.display = (i == 1) ? "" : "none"; var cons = ci.content; for (var j = 0, con; con = cons[j++];) { var charSpan = document.createElement("span"); charSpan.innerHTML = con; domUtils.on(charSpan, "click", function () { editor.execCommand("insertHTML", this.innerHTML); dialog.close(); }); div.appendChild(charSpan); } $G("tabBodys").appendChild(div); } })(charsContent); function toArray(str) { return str.split(","); } ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/table/edittable.css ================================================ body{ overflow: hidden; width: 540px; } .wrapper { margin: 10px auto 0; font-size: 12px; overflow: hidden; width: 520px; height: 315px; } .clear { clear: both; } .wrapper .left { float: left; margin-left: 10px;; } .wrapper .right { float: right; border-left: 2px dotted #EDEDED; padding-left: 15px; } .section { margin-bottom: 15px; width: 240px; overflow: hidden; } .section h3 { font-weight: bold; padding: 5px 0; margin-bottom: 10px; border-bottom: 1px solid #EDEDED; font-size: 12px; } .section ul { list-style: none; overflow: hidden; clear: both; } .section li { float: left; width: 120px;; } .section .tone { width: 80px;; } .section .preview { width: 220px; } .section .preview table { text-align: center; vertical-align: middle; color: #666; } .section .preview caption { font-weight: bold; } .section .preview td { border-width: 1px; border-style: solid; height: 22px; } .section .preview th { border-style: solid; border-color: #DDD; border-width: 2px 1px 1px 1px; height: 22px; background-color: #F7F7F7; } ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/table/edittable.html ================================================

    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/table/edittable.js ================================================ /** * Created with JetBrains PhpStorm. * User: xuheng * Date: 12-12-19 * Time: 下午4:55 * To change this template use File | Settings | File Templates. */ (function () { var title = $G("J_title"), titleCol = $G("J_titleCol"), caption = $G("J_caption"), sorttable = $G("J_sorttable"), autoSizeContent = $G("J_autoSizeContent"), autoSizePage = $G("J_autoSizePage"), tone = $G("J_tone"), me, preview = $G("J_preview"); var editTable = function () { me = this; me.init(); }; editTable.prototype = { init:function () { var colorPiker = new UE.ui.ColorPicker({ editor:editor }), colorPop = new UE.ui.Popup({ editor:editor, content:colorPiker }); title.checked = editor.queryCommandState("inserttitle") == -1; titleCol.checked = editor.queryCommandState("inserttitlecol") == -1; caption.checked = editor.queryCommandState("insertcaption") == -1; sorttable.checked = editor.queryCommandState("enablesort") == 1; var enablesortState = editor.queryCommandState("enablesort"), disablesortState = editor.queryCommandState("disablesort"); sorttable.checked = !!(enablesortState < 0 && disablesortState >=0); sorttable.disabled = !!(enablesortState < 0 && disablesortState < 0); sorttable.title = enablesortState < 0 && disablesortState < 0 ? lang.errorMsg:''; me.createTable(title.checked, titleCol.checked, caption.checked); me.setAutoSize(); me.setColor(me.getColor()); domUtils.on(title, "click", me.titleHanler); domUtils.on(titleCol, "click", me.titleColHanler); domUtils.on(caption, "click", me.captionHanler); domUtils.on(sorttable, "click", me.sorttableHanler); domUtils.on(autoSizeContent, "click", me.autoSizeContentHanler); domUtils.on(autoSizePage, "click", me.autoSizePageHanler); domUtils.on(tone, "click", function () { colorPop.showAnchor(tone); }); domUtils.on(document, 'mousedown', function () { colorPop.hide(); }); colorPiker.addListener("pickcolor", function () { me.setColor(arguments[1]); colorPop.hide(); }); colorPiker.addListener("picknocolor", function () { me.setColor(""); colorPop.hide(); }); }, createTable:function (hasTitle, hasTitleCol, hasCaption) { var arr = [], sortSpan = '^'; arr.push(""); if (hasCaption) { arr.push("") } if (hasTitle) { arr.push(""); if(hasTitleCol) { arr.push(""); } for (var j = 0; j < 5; j++) { arr.push(""); } arr.push(""); } for (var i = 0; i < 6; i++) { arr.push(""); if(hasTitleCol) { arr.push("") } for (var k = 0; k < 5; k++) { arr.push("") } arr.push(""); } arr.push("
    " + lang.captionName + "
    " + lang.titleName + "" + lang.titleName + "
    " + lang.titleName + "" + lang.cellsName + "
    "); preview.innerHTML = arr.join(""); this.updateSortSpan(); }, titleHanler:function () { var example = $G("J_example"), frg=document.createDocumentFragment(), color = domUtils.getComputedStyle(domUtils.getElementsByTagName(example, "td")[0], "border-color"), colCount = example.rows[0].children.length; if (title.checked) { example.insertRow(0); for (var i = 0, node; i < colCount; i++) { node = document.createElement("th"); node.innerHTML = lang.titleName; frg.appendChild(node); } example.rows[0].appendChild(frg); } else { domUtils.remove(example.rows[0]); } me.setColor(color); me.updateSortSpan(); }, titleColHanler:function () { var example = $G("J_example"), color = domUtils.getComputedStyle(domUtils.getElementsByTagName(example, "td")[0], "border-color"), colArr = example.rows, colCount = colArr.length; if (titleCol.checked) { for (var i = 0, node; i < colCount; i++) { node = document.createElement("th"); node.innerHTML = lang.titleName; colArr[i].insertBefore(node, colArr[i].children[0]); } } else { for (var i = 0; i < colCount; i++) { domUtils.remove(colArr[i].children[0]); } } me.setColor(color); me.updateSortSpan(); }, captionHanler:function () { var example = $G("J_example"); if (caption.checked) { var row = document.createElement('caption'); row.innerHTML = lang.captionName; example.insertBefore(row, example.firstChild); } else { domUtils.remove(domUtils.getElementsByTagName(example, 'caption')[0]); } }, sorttableHanler:function(){ me.updateSortSpan(); }, autoSizeContentHanler:function () { var example = $G("J_example"); example.removeAttribute("width"); }, autoSizePageHanler:function () { var example = $G("J_example"); var tds = example.getElementsByTagName(example, "td"); utils.each(tds, function (td) { td.removeAttribute("width"); }); example.setAttribute('width', '100%'); }, updateSortSpan: function(){ var example = $G("J_example"), row = example.rows[0]; var spans = domUtils.getElementsByTagName(example,"span"); utils.each(spans,function(span){ span.parentNode.removeChild(span); }); if (sorttable.checked) { utils.each(row.cells, function(cell, i){ var span = document.createElement("span"); span.innerHTML = "^"; cell.appendChild(span); }); } }, getColor:function () { var start = editor.selection.getStart(), color, cell = domUtils.findParentByTagName(start, ["td", "th", "caption"], true); color = cell && domUtils.getComputedStyle(cell, "border-color"); if (!color) color = "#DDDDDD"; return color; }, setColor:function (color) { var example = $G("J_example"), arr = domUtils.getElementsByTagName(example, "td").concat( domUtils.getElementsByTagName(example, "th"), domUtils.getElementsByTagName(example, "caption") ); tone.value = color; utils.each(arr, function (node) { node.style.borderColor = color; }); }, setAutoSize:function () { var me = this; autoSizePage.checked = true; me.autoSizePageHanler(); } }; new editTable; dialog.onok = function () { editor.__hasEnterExecCommand = true; var checks = { title:"inserttitle deletetitle", titleCol:"inserttitlecol deletetitlecol", caption:"insertcaption deletecaption", sorttable:"enablesort disablesort" }; editor.fireEvent('saveScene'); for(var i in checks){ var cmds = checks[i].split(" "), input = $G("J_" + i); if(input["checked"]){ editor.queryCommandState(cmds[0])!=-1 &&editor.execCommand(cmds[0]); }else{ editor.queryCommandState(cmds[1])!=-1 &&editor.execCommand(cmds[1]); } } editor.execCommand("edittable", tone.value); autoSizeContent.checked ?editor.execCommand('adaptbytext') : ""; autoSizePage.checked ? editor.execCommand("adaptbywindow") : ""; editor.fireEvent('saveScene'); editor.__hasEnterExecCommand = false; }; })(); ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/table/edittd.html ================================================
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/table/edittip.html ================================================ 表格删除提示
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/template/config.js ================================================ /** * Created with JetBrains PhpStorm. * User: xuheng * Date: 12-8-8 * Time: 下午2:00 * To change this template use File | Settings | File Templates. */ var templates = [ { "pre":"pre0.png", 'title':lang.blank, 'preHtml':'

     欢迎使用UEditor!

    ', "html":'

    欢迎使用UEditor!

    ' }, { "pre":"pre1.png", 'title':lang.blog, 'preHtml':'

    深入理解Range

    UEditor二次开发

    什么是Range

    对于“插入”选项卡上的库,在设计时都充分考虑了其中的项与文档整体外观的协调性。


    Range能干什么

    在“开始”选项卡上,通过从快速样式库中为所选文本选择一种外观,您可以方便地更改文档中所选文本的格式。

    ', "html":'

    [键入文档标题]

    [键入文档副标题]

    [标题 1]

    对于“插入”选项卡上的库,在设计时都充分考虑了其中的项与文档整体外观的协调性。 您可以使用这些库来插入表格、页眉、页脚、列表、封面以及其他文档构建基块。 您创建的图片、图表或关系图也将与当前的文档外观协调一致。

    [标题 2]

    在“开始”选项卡上,通过从快速样式库中为所选文本选择一种外观,您可以方便地更改文档中所选文本的格式。 您还可以使用“开始”选项卡上的其他控件来直接设置文本格式。大多数控件都允许您选择是使用当前主题外观,还是使用某种直接指定的格式。

    [标题 3]

    对于“插入”选项卡上的库,在设计时都充分考虑了其中的项与文档整体外观的协调性。 您可以使用这些库来插入表格、页眉、页脚、列表、封面以及其他文档构建基块。 您创建的图片、图表或关系图也将与当前的文档外观协调一致。


    ' }, { "pre":"pre2.png", 'title':lang.resume, 'preHtml':'

    WEB前端开发简历


    联系电话:[键入您的电话]

    电子邮件:[键入您的电子邮件地址]

    家庭住址:[键入您的地址]

    目标职位

    WEB前端研发工程师

    学历

    1. [起止时间] [学校名称] [所学专业] [所获学位]

    工作经验


    ', "html":'

    [此处键入简历标题]


    【此处插入照片】


    联系电话:[键入您的电话]


    电子邮件:[键入您的电子邮件地址]


    家庭住址:[键入您的地址]


    目标职位

    [此处键入您的期望职位]

    学历

    1. [键入起止时间] [键入学校名称] [键入所学专业] [键入所获学位]

    2. [键入起止时间] [键入学校名称] [键入所学专业] [键入所获学位]

    工作经验

    1. [键入起止时间] [键入公司名称] [键入职位名称]

      1. [键入负责项目] [键入项目简介]

      2. [键入负责项目] [键入项目简介]

    2. [键入起止时间] [键入公司名称] [键入职位名称]

      1. [键入负责项目] [键入项目简介]

    掌握技能

     [这里可以键入您所掌握的技能]

    ' }, { "pre":"pre3.png", 'title':lang.richText, 'preHtml':'

    [此处键入文章标题]

    图文混排方法

    图片居左,文字围绕图片排版

    方法:在文字前面插入图片,设置居左对齐,然后即可在右边输入多行文


    还有没有什么其他的环绕方式呢?这里是居右环绕


    欢迎大家多多尝试,为UEditor提供更多高质量模板!

    ', "html":'


    [此处键入文章标题]

    图文混排方法

    1. 图片居左,文字围绕图片排版

    方法:在文字前面插入图片,设置居左对齐,然后即可在右边输入多行文本


    2. 图片居右,文字围绕图片排版

    方法:在文字前面插入图片,设置居右对齐,然后即可在左边输入多行文本


    3. 图片居中环绕排版

    方法:亲,这个真心没有办法。。。



    还有没有什么其他的环绕方式呢?这里是居右环绕


    欢迎大家多多尝试,为UEditor提供更多高质量模板!


    占位


    占位


    占位


    占位


    占位



    ' }, { "pre":"pre4.png", 'title':lang.sciPapers, 'preHtml':'

    [键入文章标题]

    摘要:这里可以输入很长很长很长很长很长很长很长很长很差的摘要

    标题 1

    这里可以输入很多内容,可以图文混排,可以有列表等。

    标题 2

    1. 列表 1

    2. 列表 2

      1. 多级列表 1

      2. 多级列表 2

    3. 列表 3

    标题 3

    来个文字图文混排的


    ', 'html':'

    [键入文章标题]

    摘要:这里可以输入很长很长很长很长很长很长很长很长很差的摘要

    标题 1

    这里可以输入很多内容,可以图文混排,可以有列表等。

    标题 2

    来个列表瞅瞅:

    1. 列表 1

    2. 列表 2

      1. 多级列表 1

      2. 多级列表 2

    3. 列表 3

    标题 3

    来个文字图文混排的

    这里可以多行

    右边是图片

    绝对没有问题的,不信你也可以试试看


    ' } ]; ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/template/template.css ================================================ .wrap{ padding: 5px;font-size: 14px;} .left{width:425px;float: left;} .right{width:160px;border: 1px solid #ccc;float: right;padding: 5px;margin-right: 5px;} .right .pre{height: 332px;overflow-y: auto;} .right .preitem{border: white 1px solid;margin: 5px 0;padding: 2px 0;} .right .preitem:hover{background-color: lemonChiffon;cursor: pointer;border: #ccc 1px solid;} .right .preitem img{display: block;margin: 0 auto;width:100px;} .clear{clear: both;} .top{height:26px;line-height: 26px;padding: 5px;} .bottom{height:320px;width:100%;margin: 0 auto;} .transparent{ background: url("images/bg.gif") repeat;} .bottom table tr td{border:1px dashed #ccc;} #colorPicker{width: 17px;height: 17px;border: 1px solid #CCC;display: inline-block;border-radius: 3px;box-shadow: 2px 2px 5px #D3D6DA;} .border_style1{padding:2px;border: 1px solid #ccc;border-radius: 5px;box-shadow:2px 2px 5px #d3d6da;} p{margin: 5px 0} table{clear:both;margin-bottom:10px;border-collapse:collapse;word-break:break-all;} li{clear:both} ol{padding-left:40px; } ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/template/template.html ================================================
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/template/template.js ================================================ /** * Created with JetBrains PhpStorm. * User: xuheng * Date: 12-8-8 * Time: 下午2:09 * To change this template use File | Settings | File Templates. */ (function () { var me = editor, preview = $G( "preview" ), preitem = $G( "preitem" ), tmps = templates, currentTmp; var initPre = function () { var str = ""; for ( var i = 0, tmp; tmp = tmps[i++]; ) { str += '
    '; } preitem.innerHTML = str; }; var pre = function ( n ) { var tmp = tmps[n - 1]; currentTmp = tmp; clearItem(); domUtils.setStyles( preitem.childNodes[n - 1], { "background-color":"lemonChiffon", "border":"#ccc 1px solid" } ); preview.innerHTML = tmp.preHtml ? tmp.preHtml : ""; }; var clearItem = function () { var items = preitem.children; for ( var i = 0, item; item = items[i++]; ) { domUtils.setStyles( item, { "background-color":"", "border":"white 1px solid" } ); } }; dialog.onok = function () { if ( !$G( "issave" ).checked ){ me.execCommand( "cleardoc" ); } var obj = { html:currentTmp && currentTmp.html }; me.execCommand( "template", obj ); }; initPre(); window.pre = pre; pre(2) })(); ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/video/video.css ================================================ @charset "utf-8"; .wrapper{ width: 570px;_width:575px;margin: 10px auto; zoom:1;position: relative} .tabbody{height: 335px;} .tabbody .panel { position: absolute; width: 0; height: 0; background: #fff; overflow: hidden; display: none; } .tabbody .panel.focus { width: 100%; height: 335px; display: block; } .tabbody .panel table td{vertical-align: middle;} #videoUrl { width: 490px; height: 21px; line-height: 21px; margin: 8px 5px; background: #FFF; border: 1px solid #d7d7d7; } #videoSearchTxt{margin-left:15px;background: #FFF;width:200px;height:21px;line-height:21px;border: 1px solid #d7d7d7;} #searchList{width: 570px;overflow: auto;zoom:1;height: 270px;} #searchList div{float: left;width: 120px;height: 135px;margin: 5px 15px;} #searchList img{margin: 2px 8px;cursor: pointer;border: 2px solid #fff} /*不用缩略图*/ #searchList p{margin-left: 10px;} #videoType{ width: 65px; height: 23px; line-height: 22px; border: 1px solid #d7d7d7; } #videoSearchBtn,#videoSearchReset{ /*width: 80px;*/ height: 25px; line-height: 25px; background: #eee; border: 1px solid #d7d7d7; cursor: pointer; padding: 0 5px; } #preview{position: relative;width: 420px;padding:0;overflow: hidden; margin-left: 10px; _margin-left:5px; height: 280px;background-color: #ddd;float: left} #preview .previewMsg {position:absolute;top:0;margin:0;padding:0;height:280px;width:100%;background-color: #666;} #preview .previewMsg span{display:block;margin: 125px auto 0 auto;text-align:center;font-size:18px;color:#fff;} #preview .previewVideo {position:absolute;top:0;margin:0;padding:0;height:280px;width:100%;} .edui-video-wrapper fieldset{ border: 1px solid #ddd; padding-left: 5px; margin-bottom: 20px; padding-bottom: 5px; width: 115px; } #videoInfo {width: 120px;float: left;margin-left: 10px;_margin-left:7px;} fieldset{ border: 1px solid #ddd; padding-left: 5px; margin-bottom: 20px; padding-bottom: 5px; width: 115px; } fieldset legend{font-weight: bold;} fieldset p{line-height: 30px;} fieldset input.txt{ width: 65px; height: 21px; line-height: 21px; margin: 8px 5px; background: #FFF; border: 1px solid #d7d7d7; } label.url{font-weight: bold;margin-left: 5px;color: #06c;} #videoFloat div{cursor:pointer;opacity: 0.5;filter: alpha(opacity = 50);margin:9px;_margin:5px;width:38px;height:36px;float:left;} #videoFloat .focus{opacity: 1;filter: alpha(opacity = 100)} span.view{display: inline-block;width: 30px;float: right;cursor: pointer;color: blue} /* upload video */ .tabbody #upload.panel { width: 0; height: 0; overflow: hidden; position: absolute !important; clip: rect(1px, 1px, 1px, 1px); background: #fff; display: block; } .tabbody #upload.panel.focus { width: 100%; height: 335px; display: block; clip: auto; } #upload_alignment div{cursor:pointer;opacity: 0.5;filter: alpha(opacity = 50);margin:9px;_margin:5px;width:38px;height:36px;float:left;} #upload_alignment .focus{opacity: 1;filter: alpha(opacity = 100)} #upload_left { width:427px; float:left; } #upload_left .controller { height: 30px; clear: both; } #uploadVideoInfo{margin-top:10px;float:right;padding-right:8px;} #upload .queueList { margin: 0; } #upload p { margin: 0; } .element-invisible { width: 0 !important; height: 0 !important; border: 0; padding: 0; margin: 0; overflow: hidden; position: absolute !important; clip: rect(1px, 1px, 1px, 1px); } #upload .placeholder { margin: 10px; margin-right:0; border: 2px dashed #e6e6e6; *border: 0px dashed #e6e6e6; height: 161px; padding-top: 150px; text-align: center; width: 97%; float: left; background: url(./images/image.png) center 70px no-repeat; color: #cccccc; font-size: 18px; position: relative; top:0; *margin-left: 0; *left: 10px; } #upload .placeholder .webuploader-pick { font-size: 18px; background: #00b7ee; border-radius: 3px; line-height: 44px; padding: 0 30px; *width: 120px; color: #fff; display: inline-block; margin: 0 auto 20px auto; cursor: pointer; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); } #upload .placeholder .webuploader-pick-hover { background: #00a2d4; } #filePickerContainer { text-align: center; } #upload .placeholder .flashTip { color: #666666; font-size: 12px; position: absolute; width: 100%; text-align: center; bottom: 20px; } #upload .placeholder .flashTip a { color: #0785d1; text-decoration: none; } #upload .placeholder .flashTip a:hover { text-decoration: underline; } #upload .placeholder.webuploader-dnd-over { border-color: #999999; } #upload .filelist { list-style: none; margin: 0; padding: 0; overflow-x: hidden; overflow-y: auto; position: relative; height: 285px; } #upload .filelist:after { content: ''; display: block; width: 0; height: 0; overflow: hidden; clear: both; } #upload .filelist li { width: 113px; height: 113px; background: url(./images/bg.png); text-align: center; margin: 15px 0 0 20px; *margin: 15px 0 0 15px; position: relative; display: block; float: left; overflow: hidden; font-size: 12px; } #upload .filelist li p.log { position: relative; top: -45px; } #upload .filelist li p.title { position: absolute; top: 0; left: 0; width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; top: 5px; text-indent: 5px; text-align: left; } #upload .filelist li p.progress { position: absolute; width: 100%; bottom: 0; left: 0; height: 8px; overflow: hidden; z-index: 50; margin: 0; border-radius: 0; background: none; -webkit-box-shadow: 0 0 0; } #upload .filelist li p.progress span { display: none; overflow: hidden; width: 0; height: 100%; background: #1483d8 url(./images/progress.png) repeat-x; -webit-transition: width 200ms linear; -moz-transition: width 200ms linear; -o-transition: width 200ms linear; -ms-transition: width 200ms linear; transition: width 200ms linear; -webkit-animation: progressmove 2s linear infinite; -moz-animation: progressmove 2s linear infinite; -o-animation: progressmove 2s linear infinite; -ms-animation: progressmove 2s linear infinite; animation: progressmove 2s linear infinite; -webkit-transform: translateZ(0); } @-webkit-keyframes progressmove { 0% { background-position: 0 0; } 100% { background-position: 17px 0; } } @-moz-keyframes progressmove { 0% { background-position: 0 0; } 100% { background-position: 17px 0; } } @keyframes progressmove { 0% { background-position: 0 0; } 100% { background-position: 17px 0; } } #upload .filelist li p.imgWrap { position: relative; z-index: 2; line-height: 113px; vertical-align: middle; overflow: hidden; width: 113px; height: 113px; -webkit-transform-origin: 50% 50%; -moz-transform-origin: 50% 50%; -o-transform-origin: 50% 50%; -ms-transform-origin: 50% 50%; transform-origin: 50% 50%; -webit-transition: 200ms ease-out; -moz-transition: 200ms ease-out; -o-transition: 200ms ease-out; -ms-transition: 200ms ease-out; transition: 200ms ease-out; } #upload .filelist li p.imgWrap.notimage { margin-top: 0; width: 111px; height: 111px; border: 1px #eeeeee solid; } #upload .filelist li p.imgWrap.notimage i.file-preview { margin-top: 15px; } #upload .filelist li img { width: 100%; } #upload .filelist li p.error { background: #f43838; color: #fff; position: absolute; bottom: 0; left: 0; height: 28px; line-height: 28px; width: 100%; z-index: 100; display:none; } #upload .filelist li .success { display: block; position: absolute; left: 0; bottom: 0; height: 40px; width: 100%; z-index: 200; background: url(./images/success.png) no-repeat right bottom; background-image: url(./images/success.gif) \9; } #upload .filelist li.filePickerBlock { width: 113px; height: 113px; background: url(./images/image.png) no-repeat center 12px; border: 1px solid #eeeeee; border-radius: 0; } #upload .filelist li.filePickerBlock div.webuploader-pick { width: 100%; height: 100%; margin: 0; padding: 0; opacity: 0; background: none; font-size: 0; } #upload .filelist div.file-panel { position: absolute; height: 0; filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#80000000', endColorstr='#80000000') \0; background: rgba(0, 0, 0, 0.5); width: 100%; top: 0; left: 0; overflow: hidden; z-index: 300; } #upload .filelist div.file-panel span { width: 24px; height: 24px; display: inline; float: right; text-indent: -9999px; overflow: hidden; background: url(./images/icons.png) no-repeat; background: url(./images/icons.gif) no-repeat \9; margin: 5px 1px 1px; cursor: pointer; -webkit-tap-highlight-color: rgba(0,0,0,0); -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } #upload .filelist div.file-panel span.rotateLeft { display:none; background-position: 0 -24px; } #upload .filelist div.file-panel span.rotateLeft:hover { background-position: 0 0; } #upload .filelist div.file-panel span.rotateRight { display:none; background-position: -24px -24px; } #upload .filelist div.file-panel span.rotateRight:hover { background-position: -24px 0; } #upload .filelist div.file-panel span.cancel { background-position: -48px -24px; } #upload .filelist div.file-panel span.cancel:hover { background-position: -48px 0; } #upload .statusBar { height: 45px; border-bottom: 1px solid #dadada; margin: 0 10px; padding: 0; line-height: 45px; vertical-align: middle; position: relative; } #upload .statusBar .progress { border: 1px solid #1483d8; width: 198px; background: #fff; height: 18px; position: absolute; top: 12px; display: none; text-align: center; line-height: 18px; color: #6dbfff; margin: 0 10px 0 0; } #upload .statusBar .progress span.percentage { width: 0; height: 100%; left: 0; top: 0; background: #1483d8; position: absolute; } #upload .statusBar .progress span.text { position: relative; z-index: 10; } #upload .statusBar .info { display: inline-block; font-size: 14px; color: #666666; } #upload .statusBar .btns { position: absolute; top: 7px; right: 0; line-height: 30px; } #filePickerBtn { display: inline-block; float: left; } #upload .statusBar .btns .webuploader-pick, #upload .statusBar .btns .uploadBtn, #upload .statusBar .btns .uploadBtn.state-uploading, #upload .statusBar .btns .uploadBtn.state-paused { background: #ffffff; border: 1px solid #cfcfcf; color: #565656; padding: 0 18px; display: inline-block; border-radius: 3px; margin-left: 10px; cursor: pointer; font-size: 14px; float: left; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } #upload .statusBar .btns .webuploader-pick-hover, #upload .statusBar .btns .uploadBtn:hover, #upload .statusBar .btns .uploadBtn.state-uploading:hover, #upload .statusBar .btns .uploadBtn.state-paused:hover { background: #f0f0f0; } #upload .statusBar .btns .uploadBtn, #upload .statusBar .btns .uploadBtn.state-paused{ background: #00b7ee; color: #fff; border-color: transparent; } #upload .statusBar .btns .uploadBtn:hover, #upload .statusBar .btns .uploadBtn.state-paused:hover{ background: #00a2d4; } #upload .statusBar .btns .uploadBtn.disabled { pointer-events: none; filter:alpha(opacity=60); -moz-opacity:0.6; -khtml-opacity: 0.6; opacity: 0.6; } /* 在线文件的文件预览图标 */ i.file-preview { display: block; margin: 10px auto; width: 70px; height: 70px; background-image: url("./images/file-icons.png"); background-image: url("./images/file-icons.gif") \9; background-position: -140px center; background-repeat: no-repeat; } i.file-preview.file-type-dir{ background-position: 0 center; } i.file-preview.file-type-file{ background-position: -140px center; } i.file-preview.file-type-filelist{ background-position: -210px center; } i.file-preview.file-type-zip, i.file-preview.file-type-rar, i.file-preview.file-type-7z, i.file-preview.file-type-tar, i.file-preview.file-type-gz, i.file-preview.file-type-bz2{ background-position: -280px center; } i.file-preview.file-type-xls, i.file-preview.file-type-xlsx{ background-position: -350px center; } i.file-preview.file-type-doc, i.file-preview.file-type-docx{ background-position: -420px center; } i.file-preview.file-type-ppt, i.file-preview.file-type-pptx{ background-position: -490px center; } i.file-preview.file-type-vsd{ background-position: -560px center; } i.file-preview.file-type-pdf{ background-position: -630px center; } i.file-preview.file-type-txt, i.file-preview.file-type-md, i.file-preview.file-type-json, i.file-preview.file-type-htm, i.file-preview.file-type-xml, i.file-preview.file-type-html, i.file-preview.file-type-js, i.file-preview.file-type-css, i.file-preview.file-type-php, i.file-preview.file-type-jsp, i.file-preview.file-type-asp{ background-position: -700px center; } i.file-preview.file-type-apk{ background-position: -770px center; } i.file-preview.file-type-exe{ background-position: -840px center; } i.file-preview.file-type-ipa{ background-position: -910px center; } i.file-preview.file-type-mp4, i.file-preview.file-type-swf, i.file-preview.file-type-mkv, i.file-preview.file-type-avi, i.file-preview.file-type-flv, i.file-preview.file-type-mov, i.file-preview.file-type-mpg, i.file-preview.file-type-mpeg, i.file-preview.file-type-ogv, i.file-preview.file-type-webm, i.file-preview.file-type-rm, i.file-preview.file-type-rmvb{ background-position: -980px center; } i.file-preview.file-type-ogg, i.file-preview.file-type-wav, i.file-preview.file-type-wmv, i.file-preview.file-type-mid, i.file-preview.file-type-mp3{ background-position: -1050px center; } i.file-preview.file-type-jpg, i.file-preview.file-type-jpeg, i.file-preview.file-type-gif, i.file-preview.file-type-bmp, i.file-preview.file-type-png, i.file-preview.file-type-psd{ background-position: -140px center; } ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/video/video.html ================================================
    0%
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/video/video.js ================================================ /** * Created by JetBrains PhpStorm. * User: taoqili * Date: 12-2-20 * Time: 上午11:19 * To change this template use File | Settings | File Templates. */ (function(){ var video = {}, uploadVideoList = [], isModifyUploadVideo = false, uploadFile; window.onload = function(){ $focus($G("videoUrl")); initTabs(); initVideo(); initUpload(); }; /* 初始化tab标签 */ function initTabs(){ var tabs = $G('tabHeads').children; for (var i = 0; i < tabs.length; i++) { domUtils.on(tabs[i], "click", function (e) { var j, bodyId, target = e.target || e.srcElement; for (j = 0; j < tabs.length; j++) { bodyId = tabs[j].getAttribute('data-content-id'); if(tabs[j] == target){ domUtils.addClass(tabs[j], 'focus'); domUtils.addClass($G(bodyId), 'focus'); }else { domUtils.removeClasses(tabs[j], 'focus'); domUtils.removeClasses($G(bodyId), 'focus'); } } }); } } function initVideo(){ createAlignButton( ["videoFloat", "upload_alignment"] ); addUrlChangeListener($G("videoUrl")); addOkListener(); //编辑视频时初始化相关信息 (function(){ var img = editor.selection.getRange().getClosedNode(),url; if(img && img.className){ var hasFakedClass = (img.className == "edui-faked-video"), hasUploadClass = img.className.indexOf("edui-upload-video")!=-1; if(hasFakedClass || hasUploadClass) { $G("videoUrl").value = url = img.getAttribute("_url"); $G("videoWidth").value = img.width; $G("videoHeight").value = img.height; var align = domUtils.getComputedStyle(img,"float"), parentAlign = domUtils.getComputedStyle(img.parentNode,"text-align"); updateAlignButton(parentAlign==="center"?"center":align); } if(hasUploadClass) { isModifyUploadVideo = true; } } createPreviewVideo(url); })(); } /** * 监听确认和取消两个按钮事件,用户执行插入或者清空正在播放的视频实例操作 */ function addOkListener(){ dialog.onok = function(){ $G("preview").innerHTML = ""; var currentTab = findFocus("tabHeads","tabSrc"); switch(currentTab){ case "video": return insertSingle(); break; case "videoSearch": return insertSearch("searchList"); break; case "upload": return insertUpload(); break; } }; dialog.oncancel = function(){ $G("preview").innerHTML = ""; }; } /** * 依据传入的align值更新按钮信息 * @param align */ function updateAlignButton( align ) { var aligns = $G( "videoFloat" ).children; for ( var i = 0, ci; ci = aligns[i++]; ) { if ( ci.getAttribute( "name" ) == align ) { if ( ci.className !="focus" ) { ci.className = "focus"; } } else { if ( ci.className =="focus" ) { ci.className = ""; } } } } /** * 将单个视频信息插入编辑器中 */ function insertSingle(){ var width = $G("videoWidth"), height = $G("videoHeight"), url=$G('videoUrl').value, align = findFocus("videoFloat","name"); if(!url) return false; if ( !checkNum( [width, height] ) ) return false; editor.execCommand('insertvideo', { url: convert_url(url), width: width.value, height: height.value, align: align }, isModifyUploadVideo ? 'upload':null); } /** * 将元素id下的所有代表视频的图片插入编辑器中 * @param id */ function insertSearch(id){ var imgs = domUtils.getElementsByTagName($G(id),"img"), videoObjs=[]; for(var i=0,img; img=imgs[i++];){ if(img.getAttribute("selected")){ videoObjs.push({ url:img.getAttribute("ue_video_url"), width:420, height:280, align:"none" }); } } editor.execCommand('insertvideo',videoObjs); } /** * 找到id下具有focus类的节点并返回该节点下的某个属性 * @param id * @param returnProperty */ function findFocus( id, returnProperty ) { var tabs = $G( id ).children, property; for ( var i = 0, ci; ci = tabs[i++]; ) { if ( ci.className=="focus" ) { property = ci.getAttribute( returnProperty ); break; } } return property; } function convert_url(url){ if ( !url ) return ''; url = utils.trim(url) .replace(/v\.youku\.com\/v_show\/id_([\w\-=]+)\.html/i, 'player.youku.com/player.php/sid/$1/v.swf') .replace(/(www\.)?youtube\.com\/watch\?v=([\w\-]+)/i, "www.youtube.com/v/$2") .replace(/youtu.be\/(\w+)$/i, "www.youtube.com/v/$1") .replace(/v\.ku6\.com\/.+\/([\w\.]+)\.html.*$/i, "player.ku6.com/refer/$1/v.swf") .replace(/www\.56\.com\/u\d+\/v_([\w\-]+)\.html/i, "player.56.com/v_$1.swf") .replace(/www.56.com\/w\d+\/play_album\-aid\-\d+_vid\-([^.]+)\.html/i, "player.56.com/v_$1.swf") .replace(/v\.pps\.tv\/play_([\w]+)\.html.*$/i, "player.pps.tv/player/sid/$1/v.swf") .replace(/www\.letv\.com\/ptv\/vplay\/([\d]+)\.html.*$/i, "i7.imgs.letv.com/player/swfPlayer.swf?id=$1&autoplay=0") .replace(/www\.tudou\.com\/programs\/view\/([\w\-]+)\/?/i, "www.tudou.com/v/$1") .replace(/v\.qq\.com\/cover\/[\w]+\/[\w]+\/([\w]+)\.html/i, "static.video.qq.com/TPout.swf?vid=$1") .replace(/v\.qq\.com\/.+[\?\&]vid=([^&]+).*$/i, "static.video.qq.com/TPout.swf?vid=$1") .replace(/my\.tv\.sohu\.com\/[\w]+\/[\d]+\/([\d]+)\.shtml.*$/i, "share.vrs.sohu.com/my/v.swf&id=$1"); return url; } /** * 检测传入的所有input框中输入的长宽是否是正数 * @param nodes input框集合, */ function checkNum( nodes ) { for ( var i = 0, ci; ci = nodes[i++]; ) { var value = ci.value; if ( !isNumber( value ) && value) { alert( lang.numError ); ci.value = ""; ci.focus(); return false; } } return true; } /** * 数字判断 * @param value */ function isNumber( value ) { return /(0|^[1-9]\d*$)/.test( value ); } /** * 创建图片浮动选择按钮 * @param ids */ function createAlignButton( ids ) { for ( var i = 0, ci; ci = ids[i++]; ) { var floatContainer = $G( ci ), nameMaps = {"none":lang['default'], "left":lang.floatLeft, "right":lang.floatRight, "center":lang.block}; for ( var j in nameMaps ) { var div = document.createElement( "div" ); div.setAttribute( "name", j ); if ( j == "none" ) div.className="focus"; div.style.cssText = "background:url(images/" + j + "_focus.jpg);"; div.setAttribute( "title", nameMaps[j] ); floatContainer.appendChild( div ); } switchSelect( ci ); } } /** * 选择切换 * @param selectParentId */ function switchSelect( selectParentId ) { var selects = $G( selectParentId ).children; for ( var i = 0, ci; ci = selects[i++]; ) { domUtils.on( ci, "click", function () { for ( var j = 0, cj; cj = selects[j++]; ) { cj.className = ""; cj.removeAttribute && cj.removeAttribute( "class" ); } this.className = "focus"; } ) } } /** * 监听url改变事件 * @param url */ function addUrlChangeListener(url){ if (browser.ie) { url.onpropertychange = function () { createPreviewVideo( this.value ); } } else { url.addEventListener( "input", function () { createPreviewVideo( this.value ); }, false ); } } /** * 根据url生成视频预览 * @param url */ function createPreviewVideo(url){ if ( !url )return; var conUrl = convert_url(url); conUrl = utils.unhtmlForUrl(conUrl); $G("preview").innerHTML = '
    '+lang.urlError+'
    '+ '' + ''; } /* 插入上传视频 */ function insertUpload(){ var videoObjs=[], uploadDir = editor.getOpt('videoUrlPrefix'), width = parseInt($G('upload_width').value, 10) || 420, height = parseInt($G('upload_height').value, 10) || 280, align = findFocus("upload_alignment","name") || 'none'; for(var key in uploadVideoList) { var file = uploadVideoList[key]; videoObjs.push({ url: uploadDir + file.url, width:width, height:height, align:align }); } var count = uploadFile.getQueueCount(); if (count) { $('.info', '#queueList').html('' + '还有2个未上传文件'.replace(/[\d]/, count) + ''); return false; } else { editor.execCommand('insertvideo', videoObjs, 'upload'); } } /*初始化上传标签*/ function initUpload(){ uploadFile = new UploadFile('queueList'); } /* 上传附件 */ function UploadFile(target) { this.$wrap = target.constructor == String ? $('#' + target) : $(target); this.init(); } UploadFile.prototype = { init: function () { this.fileList = []; this.initContainer(); this.initUploader(); }, initContainer: function () { this.$queue = this.$wrap.find('.filelist'); }, /* 初始化容器 */ initUploader: function () { var _this = this, $ = jQuery, // just in case. Make sure it's not an other libaray. $wrap = _this.$wrap, // 图片容器 $queue = $wrap.find('.filelist'), // 状态栏,包括进度和控制按钮 $statusBar = $wrap.find('.statusBar'), // 文件总体选择信息。 $info = $statusBar.find('.info'), // 上传按钮 $upload = $wrap.find('.uploadBtn'), // 上传按钮 $filePickerBtn = $wrap.find('.filePickerBtn'), // 上传按钮 $filePickerBlock = $wrap.find('.filePickerBlock'), // 没选择文件之前的内容。 $placeHolder = $wrap.find('.placeholder'), // 总体进度条 $progress = $statusBar.find('.progress').hide(), // 添加的文件数量 fileCount = 0, // 添加的文件总大小 fileSize = 0, // 优化retina, 在retina下这个值是2 ratio = window.devicePixelRatio || 1, // 缩略图大小 thumbnailWidth = 113 * ratio, thumbnailHeight = 113 * ratio, // 可能有pedding, ready, uploading, confirm, done. state = '', // 所有文件的进度信息,key为file id percentages = {}, supportTransition = (function () { var s = document.createElement('p').style, r = 'transition' in s || 'WebkitTransition' in s || 'MozTransition' in s || 'msTransition' in s || 'OTransition' in s; s = null; return r; })(), // WebUploader实例 uploader, actionUrl = editor.getActionUrl(editor.getOpt('videoActionName')), fileMaxSize = editor.getOpt('videoMaxSize'), acceptExtensions = (editor.getOpt('videoAllowFiles') || []).join('').replace(/\./g, ',').replace(/^[,]/, '');; if (!WebUploader.Uploader.support()) { $('#filePickerReady').after($('
    ').html(lang.errorNotSupport)).hide(); return; } else if (!editor.getOpt('videoActionName')) { $('#filePickerReady').after($('
    ').html(lang.errorLoadConfig)).hide(); return; } uploader = _this.uploader = WebUploader.create({ pick: { id: '#filePickerReady', label: lang.uploadSelectFile }, swf: '../../third-party/webuploader/Uploader.swf', server: actionUrl, fileVal: editor.getOpt('videoFieldName'), duplicate: true, fileSingleSizeLimit: fileMaxSize, compress: false }); uploader.addButton({ id: '#filePickerBlock' }); uploader.addButton({ id: '#filePickerBtn', label: lang.uploadAddFile }); setState('pedding'); // 当有文件添加进来时执行,负责view的创建 function addFile(file) { var $li = $('
  • ' + '

    ' + file.name + '

    ' + '

    ' + '

    ' + '
  • '), $btns = $('
    ' + '' + lang.uploadDelete + '' + '' + lang.uploadTurnRight + '' + '' + lang.uploadTurnLeft + '
    ').appendTo($li), $prgress = $li.find('p.progress span'), $wrap = $li.find('p.imgWrap'), $info = $('

    ').hide().appendTo($li), showError = function (code) { switch (code) { case 'exceed_size': text = lang.errorExceedSize; break; case 'interrupt': text = lang.errorInterrupt; break; case 'http': text = lang.errorHttp; break; case 'not_allow_type': text = lang.errorFileType; break; default: text = lang.errorUploadRetry; break; } $info.text(text).show(); }; if (file.getStatus() === 'invalid') { showError(file.statusText); } else { $wrap.text(lang.uploadPreview); if ('|png|jpg|jpeg|bmp|gif|'.indexOf('|'+file.ext.toLowerCase()+'|') == -1) { $wrap.empty().addClass('notimage').append('' + '' + file.name + ''); } else { if (browser.ie && browser.version <= 7) { $wrap.text(lang.uploadNoPreview); } else { uploader.makeThumb(file, function (error, src) { if (error || !src || (/^data:/.test(src) && browser.ie && browser.version <= 7)) { $wrap.text(lang.uploadNoPreview); } else { var $img = $(''); $wrap.empty().append($img); $img.on('error', function () { $wrap.text(lang.uploadNoPreview); }); } }, thumbnailWidth, thumbnailHeight); } } percentages[ file.id ] = [ file.size, 0 ]; file.rotation = 0; /* 检查文件格式 */ if (!file.ext || acceptExtensions.indexOf(file.ext.toLowerCase()) == -1) { showError('not_allow_type'); uploader.removeFile(file); } } file.on('statuschange', function (cur, prev) { if (prev === 'progress') { $prgress.hide().width(0); } else if (prev === 'queued') { $li.off('mouseenter mouseleave'); $btns.remove(); } // 成功 if (cur === 'error' || cur === 'invalid') { showError(file.statusText); percentages[ file.id ][ 1 ] = 1; } else if (cur === 'interrupt') { showError('interrupt'); } else if (cur === 'queued') { percentages[ file.id ][ 1 ] = 0; } else if (cur === 'progress') { $info.hide(); $prgress.css('display', 'block'); } else if (cur === 'complete') { } $li.removeClass('state-' + prev).addClass('state-' + cur); }); $li.on('mouseenter', function () { $btns.stop().animate({height: 30}); }); $li.on('mouseleave', function () { $btns.stop().animate({height: 0}); }); $btns.on('click', 'span', function () { var index = $(this).index(), deg; switch (index) { case 0: uploader.removeFile(file); return; case 1: file.rotation += 90; break; case 2: file.rotation -= 90; break; } if (supportTransition) { deg = 'rotate(' + file.rotation + 'deg)'; $wrap.css({ '-webkit-transform': deg, '-mos-transform': deg, '-o-transform': deg, 'transform': deg }); } else { $wrap.css('filter', 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' + (~~((file.rotation / 90) % 4 + 4) % 4) + ')'); } }); $li.insertBefore($filePickerBlock); } // 负责view的销毁 function removeFile(file) { var $li = $('#' + file.id); delete percentages[ file.id ]; updateTotalProgress(); $li.off().find('.file-panel').off().end().remove(); } function updateTotalProgress() { var loaded = 0, total = 0, spans = $progress.children(), percent; $.each(percentages, function (k, v) { total += v[ 0 ]; loaded += v[ 0 ] * v[ 1 ]; }); percent = total ? loaded / total : 0; spans.eq(0).text(Math.round(percent * 100) + '%'); spans.eq(1).css('width', Math.round(percent * 100) + '%'); updateStatus(); } function setState(val, files) { if (val != state) { var stats = uploader.getStats(); $upload.removeClass('state-' + state); $upload.addClass('state-' + val); switch (val) { /* 未选择文件 */ case 'pedding': $queue.addClass('element-invisible'); $statusBar.addClass('element-invisible'); $placeHolder.removeClass('element-invisible'); $progress.hide(); $info.hide(); uploader.refresh(); break; /* 可以开始上传 */ case 'ready': $placeHolder.addClass('element-invisible'); $queue.removeClass('element-invisible'); $statusBar.removeClass('element-invisible'); $progress.hide(); $info.show(); $upload.text(lang.uploadStart); uploader.refresh(); break; /* 上传中 */ case 'uploading': $progress.show(); $info.hide(); $upload.text(lang.uploadPause); break; /* 暂停上传 */ case 'paused': $progress.show(); $info.hide(); $upload.text(lang.uploadContinue); break; case 'confirm': $progress.show(); $info.hide(); $upload.text(lang.uploadStart); stats = uploader.getStats(); if (stats.successNum && !stats.uploadFailNum) { setState('finish'); return; } break; case 'finish': $progress.hide(); $info.show(); if (stats.uploadFailNum) { $upload.text(lang.uploadRetry); } else { $upload.text(lang.uploadStart); } break; } state = val; updateStatus(); } if (!_this.getQueueCount()) { $upload.addClass('disabled') } else { $upload.removeClass('disabled') } } function updateStatus() { var text = '', stats; if (state === 'ready') { text = lang.updateStatusReady.replace('_', fileCount).replace('_KB', WebUploader.formatSize(fileSize)); } else if (state === 'confirm') { stats = uploader.getStats(); if (stats.uploadFailNum) { text = lang.updateStatusConfirm.replace('_', stats.successNum).replace('_', stats.successNum); } } else { stats = uploader.getStats(); text = lang.updateStatusFinish.replace('_', fileCount). replace('_KB', WebUploader.formatSize(fileSize)). replace('_', stats.successNum); if (stats.uploadFailNum) { text += lang.updateStatusError.replace('_', stats.uploadFailNum); } } $info.html(text); } uploader.on('fileQueued', function (file) { fileCount++; fileSize += file.size; if (fileCount === 1) { $placeHolder.addClass('element-invisible'); $statusBar.show(); } addFile(file); }); uploader.on('fileDequeued', function (file) { fileCount--; fileSize -= file.size; removeFile(file); updateTotalProgress(); }); uploader.on('filesQueued', function (file) { if (!uploader.isInProgress() && (state == 'pedding' || state == 'finish' || state == 'confirm' || state == 'ready')) { setState('ready'); } updateTotalProgress(); }); uploader.on('all', function (type, files) { switch (type) { case 'uploadFinished': setState('confirm', files); break; case 'startUpload': /* 添加额外的GET参数 */ var params = utils.serializeParam(editor.queryCommandValue('serverparam')) || '', url = utils.formatUrl(actionUrl + (actionUrl.indexOf('?') == -1 ? '?':'&') + 'encode=utf-8&' + params); uploader.option('server', url); setState('uploading', files); break; case 'stopUpload': setState('paused', files); break; } }); uploader.on('uploadBeforeSend', function (file, data, header) { //这里可以通过data对象添加POST参数 header['X_Requested_With'] = 'XMLHttpRequest'; // HaoChuan9421 if(editor.options.headers && Object.prototype.toString.apply(editor.options.headers) === "[object Object]"){ for(var key in editor.options.headers){ header[key] = editor.options.headers[key] } } }); uploader.on('uploadProgress', function (file, percentage) { var $li = $('#' + file.id), $percent = $li.find('.progress span'); $percent.css('width', percentage * 100 + '%'); percentages[ file.id ][ 1 ] = percentage; updateTotalProgress(); }); uploader.on('uploadSuccess', function (file, ret) { var $file = $('#' + file.id); try { var responseText = (ret._raw || ret), json = utils.str2json(responseText); if (json.state == 'SUCCESS') { uploadVideoList.push({ 'url': json.url, 'type': json.type, 'original':json.original }); $file.append(''); } else { $file.find('.error').text(json.state).show(); } } catch (e) { $file.find('.error').text(lang.errorServerUpload).show(); } }); uploader.on('uploadError', function (file, code) { }); uploader.on('error', function (code, file) { if (code == 'Q_TYPE_DENIED' || code == 'F_EXCEED_SIZE') { addFile(file); } }); uploader.on('uploadComplete', function (file, ret) { }); $upload.on('click', function () { if ($(this).hasClass('disabled')) { return false; } if (state === 'ready') { uploader.upload(); } else if (state === 'paused') { uploader.upload(); } else if (state === 'uploading') { uploader.stop(); } }); $upload.addClass('state-' + state); updateTotalProgress(); }, getQueueCount: function () { var file, i, status, readyFile = 0, files = this.uploader.getFiles(); for (i = 0; file = files[i++]; ) { status = file.getStatus(); if (status == 'queued' || status == 'uploading' || status == 'progress') readyFile++; } return readyFile; }, refresh: function(){ this.uploader.refresh(); } }; })(); ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/webapp/webapp.html ================================================
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/wordimage/tangram.js ================================================ // Copyright (c) 2009, Baidu Inc. All rights reserved. // // Licensed under the BSD License // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http:// tangram.baidu.com/license.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @namespace T Tangram七巧板 * @name T * @version 1.6.0 */ /** * 声明baidu包 * @author: allstar, erik, meizz, berg */ var T, baidu = T = baidu || {version: "1.5.0"}; baidu.guid = "$BAIDU$"; baidu.$$ = window[baidu.guid] = window[baidu.guid] || {global:{}}; /** * 使用flash资源封装的一些功能 * @namespace baidu.flash */ baidu.flash = baidu.flash || {}; /** * 操作dom的方法 * @namespace baidu.dom */ baidu.dom = baidu.dom || {}; /** * 从文档中获取指定的DOM元素 * @name baidu.dom.g * @function * @grammar baidu.dom.g(id) * @param {string|HTMLElement} id 元素的id或DOM元素. * @shortcut g,T.G * @meta standard * @see baidu.dom.q * * @return {HTMLElement|null} 获取的元素,查找不到时返回null,如果参数不合法,直接返回参数. */ baidu.dom.g = function(id) { if (!id) return null; if ('string' == typeof id || id instanceof String) { return document.getElementById(id); } else if (id.nodeName && (id.nodeType == 1 || id.nodeType == 9)) { return id; } return null; }; baidu.g = baidu.G = baidu.dom.g; /** * 操作数组的方法 * @namespace baidu.array */ baidu.array = baidu.array || {}; /** * 遍历数组中所有元素 * @name baidu.array.each * @function * @grammar baidu.array.each(source, iterator[, thisObject]) * @param {Array} source 需要遍历的数组 * @param {Function} iterator 对每个数组元素进行调用的函数,该函数有两个参数,第一个为数组元素,第二个为数组索引值,function (item, index)。 * @param {Object} [thisObject] 函数调用时的this指针,如果没有此参数,默认是当前遍历的数组 * @remark * each方法不支持对Object的遍历,对Object的遍历使用baidu.object.each 。 * @shortcut each * @meta standard * * @returns {Array} 遍历的数组 */ baidu.each = baidu.array.forEach = baidu.array.each = function (source, iterator, thisObject) { var returnValue, item, i, len = source.length; if ('function' == typeof iterator) { for (i = 0; i < len; i++) { item = source[i]; returnValue = iterator.call(thisObject || source, item, i); if (returnValue === false) { break; } } } return source; }; /** * 对语言层面的封装,包括类型判断、模块扩展、继承基类以及对象自定义事件的支持。 * @namespace baidu.lang */ baidu.lang = baidu.lang || {}; /** * 判断目标参数是否为function或Function实例 * @name baidu.lang.isFunction * @function * @grammar baidu.lang.isFunction(source) * @param {Any} source 目标参数 * @version 1.2 * @see baidu.lang.isString,baidu.lang.isObject,baidu.lang.isNumber,baidu.lang.isArray,baidu.lang.isElement,baidu.lang.isBoolean,baidu.lang.isDate * @meta standard * @returns {boolean} 类型判断结果 */ baidu.lang.isFunction = function (source) { return '[object Function]' == Object.prototype.toString.call(source); }; /** * 判断目标参数是否string类型或String对象 * @name baidu.lang.isString * @function * @grammar baidu.lang.isString(source) * @param {Any} source 目标参数 * @shortcut isString * @meta standard * @see baidu.lang.isObject,baidu.lang.isNumber,baidu.lang.isArray,baidu.lang.isElement,baidu.lang.isBoolean,baidu.lang.isDate * * @returns {boolean} 类型判断结果 */ baidu.lang.isString = function (source) { return '[object String]' == Object.prototype.toString.call(source); }; baidu.isString = baidu.lang.isString; /** * 判断浏览器类型和特性的属性 * @namespace baidu.browser */ baidu.browser = baidu.browser || {}; /** * 判断是否为opera浏览器 * @property opera opera版本号 * @grammar baidu.browser.opera * @meta standard * @see baidu.browser.ie,baidu.browser.firefox,baidu.browser.safari,baidu.browser.chrome * @returns {Number} opera版本号 */ /** * opera 从10开始不是用opera后面的字符串进行版本的判断 * 在Browser identification最后添加Version + 数字进行版本标识 * opera后面的数字保持在9.80不变 */ baidu.browser.opera = /opera(\/| )(\d+(\.\d+)?)(.+?(version\/(\d+(\.\d+)?)))?/i.test(navigator.userAgent) ? + ( RegExp["\x246"] || RegExp["\x242"] ) : undefined; /** * 在目标元素的指定位置插入HTML代码 * @name baidu.dom.insertHTML * @function * @grammar baidu.dom.insertHTML(element, position, html) * @param {HTMLElement|string} element 目标元素或目标元素的id * @param {string} position 插入html的位置信息,取值为beforeBegin,afterBegin,beforeEnd,afterEnd * @param {string} html 要插入的html * @remark * * 对于position参数,大小写不敏感
    * 参数的意思:beforeBegin<span>afterBegin this is span! beforeEnd</span> afterEnd
    * 此外,如果使用本函数插入带有script标签的HTML字符串,script标签对应的脚本将不会被执行。 * * @shortcut insertHTML * @meta standard * * @returns {HTMLElement} 目标元素 */ baidu.dom.insertHTML = function (element, position, html) { element = baidu.dom.g(element); var range,begin; if (element.insertAdjacentHTML && !baidu.browser.opera) { element.insertAdjacentHTML(position, html); } else { range = element.ownerDocument.createRange(); position = position.toUpperCase(); if (position == 'AFTERBEGIN' || position == 'BEFOREEND') { range.selectNodeContents(element); range.collapse(position == 'AFTERBEGIN'); } else { begin = position == 'BEFOREBEGIN'; range[begin ? 'setStartBefore' : 'setEndAfter'](element); range.collapse(begin); } range.insertNode(range.createContextualFragment(html)); } return element; }; baidu.insertHTML = baidu.dom.insertHTML; /** * 操作flash对象的方法,包括创建flash对象、获取flash对象以及判断flash插件的版本号 * @namespace baidu.swf */ baidu.swf = baidu.swf || {}; /** * 浏览器支持的flash插件版本 * @property version 浏览器支持的flash插件版本 * @grammar baidu.swf.version * @return {String} 版本号 * @meta standard */ baidu.swf.version = (function () { var n = navigator; if (n.plugins && n.mimeTypes.length) { var plugin = n.plugins["Shockwave Flash"]; if (plugin && plugin.description) { return plugin.description .replace(/([a-zA-Z]|\s)+/, "") .replace(/(\s)+r/, ".") + ".0"; } } else if (window.ActiveXObject && !window.opera) { for (var i = 12; i >= 2; i--) { try { var c = new ActiveXObject('ShockwaveFlash.ShockwaveFlash.' + i); if (c) { var version = c.GetVariable("$version"); return version.replace(/WIN/g,'').replace(/,/g,'.'); } } catch(e) {} } } })(); /** * 操作字符串的方法 * @namespace baidu.string */ baidu.string = baidu.string || {}; /** * 对目标字符串进行html编码 * @name baidu.string.encodeHTML * @function * @grammar baidu.string.encodeHTML(source) * @param {string} source 目标字符串 * @remark * 编码字符有5个:&<>"' * @shortcut encodeHTML * @meta standard * @see baidu.string.decodeHTML * * @returns {string} html编码后的字符串 */ baidu.string.encodeHTML = function (source) { return String(source) .replace(/&/g,'&') .replace(//g,'>') .replace(/"/g, """) .replace(/'/g, "'"); }; baidu.encodeHTML = baidu.string.encodeHTML; /** * 创建flash对象的html字符串 * @name baidu.swf.createHTML * @function * @grammar baidu.swf.createHTML(options) * * @param {Object} options 创建flash的选项参数 * @param {string} options.id 要创建的flash的标识 * @param {string} options.url flash文件的url * @param {String} options.errorMessage 未安装flash player或flash player版本号过低时的提示 * @param {string} options.ver 最低需要的flash player版本号 * @param {string} options.width flash的宽度 * @param {string} options.height flash的高度 * @param {string} options.align flash的对齐方式,允许值:middle/left/right/top/bottom * @param {string} options.base 设置用于解析swf文件中的所有相对路径语句的基本目录或URL * @param {string} options.bgcolor swf文件的背景色 * @param {string} options.salign 设置缩放的swf文件在由width和height设置定义的区域内的位置。允许值:l/r/t/b/tl/tr/bl/br * @param {boolean} options.menu 是否显示右键菜单,允许值:true/false * @param {boolean} options.loop 播放到最后一帧时是否重新播放,允许值: true/false * @param {boolean} options.play flash是否在浏览器加载时就开始播放。允许值:true/false * @param {string} options.quality 设置flash播放的画质,允许值:low/medium/high/autolow/autohigh/best * @param {string} options.scale 设置flash内容如何缩放来适应设置的宽高。允许值:showall/noborder/exactfit * @param {string} options.wmode 设置flash的显示模式。允许值:window/opaque/transparent * @param {string} options.allowscriptaccess 设置flash与页面的通信权限。允许值:always/never/sameDomain * @param {string} options.allownetworking 设置swf文件中允许使用的网络API。允许值:all/internal/none * @param {boolean} options.allowfullscreen 是否允许flash全屏。允许值:true/false * @param {boolean} options.seamlesstabbing 允许设置执行无缝跳格,从而使用户能跳出flash应用程序。该参数只能在安装Flash7及更高版本的Windows中使用。允许值:true/false * @param {boolean} options.devicefont 设置静态文本对象是否以设备字体呈现。允许值:true/false * @param {boolean} options.swliveconnect 第一次加载flash时浏览器是否应启动Java。允许值:true/false * @param {Object} options.vars 要传递给flash的参数,支持JSON或string类型。 * * @see baidu.swf.create * @meta standard * @returns {string} flash对象的html字符串 */ baidu.swf.createHTML = function (options) { options = options || {}; var version = baidu.swf.version, needVersion = options['ver'] || '6.0.0', vUnit1, vUnit2, i, k, len, item, tmpOpt = {}, encodeHTML = baidu.string.encodeHTML; for (k in options) { tmpOpt[k] = options[k]; } options = tmpOpt; if (version) { version = version.split('.'); needVersion = needVersion.split('.'); for (i = 0; i < 3; i++) { vUnit1 = parseInt(version[i], 10); vUnit2 = parseInt(needVersion[i], 10); if (vUnit2 < vUnit1) { break; } else if (vUnit2 > vUnit1) { return ''; } } } else { return ''; } var vars = options['vars'], objProperties = ['classid', 'codebase', 'id', 'width', 'height', 'align']; options['align'] = options['align'] || 'middle'; options['classid'] = 'clsid:d27cdb6e-ae6d-11cf-96b8-444553540000'; options['codebase'] = 'http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,0,0'; options['movie'] = options['url'] || ''; delete options['vars']; delete options['url']; if ('string' == typeof vars) { options['flashvars'] = vars; } else { var fvars = []; for (k in vars) { item = vars[k]; fvars.push(k + "=" + encodeURIComponent(item)); } options['flashvars'] = fvars.join('&'); } var str = [''); var params = { 'wmode' : 1, 'scale' : 1, 'quality' : 1, 'play' : 1, 'loop' : 1, 'menu' : 1, 'salign' : 1, 'bgcolor' : 1, 'base' : 1, 'allowscriptaccess' : 1, 'allownetworking' : 1, 'allowfullscreen' : 1, 'seamlesstabbing' : 1, 'devicefont' : 1, 'swliveconnect' : 1, 'flashvars' : 1, 'movie' : 1 }; for (k in options) { item = options[k]; k = k.toLowerCase(); if (params[k] && (item || item === false || item === 0)) { str.push(''); } } options['src'] = options['movie']; options['name'] = options['id']; delete options['id']; delete options['movie']; delete options['classid']; delete options['codebase']; options['type'] = 'application/x-shockwave-flash'; options['pluginspage'] = 'http://www.macromedia.com/go/getflashplayer'; str.push(''); return str.join(''); }; /** * 在页面中创建一个flash对象 * @name baidu.swf.create * @function * @grammar baidu.swf.create(options[, container]) * * @param {Object} options 创建flash的选项参数 * @param {string} options.id 要创建的flash的标识 * @param {string} options.url flash文件的url * @param {String} options.errorMessage 未安装flash player或flash player版本号过低时的提示 * @param {string} options.ver 最低需要的flash player版本号 * @param {string} options.width flash的宽度 * @param {string} options.height flash的高度 * @param {string} options.align flash的对齐方式,允许值:middle/left/right/top/bottom * @param {string} options.base 设置用于解析swf文件中的所有相对路径语句的基本目录或URL * @param {string} options.bgcolor swf文件的背景色 * @param {string} options.salign 设置缩放的swf文件在由width和height设置定义的区域内的位置。允许值:l/r/t/b/tl/tr/bl/br * @param {boolean} options.menu 是否显示右键菜单,允许值:true/false * @param {boolean} options.loop 播放到最后一帧时是否重新播放,允许值: true/false * @param {boolean} options.play flash是否在浏览器加载时就开始播放。允许值:true/false * @param {string} options.quality 设置flash播放的画质,允许值:low/medium/high/autolow/autohigh/best * @param {string} options.scale 设置flash内容如何缩放来适应设置的宽高。允许值:showall/noborder/exactfit * @param {string} options.wmode 设置flash的显示模式。允许值:window/opaque/transparent * @param {string} options.allowscriptaccess 设置flash与页面的通信权限。允许值:always/never/sameDomain * @param {string} options.allownetworking 设置swf文件中允许使用的网络API。允许值:all/internal/none * @param {boolean} options.allowfullscreen 是否允许flash全屏。允许值:true/false * @param {boolean} options.seamlesstabbing 允许设置执行无缝跳格,从而使用户能跳出flash应用程序。该参数只能在安装Flash7及更高版本的Windows中使用。允许值:true/false * @param {boolean} options.devicefont 设置静态文本对象是否以设备字体呈现。允许值:true/false * @param {boolean} options.swliveconnect 第一次加载flash时浏览器是否应启动Java。允许值:true/false * @param {Object} options.vars 要传递给flash的参数,支持JSON或string类型。 * * @param {HTMLElement|string} [container] flash对象的父容器元素,不传递该参数时在当前代码位置创建flash对象。 * @meta standard * @see baidu.swf.createHTML,baidu.swf.getMovie */ baidu.swf.create = function (options, target) { options = options || {}; var html = baidu.swf.createHTML(options) || options['errorMessage'] || ''; if (target && 'string' == typeof target) { target = document.getElementById(target); } baidu.dom.insertHTML( target || document.body ,'beforeEnd',html ); }; /** * 判断是否为ie浏览器 * @name baidu.browser.ie * @field * @grammar baidu.browser.ie * @returns {Number} IE版本号 */ baidu.browser.ie = baidu.ie = /msie (\d+\.\d+)/i.test(navigator.userAgent) ? (document.documentMode || + RegExp['\x241']) : undefined; /** * 移除数组中的项 * @name baidu.array.remove * @function * @grammar baidu.array.remove(source, match) * @param {Array} source 需要移除项的数组 * @param {Any} match 要移除的项 * @meta standard * @see baidu.array.removeAt * * @returns {Array} 移除后的数组 */ baidu.array.remove = function (source, match) { var len = source.length; while (len--) { if (len in source && source[len] === match) { source.splice(len, 1); } } return source; }; /** * 判断目标参数是否Array对象 * @name baidu.lang.isArray * @function * @grammar baidu.lang.isArray(source) * @param {Any} source 目标参数 * @meta standard * @see baidu.lang.isString,baidu.lang.isObject,baidu.lang.isNumber,baidu.lang.isElement,baidu.lang.isBoolean,baidu.lang.isDate * * @returns {boolean} 类型判断结果 */ baidu.lang.isArray = function (source) { return '[object Array]' == Object.prototype.toString.call(source); }; /** * 将一个变量转换成array * @name baidu.lang.toArray * @function * @grammar baidu.lang.toArray(source) * @param {mix} source 需要转换成array的变量 * @version 1.3 * @meta standard * @returns {array} 转换后的array */ baidu.lang.toArray = function (source) { if (source === null || source === undefined) return []; if (baidu.lang.isArray(source)) return source; if (typeof source.length !== 'number' || typeof source === 'string' || baidu.lang.isFunction(source)) { return [source]; } if (source.item) { var l = source.length, array = new Array(l); while (l--) array[l] = source[l]; return array; } return [].slice.call(source); }; /** * 获得flash对象的实例 * @name baidu.swf.getMovie * @function * @grammar baidu.swf.getMovie(name) * @param {string} name flash对象的名称 * @see baidu.swf.create * @meta standard * @returns {HTMLElement} flash对象的实例 */ baidu.swf.getMovie = function (name) { var movie = document[name], ret; return baidu.browser.ie == 9 ? movie && movie.length ? (ret = baidu.array.remove(baidu.lang.toArray(movie),function(item){ return item.tagName.toLowerCase() != "embed"; })).length == 1 ? ret[0] : ret : movie : movie || window[name]; }; baidu.flash._Base = (function(){ var prefix = 'bd__flash__'; /** * 创建一个随机的字符串 * @private * @return {String} */ function _createString(){ return prefix + Math.floor(Math.random() * 2147483648).toString(36); }; /** * 检查flash状态 * @private * @param {Object} target flash对象 * @return {Boolean} */ function _checkReady(target){ if(typeof target !== 'undefined' && typeof target.flashInit !== 'undefined' && target.flashInit()){ return true; }else{ return false; } }; /** * 调用之前进行压栈的函数 * @private * @param {Array} callQueue 调用队列 * @param {Object} target flash对象 * @return {Null} */ function _callFn(callQueue, target){ var result = null; callQueue = callQueue.reverse(); baidu.each(callQueue, function(item){ result = target.call(item.fnName, item.params); item.callBack(result); }); }; /** * 为传入的匿名函数创建函数名 * @private * @param {String|Function} fun 传入的匿名函数或者函数名 * @return {String} */ function _createFunName(fun){ var name = ''; if(baidu.lang.isFunction(fun)){ name = _createString(); window[name] = function(){ fun.apply(window, arguments); }; return name; }else if(baidu.lang.isString){ return fun; } }; /** * 绘制flash * @private * @param {Object} options 创建参数 * @return {Object} */ function _render(options){ if(!options.id){ options.id = _createString(); } var container = options.container || ''; delete(options.container); baidu.swf.create(options, container); return baidu.swf.getMovie(options.id); }; return function(options, callBack){ var me = this, autoRender = (typeof options.autoRender !== 'undefined' ? options.autoRender : true), createOptions = options.createOptions || {}, target = null, isReady = false, callQueue = [], timeHandle = null, callBack = callBack || []; /** * 将flash文件绘制到页面上 * @public * @return {Null} */ me.render = function(){ target = _render(createOptions); if(callBack.length > 0){ baidu.each(callBack, function(funName, index){ callBack[index] = _createFunName(options[funName] || new Function()); }); } me.call('setJSFuncName', [callBack]); }; /** * 返回flash状态 * @return {Boolean} */ me.isReady = function(){ return isReady; }; /** * 调用flash接口的统一入口 * @param {String} fnName 调用的函数名 * @param {Array} params 传入的参数组成的数组,若不许要参数,需传入空数组 * @param {Function} [callBack] 异步调用后将返回值作为参数的调用回调函数,如无返回值,可以不传入此参数 * @return {Null} */ me.call = function(fnName, params, callBack){ if(!fnName) return null; callBack = callBack || new Function(); var result = null; if(isReady){ result = target.call(fnName, params); callBack(result); }else{ callQueue.push({ fnName: fnName, params: params, callBack: callBack }); (!timeHandle) && (timeHandle = setInterval(_check, 200)); } }; /** * 为传入的匿名函数创建函数名 * @public * @param {String|Function} fun 传入的匿名函数或者函数名 * @return {String} */ me.createFunName = function(fun){ return _createFunName(fun); }; /** * 检查flash是否ready, 并进行调用 * @private * @return {Null} */ function _check(){ if(_checkReady(target)){ clearInterval(timeHandle); timeHandle = null; _call(); isReady = true; } }; /** * 调用之前进行压栈的函数 * @private * @return {Null} */ function _call(){ _callFn(callQueue, target); callQueue = []; } autoRender && me.render(); }; })(); /** * 创建flash based imageUploader * @class * @grammar baidu.flash.imageUploader(options) * @param {Object} createOptions 创建flash时需要的参数,请参照baidu.swf.create文档 * @config {Object} vars 创建imageUploader时所需要的参数 * @config {Number} vars.gridWidth 每一个预览图片所占的宽度,应该为flash寛的整除 * @config {Number} vars.gridHeight 每一个预览图片所占的高度,应该为flash高的整除 * @config {Number} vars.picWidth 单张预览图片的宽度 * @config {Number} vars.picHeight 单张预览图片的高度 * @config {String} vars.uploadDataFieldName POST请求中图片数据的key,默认值'picdata' * @config {String} vars.picDescFieldName POST请求中图片描述的key,默认值'picDesc' * @config {Number} vars.maxSize 文件的最大体积,单位'MB' * @config {Number} vars.compressSize 上传前如果图片体积超过该值,会先压缩 * @config {Number} vars.maxNum:32 最大上传多少个文件 * @config {Number} vars.compressLength 能接受的最大边长,超过该值会等比压缩 * @config {String} vars.url 上传的url地址 * @config {Number} vars.mode mode == 0时,是使用滚动条,mode == 1时,拉伸flash, 默认值为0 * @see baidu.swf.createHTML * @param {String} backgroundUrl 背景图片路径 * @param {String} listBacgroundkUrl 布局控件背景 * @param {String} buttonUrl 按钮图片不背景 * @param {String|Function} selectFileCallback 选择文件的回调 * @param {String|Function} exceedFileCallback文件超出限制的最大体积时的回调 * @param {String|Function} deleteFileCallback 删除文件的回调 * @param {String|Function} startUploadCallback 开始上传某个文件时的回调 * @param {String|Function} uploadCompleteCallback 某个文件上传完成的回调 * @param {String|Function} uploadErrorCallback 某个文件上传失败的回调 * @param {String|Function} allCompleteCallback 全部上传完成时的回调 * @param {String|Function} changeFlashHeight 改变Flash的高度,mode==1的时候才有用 */ baidu.flash.imageUploader = baidu.flash.imageUploader || function(options){ var me = this, options = options || {}, _flash = new baidu.flash._Base(options, [ 'selectFileCallback', 'exceedFileCallback', 'deleteFileCallback', 'startUploadCallback', 'uploadCompleteCallback', 'uploadErrorCallback', 'allCompleteCallback', 'changeFlashHeight' ]); /** * 开始或回复上传图片 * @public * @return {Null} */ me.upload = function(){ _flash.call('upload'); }; /** * 暂停上传图片 * @public * @return {Null} */ me.pause = function(){ _flash.call('pause'); }; me.addCustomizedParams = function(index,obj){ _flash.call('addCustomizedParams',[index,obj]); } }; /** * 操作原生对象的方法 * @namespace baidu.object */ baidu.object = baidu.object || {}; /** * 将源对象的所有属性拷贝到目标对象中 * @author erik * @name baidu.object.extend * @function * @grammar baidu.object.extend(target, source) * @param {Object} target 目标对象 * @param {Object} source 源对象 * @see baidu.array.merge * @remark * 1.目标对象中,与源对象key相同的成员将会被覆盖。
    2.源对象的prototype成员不会拷贝。 * @shortcut extend * @meta standard * * @returns {Object} 目标对象 */ baidu.extend = baidu.object.extend = function (target, source) { for (var p in source) { if (source.hasOwnProperty(p)) { target[p] = source[p]; } } return target; }; /** * 创建flash based fileUploader * @class * @grammar baidu.flash.fileUploader(options) * @param {Object} options * @config {Object} createOptions 创建flash时需要的参数,请参照baidu.swf.create文档 * @config {String} createOptions.width * @config {String} createOptions.height * @config {Number} maxNum 最大可选文件数 * @config {Function|String} selectFile * @config {Function|String} exceedMaxSize * @config {Function|String} deleteFile * @config {Function|String} uploadStart * @config {Function|String} uploadComplete * @config {Function|String} uploadError * @config {Function|String} uploadProgress */ baidu.flash.fileUploader = baidu.flash.fileUploader || function(options){ var me = this, options = options || {}; options.createOptions = baidu.extend({ wmod: 'transparent' },options.createOptions || {}); var _flash = new baidu.flash._Base(options, [ 'selectFile', 'exceedMaxSize', 'deleteFile', 'uploadStart', 'uploadComplete', 'uploadError', 'uploadProgress' ]); _flash.call('setMaxNum', options.maxNum ? [options.maxNum] : [1]); /** * 设置当鼠标移动到flash上时,是否变成手型 * @public * @param {Boolean} isCursor * @return {Null} */ me.setHandCursor = function(isCursor){ _flash.call('setHandCursor', [isCursor || false]); }; /** * 设置鼠标相应函数名 * @param {String|Function} fun */ me.setMSFunName = function(fun){ _flash.call('setMSFunName',[_flash.createFunName(fun)]); }; /** * 执行上传操作 * @param {String} url 上传的url * @param {String} fieldName 上传的表单字段名 * @param {Object} postData 键值对,上传的POST数据 * @param {Number|Array|null|-1} [index]上传的文件序列 * Int值上传该文件 * Array一次串行上传该序列文件 * -1/null上传所有文件 * @return {Null} */ me.upload = function(url, fieldName, postData, index){ if(typeof url !== 'string' || typeof fieldName !== 'string') return null; if(typeof index === 'undefined') index = -1; _flash.call('upload', [url, fieldName, postData, index]); }; /** * 取消上传操作 * @public * @param {Number|-1} index */ me.cancel = function(index){ if(typeof index === 'undefined') index = -1; _flash.call('cancel', [index]); }; /** * 删除文件 * @public * @param {Number|Array} [index] 要删除的index,不传则全部删除 * @param {Function} callBack * */ me.deleteFile = function(index, callBack){ var callBackAll = function(list){ callBack && callBack(list); }; if(typeof index === 'undefined'){ _flash.call('deleteFilesAll', [], callBackAll); return; }; if(typeof index === 'Number') index = [index]; index.sort(function(a,b){ return b-a; }); baidu.each(index, function(item){ _flash.call('deleteFileBy', item, callBackAll); }); }; /** * 添加文件类型,支持macType * @public * @param {Object|Array[Object]} type {description:String, extention:String} * @return {Null}; */ me.addFileType = function(type){ var type = type || [[]]; if(type instanceof Array) type = [type]; else type = [[type]]; _flash.call('addFileTypes', type); }; /** * 设置文件类型,支持macType * @public * @param {Object|Array[Object]} type {description:String, extention:String} * @return {Null}; */ me.setFileType = function(type){ var type = type || [[]]; if(type instanceof Array) type = [type]; else type = [[type]]; _flash.call('setFileTypes', type); }; /** * 设置可选文件的数量限制 * @public * @param {Number} num * @return {Null} */ me.setMaxNum = function(num){ _flash.call('setMaxNum', [num]); }; /** * 设置可选文件大小限制,以兆M为单位 * @public * @param {Number} num,0为无限制 * @return {Null} */ me.setMaxSize = function(num){ _flash.call('setMaxSize', [num]); }; /** * @public */ me.getFileAll = function(callBack){ _flash.call('getFileAll', [], callBack); }; /** * @public * @param {Number} index * @param {Function} [callBack] */ me.getFileByIndex = function(index, callBack){ _flash.call('getFileByIndex', [], callBack); }; /** * @public * @param {Number} index * @param {function} [callBack] */ me.getStatusByIndex = function(index, callBack){ _flash.call('getStatusByIndex', [], callBack); }; }; /** * 使用动态script标签请求服务器资源,包括由服务器端的回调和浏览器端的回调 * @namespace baidu.sio */ baidu.sio = baidu.sio || {}; /** * * @param {HTMLElement} src script节点 * @param {String} url script节点的地址 * @param {String} [charset] 编码 */ baidu.sio._createScriptTag = function(scr, url, charset){ scr.setAttribute('type', 'text/javascript'); charset && scr.setAttribute('charset', charset); scr.setAttribute('src', url); document.getElementsByTagName('head')[0].appendChild(scr); }; /** * 删除script的属性,再删除script标签,以解决修复内存泄漏的问题 * * @param {HTMLElement} src script节点 */ baidu.sio._removeScriptTag = function(scr){ if (scr.clearAttributes) { scr.clearAttributes(); } else { for (var attr in scr) { if (scr.hasOwnProperty(attr)) { delete scr[attr]; } } } if(scr && scr.parentNode){ scr.parentNode.removeChild(scr); } scr = null; }; /** * 通过script标签加载数据,加载完成由浏览器端触发回调 * @name baidu.sio.callByBrowser * @function * @grammar baidu.sio.callByBrowser(url, opt_callback, opt_options) * @param {string} url 加载数据的url * @param {Function|string} opt_callback 数据加载结束时调用的函数或函数名 * @param {Object} opt_options 其他可选项 * @config {String} [charset] script的字符集 * @config {Integer} [timeOut] 超时时间,超过这个时间将不再响应本请求,并触发onfailure函数 * @config {Function} [onfailure] timeOut设定后才生效,到达超时时间时触发本函数 * @remark * 1、与callByServer不同,callback参数只支持Function类型,不支持string。 * 2、如果请求了一个不存在的页面,callback函数在IE/opera下也会被调用,因此使用者需要在onsuccess函数中判断数据是否正确加载。 * @meta standard * @see baidu.sio.callByServer */ baidu.sio.callByBrowser = function (url, opt_callback, opt_options) { var scr = document.createElement("SCRIPT"), scriptLoaded = 0, options = opt_options || {}, charset = options['charset'], callback = opt_callback || function(){}, timeOut = options['timeOut'] || 0, timer; scr.onload = scr.onreadystatechange = function () { if (scriptLoaded) { return; } var readyState = scr.readyState; if ('undefined' == typeof readyState || readyState == "loaded" || readyState == "complete") { scriptLoaded = 1; try { callback(); clearTimeout(timer); } finally { scr.onload = scr.onreadystatechange = null; baidu.sio._removeScriptTag(scr); } } }; if( timeOut ){ timer = setTimeout(function(){ scr.onload = scr.onreadystatechange = null; baidu.sio._removeScriptTag(scr); options.onfailure && options.onfailure(); }, timeOut); } baidu.sio._createScriptTag(scr, url, charset); }; /** * 通过script标签加载数据,加载完成由服务器端触发回调 * @name baidu.sio.callByServer * @function * @grammar baidu.sio.callByServer(url, callback[, opt_options]) * @param {string} url 加载数据的url. * @param {Function|string} callback 服务器端调用的函数或函数名。如果没有指定本参数,将在URL中寻找options['queryField']做为callback的方法名. * @param {Object} opt_options 加载数据时的选项. * @config {string} [charset] script的字符集 * @config {string} [queryField] 服务器端callback请求字段名,默认为callback * @config {Integer} [timeOut] 超时时间(单位:ms),超过这个时间将不再响应本请求,并触发onfailure函数 * @config {Function} [onfailure] timeOut设定后才生效,到达超时时间时触发本函数 * @remark * 如果url中已经包含key为“options['queryField']”的query项,将会被替换成callback中参数传递或自动生成的函数名。 * @meta standard * @see baidu.sio.callByBrowser */ baidu.sio.callByServer = /**@function*/function(url, callback, opt_options) { var scr = document.createElement('SCRIPT'), prefix = 'bd__cbs__', callbackName, callbackImpl, options = opt_options || {}, charset = options['charset'], queryField = options['queryField'] || 'callback', timeOut = options['timeOut'] || 0, timer, reg = new RegExp('(\\?|&)' + queryField + '=([^&]*)'), matches; if (baidu.lang.isFunction(callback)) { callbackName = prefix + Math.floor(Math.random() * 2147483648).toString(36); window[callbackName] = getCallBack(0); } else if(baidu.lang.isString(callback)){ callbackName = callback; } else { if (matches = reg.exec(url)) { callbackName = matches[2]; } } if( timeOut ){ timer = setTimeout(getCallBack(1), timeOut); } url = url.replace(reg, '\x241' + queryField + '=' + callbackName); if (url.search(reg) < 0) { url += (url.indexOf('?') < 0 ? '?' : '&') + queryField + '=' + callbackName; } baidu.sio._createScriptTag(scr, url, charset); /* * 返回一个函数,用于立即(挂在window上)或者超时(挂在setTimeout中)时执行 */ function getCallBack(onTimeOut){ /*global callbackName, callback, scr, options;*/ return function(){ try { if( onTimeOut ){ options.onfailure && options.onfailure(); }else{ callback.apply(window, arguments); clearTimeout(timer); } window[callbackName] = null; delete window[callbackName]; } catch (exception) { } finally { baidu.sio._removeScriptTag(scr); } } } }; /** * 通过请求一个图片的方式令服务器存储一条日志 * @function * @grammar baidu.sio.log(url) * @param {string} url 要发送的地址. * @author: int08h,leeight */ baidu.sio.log = function(url) { var img = new Image(), key = 'tangram_sio_log_' + Math.floor(Math.random() * 2147483648).toString(36); window[key] = img; img.onload = img.onerror = img.onabort = function() { img.onload = img.onerror = img.onabort = null; window[key] = null; img = null; }; img.src = url; }; /* * Tangram * Copyright 2009 Baidu Inc. All rights reserved. * * path: baidu/json.js * author: erik * version: 1.1.0 * date: 2009/12/02 */ /** * 操作json对象的方法 * @namespace baidu.json */ baidu.json = baidu.json || {}; /* * Tangram * Copyright 2009 Baidu Inc. All rights reserved. * * path: baidu/json/parse.js * author: erik, berg * version: 1.2 * date: 2009/11/23 */ /** * 将字符串解析成json对象。注:不会自动祛除空格 * @name baidu.json.parse * @function * @grammar baidu.json.parse(data) * @param {string} source 需要解析的字符串 * @remark * 该方法的实现与ecma-262第五版中规定的JSON.parse不同,暂时只支持传入一个参数。后续会进行功能丰富。 * @meta standard * @see baidu.json.stringify,baidu.json.decode * * @returns {JSON} 解析结果json对象 */ baidu.json.parse = function (data) { //2010/12/09:更新至不使用原生parse,不检测用户输入是否正确 return (new Function("return (" + data + ")"))(); }; /* * Tangram * Copyright 2009 Baidu Inc. All rights reserved. * * path: baidu/json/decode.js * author: erik, cat * version: 1.3.4 * date: 2010/12/23 */ /** * 将字符串解析成json对象,为过时接口,今后会被baidu.json.parse代替 * @name baidu.json.decode * @function * @grammar baidu.json.decode(source) * @param {string} source 需要解析的字符串 * @meta out * @see baidu.json.encode,baidu.json.parse * * @returns {JSON} 解析结果json对象 */ baidu.json.decode = baidu.json.parse; /* * Tangram * Copyright 2009 Baidu Inc. All rights reserved. * * path: baidu/json/stringify.js * author: erik * version: 1.1.0 * date: 2010/01/11 */ /** * 将json对象序列化 * @name baidu.json.stringify * @function * @grammar baidu.json.stringify(value) * @param {JSON} value 需要序列化的json对象 * @remark * 该方法的实现与ecma-262第五版中规定的JSON.stringify不同,暂时只支持传入一个参数。后续会进行功能丰富。 * @meta standard * @see baidu.json.parse,baidu.json.encode * * @returns {string} 序列化后的字符串 */ baidu.json.stringify = (function () { /** * 字符串处理时需要转义的字符表 * @private */ var escapeMap = { "\b": '\\b', "\t": '\\t', "\n": '\\n', "\f": '\\f', "\r": '\\r', '"' : '\\"', "\\": '\\\\' }; /** * 字符串序列化 * @private */ function encodeString(source) { if (/["\\\x00-\x1f]/.test(source)) { source = source.replace( /["\\\x00-\x1f]/g, function (match) { var c = escapeMap[match]; if (c) { return c; } c = match.charCodeAt(); return "\\u00" + Math.floor(c / 16).toString(16) + (c % 16).toString(16); }); } return '"' + source + '"'; } /** * 数组序列化 * @private */ function encodeArray(source) { var result = ["["], l = source.length, preComma, i, item; for (i = 0; i < l; i++) { item = source[i]; switch (typeof item) { case "undefined": case "function": case "unknown": break; default: if(preComma) { result.push(','); } result.push(baidu.json.stringify(item)); preComma = 1; } } result.push("]"); return result.join(""); } /** * 处理日期序列化时的补零 * @private */ function pad(source) { return source < 10 ? '0' + source : source; } /** * 日期序列化 * @private */ function encodeDate(source){ return '"' + source.getFullYear() + "-" + pad(source.getMonth() + 1) + "-" + pad(source.getDate()) + "T" + pad(source.getHours()) + ":" + pad(source.getMinutes()) + ":" + pad(source.getSeconds()) + '"'; } return function (value) { switch (typeof value) { case 'undefined': return 'undefined'; case 'number': return isFinite(value) ? String(value) : "null"; case 'string': return encodeString(value); case 'boolean': return String(value); default: if (value === null) { return 'null'; } else if (value instanceof Array) { return encodeArray(value); } else if (value instanceof Date) { return encodeDate(value); } else { var result = ['{'], encode = baidu.json.stringify, preComma, item; for (var key in value) { if (Object.prototype.hasOwnProperty.call(value, key)) { item = value[key]; switch (typeof item) { case 'undefined': case 'unknown': case 'function': break; default: if (preComma) { result.push(','); } preComma = 1; result.push(encode(key) + ':' + encode(item)); } } } result.push('}'); return result.join(''); } } }; })(); /* * Tangram * Copyright 2009 Baidu Inc. All rights reserved. * * path: baidu/json/encode.js * author: erik, cat * version: 1.3.4 * date: 2010/12/23 */ /** * 将json对象序列化,为过时接口,今后会被baidu.json.stringify代替 * @name baidu.json.encode * @function * @grammar baidu.json.encode(value) * @param {JSON} value 需要序列化的json对象 * @meta out * @see baidu.json.decode,baidu.json.stringify * * @returns {string} 序列化后的字符串 */ baidu.json.encode = baidu.json.stringify; ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/wordimage/wordimage.html ================================================
    :
    ================================================ FILE: yshop-drink-vue3/public/UEditor22/dialogs/wordimage/wordimage.js ================================================ /** * Created by JetBrains PhpStorm. * User: taoqili * Date: 12-1-30 * Time: 下午12:50 * To change this template use File | Settings | File Templates. */ var wordImage = {}; //(function(){ var g = baidu.g, flashObj,flashContainer; wordImage.init = function(opt, callbacks) { showLocalPath("localPath"); //createCopyButton("clipboard","localPath"); createFlashUploader(opt, callbacks); addUploadListener(); addOkListener(); }; function hideFlash(){ flashObj = null; flashContainer.innerHTML = ""; } function addOkListener() { dialog.onok = function() { if (!imageUrls.length) return; var urlPrefix = editor.getOpt('imageUrlPrefix'), images = domUtils.getElementsByTagName(editor.document,"img"); editor.fireEvent('saveScene'); for (var i = 0,img; img = images[i++];) { var src = img.getAttribute("word_img"); if (!src) continue; for (var j = 0,url; url = imageUrls[j++];) { if (src.indexOf(url.original.replace(" ","")) != -1) { img.src = urlPrefix + url.url; img.setAttribute("_src", urlPrefix + url.url); //同时修改"_src"属性 img.setAttribute("title",url.title); domUtils.removeAttributes(img, ["word_img","style","width","height"]); editor.fireEvent("selectionchange"); break; } } } editor.fireEvent('saveScene'); hideFlash(); }; dialog.oncancel = function(){ hideFlash(); } } /** * 绑定开始上传事件 */ function addUploadListener() { g("upload").onclick = function () { flashObj.upload(); this.style.display = "none"; }; } function showLocalPath(id) { //单张编辑 var img = editor.selection.getRange().getClosedNode(); var images = editor.execCommand('wordimage'); if(images.length==1 || img && img.tagName == 'IMG'){ g(id).value = images[0]; return; } var path = images[0]; var leftSlashIndex = path.lastIndexOf("/")||0, //不同版本的doc和浏览器都可能影响到这个符号,故直接判断两种 rightSlashIndex = path.lastIndexOf("\\")||0, separater = leftSlashIndex > rightSlashIndex ? "/":"\\" ; path = path.substring(0, path.lastIndexOf(separater)+1); g(id).value = path; } function createFlashUploader(opt, callbacks) { //由于lang.flashI18n是静态属性,不可以直接进行修改,否则会影响到后续内容 var i18n = utils.extend({},lang.flashI18n); //处理图片资源地址的编码,补全等问题 for(var i in i18n){ if(!(i in {"lang":1,"uploadingTF":1,"imageTF":1,"textEncoding":1}) && i18n[i]){ i18n[i] = encodeURIComponent(editor.options.langPath + editor.options.lang + "/images/" + i18n[i]); } } opt = utils.extend(opt,i18n,false); var option = { createOptions:{ id:'flash', url:opt.flashUrl, width:opt.width, height:opt.height, errorMessage:lang.flashError, wmode:browser.safari ? 'transparent' : 'window', ver:'10.0.0', vars:opt, container:opt.container } }; option = extendProperty(callbacks, option); flashObj = new baidu.flash.imageUploader(option); flashContainer = $G(opt.container); } function extendProperty(fromObj, toObj) { for (var i in fromObj) { if (!toObj[i]) { toObj[i] = fromObj[i]; } } return toObj; } //})(); function getPasteData(id) { baidu.g("msg").innerHTML = lang.copySuccess + "
    "; setTimeout(function() { baidu.g("msg").innerHTML = ""; }, 5000); return baidu.g(id).value; } function createCopyButton(id, dataFrom) { baidu.swf.create({ id:"copyFlash", url:"fClipboard_ueditor.swf", width:"58", height:"25", errorMessage:"", bgColor:"#CBCBCB", wmode:"transparent", ver:"10.0.0", vars:{ tid:dataFrom } }, id ); var clipboard = baidu.swf.getMovie("copyFlash"); var clipinterval = setInterval(function() { if (clipboard && clipboard.flashInit) { clearInterval(clipinterval); clipboard.setHandCursor(true); clipboard.setContentFuncName("getPasteData"); //clipboard.setMEFuncName("mouseEventHandler"); } }, 500); } createCopyButton("clipboard", "localPath"); ================================================ FILE: yshop-drink-vue3/public/UEditor22/index.html ================================================ 完整demo

    完整demo

    ================================================ FILE: yshop-drink-vue3/public/UEditor22/lang/en/en.js ================================================ /** * Created with JetBrains PhpStorm. * User: taoqili * Date: 12-6-12 * Time: 下午6:57 * To change this template use File | Settings | File Templates. */ UE.I18N['en'] = { 'labelMap':{ 'anchor':'Anchor', 'undo':'Undo', 'redo':'Redo', 'bold':'Bold', 'indent':'Indent', 'snapscreen':'SnapScreen', 'italic':'Italic', 'underline':'Underline', 'strikethrough':'Strikethrough', 'subscript':'SubScript','fontborder':'text border', 'superscript':'SuperScript', 'formatmatch':'Format Match', 'source':'Source', 'blockquote':'BlockQuote', 'pasteplain':'PastePlain', 'selectall':'SelectAll', 'print':'Print', 'preview':'Preview', 'horizontal':'Horizontal', 'removeformat':'RemoveFormat', 'time':'Time', 'date':'Date', 'unlink':'Unlink', 'insertrow':'InsertRow', 'insertcol':'InsertCol', 'mergeright':'MergeRight', 'mergedown':'MergeDown', 'deleterow':'DeleteRow', 'deletecol':'DeleteCol', 'splittorows':'SplitToRows','insertcode':'insert code', 'splittocols':'SplitToCols', 'splittocells':'SplitToCells','deletecaption':'DeleteCaption','inserttitle':'InsertTitle', 'mergecells':'MergeCells', 'deletetable':'DeleteTable', 'cleardoc':'Clear', 'insertparagraphbeforetable':"InsertParagraphBeforeTable", 'fontfamily':'FontFamily', 'fontsize':'FontSize', 'paragraph':'Paragraph','simpleupload':'Single Image','insertimage':'Multi Image','edittable':'Edit Table', 'edittd':'Edit Td','link':'Link', 'emotion':'Emotion', 'spechars':'Spechars', 'searchreplace':'SearchReplace', 'map':'BaiduMap', 'gmap':'GoogleMap', 'insertvideo':'Video', 'help':'Help', 'justifyleft':'JustifyLeft', 'justifyright':'JustifyRight', 'justifycenter':'JustifyCenter', 'justifyjustify':'Justify', 'forecolor':'FontColor', 'backcolor':'BackColor', 'insertorderedlist':'OL', 'insertunorderedlist':'UL', 'fullscreen':'FullScreen', 'directionalityltr':'EnterFromLeft', 'directionalityrtl':'EnterFromRight', 'rowspacingtop':'RowSpacingTop', 'rowspacingbottom':'RowSpacingBottom', 'pagebreak':'PageBreak', 'insertframe':'Iframe', 'imagenone':'Default', 'imageleft':'ImageLeft', 'imageright':'ImageRight', 'attachment':'Attachment', 'imagecenter':'ImageCenter', 'wordimage':'WordImage', 'lineheight':'LineHeight','edittip':'EditTip','customstyle':'CustomStyle', 'scrawl':'Scrawl', 'autotypeset':'AutoTypeset', 'webapp':'WebAPP', 'touppercase':'UpperCase', 'tolowercase':'LowerCase','template':'Template','background':'Background','inserttable':'InsertTable', 'music':'Music', 'charts': 'charts','drafts': 'Load from Drafts' }, 'insertorderedlist':{ 'num':'1,2,3...', 'num1':'1),2),3)...', 'num2':'(1),(2),(3)...', 'cn':'一,二,三....', 'cn1':'一),二),三)....', 'cn2':'(一),(二),(三)....', 'decimal':'1,2,3...', 'lower-alpha':'a,b,c...', 'lower-roman':'i,ii,iii...', 'upper-alpha':'A,B,C...', 'upper-roman':'I,II,III...' }, 'insertunorderedlist':{ 'circle':'○ Circle', 'disc':'● Circle dot', 'square':'■ Rectangle ', 'dash' :'- Dash', 'dot' : '。dot' }, 'paragraph':{'p':'Paragraph', 'h1':'Title 1', 'h2':'Title 2', 'h3':'Title 3', 'h4':'Title 4', 'h5':'Title 5', 'h6':'Title 6'}, 'fontfamily':{ 'songti':'Sim Sun', 'kaiti':'Sim Kai', 'heiti':'Sim Hei', 'lishu':'Sim Li', 'yahei': 'Microsoft YaHei', 'andaleMono':'Andale Mono', 'arial': 'Arial', 'arialBlack':'Arial Black', 'comicSansMs':'Comic Sans MS', 'impact':'Impact', 'timesNewRoman':'Times New Roman' }, 'customstyle':{ 'tc':'Title center', 'tl':'Title left', 'im':'Important', 'hi':'Highlight' }, 'autoupload': { 'exceedSizeError': 'File Size Exceed', 'exceedTypeError': 'File Type Not Allow', 'jsonEncodeError': 'Server Return Format Error', 'loading':"loading...", 'loadError':"load error", 'errorLoadConfig': 'Server config not loaded, upload can not work.', }, 'simpleupload':{ 'exceedSizeError': 'File Size Exceed', 'exceedTypeError': 'File Type Not Allow', 'jsonEncodeError': 'Server Return Format Error', 'loading':"loading...", 'loadError':"load error", 'errorLoadConfig': 'Server config not loaded, upload can not work.', }, 'elementPathTip':"Path", 'wordCountTip':"Word Count", 'wordCountMsg':'{#count} characters entered,{#leave} left. ', 'wordOverFlowMsg':'The number of characters has exceeded allowable maximum values, the server may refuse to save!', 'ok':"OK", 'cancel':"Cancel", 'closeDialog':"closeDialog", 'tableDrag':"You must import the file uiUtils.js before drag! ", 'autofloatMsg':"The plugin AutoFloat depends on EditorUI!", 'loadconfigError': 'Get server config error.', 'loadconfigFormatError': 'Server config format error.', 'loadconfigHttpError': 'Get server config http error.', 'snapScreen_plugin':{ 'browserMsg':"Only IE supported!", 'callBackErrorMsg':"The callback data is wrong,please check the config!", 'uploadErrorMsg':"Upload error,please check your server environment! " }, 'insertcode':{ 'as3':'ActionScript 3', 'bash':'Bash/Shell', 'cpp':'C/C++', 'css':'CSS', 'cf':'ColdFusion', 'c#':'C#', 'delphi':'Delphi', 'diff':'Diff', 'erlang':'Erlang', 'groovy':'Groovy', 'html':'HTML', 'java':'Java', 'jfx':'JavaFX', 'js':'JavaScript', 'pl':'Perl', 'php':'PHP', 'plain':'Plain Text', 'ps':'PowerShell', 'python':'Python', 'ruby':'Ruby', 'scala':'Scala', 'sql':'SQL', 'vb':'Visual Basic', 'xml':'XML' }, 'confirmClear':"Do you confirm to clear the Document?", 'contextMenu':{ 'delete':"Delete", 'selectall':"Select all", 'deletecode':"Delete Code", 'cleardoc':"Clear Document", 'confirmclear':"Do you confirm to clear the Document?", 'unlink':"Unlink", 'paragraph':"Paragraph", 'edittable':"Table property", 'aligncell':'Align cell', 'aligntable':'Table alignment', 'tableleft':'Left float', 'tablecenter':'Center', 'tableright':'Right float', 'aligntd':'Cell alignment', 'edittd':"Cell property", 'setbordervisible':'set table edge visible', 'table':"Table", 'justifyleft':'Justify Left', 'justifyright':'Justify Right', 'justifycenter':'Justify Center', 'justifyjustify':'Default', 'deletetable':"Delete table", 'insertparagraphbefore':"InsertedBeforeLine", 'insertparagraphafter':'InsertedAfterLine', 'inserttable':'Insert table', 'insertcaption':'Insert caption', 'deletecaption':'Delete Caption', 'inserttitle':'Insert Title', 'deletetitle':'Delete Title', 'inserttitlecol':'Insert Title Col', 'deletetitlecol':'Delete Title Col', 'averageDiseRow':'AverageDise Row', 'averageDisCol':'AverageDis Col', 'deleterow':"Delete row", 'deletecol':"Delete col", 'insertrow':"Insert row", 'insertcol':"Insert col", 'insertrownext':'Insert Row Next', 'insertcolnext':'Insert Col Next', 'mergeright':"Merge right", 'mergeleft':"Merge left", 'mergedown':"Merge down", 'mergecells':"Merge cells", 'splittocells':"Split to cells", 'splittocols':"Split to Cols", 'splittorows':"Split to Rows", 'tablesort':'Table sorting', 'enablesort':'Sorting Enable', 'disablesort':'Sorting Disable', 'reversecurrent':'Reverse current', 'orderbyasc':'Order By ASCII', 'reversebyasc':'Reverse By ASCII', 'orderbynum':'Order By Num', 'reversebynum':'Reverse By Num', 'borderbk':'Border shading', 'setcolor':'interlaced color', 'unsetcolor':'Cancel interlacedcolor', 'setbackground':'Background interlaced', 'unsetbackground':'Cancel Bk interlaced', 'redandblue':'Blue and red', 'threecolorgradient':'Three-color gradient', 'copy':"Copy(Ctrl + c)", 'copymsg':"Browser does not support. Please use 'Ctrl + c' instead!", 'paste':"Paste(Ctrl + v)", 'pastemsg':"Browser does not support. Please use 'Ctrl + v' instead!" }, 'copymsg': "Browser does not support. Please use 'Ctrl + c' instead!", 'pastemsg': "Browser does not support. Please use 'Ctrl + v' instead!", 'anthorMsg':"Link", 'clearColor':'Clear', 'standardColor':'Standard color', 'themeColor':'Theme color', 'property':'Property', 'default':'Default', 'modify':'Modify', 'justifyleft':'Justify Left', 'justifyright':'Justify Right', 'justifycenter':'Justify Center', 'justify':'Default', 'clear':'Clear', 'anchorMsg':'Anchor', 'delete':'Delete', 'clickToUpload':"Click to upload", 'unset':'Language hasn\'t been set!', 't_row':'row', 't_col':'col', 'pasteOpt':'Paste Option', 'pasteSourceFormat':"Keep Source Formatting", 'tagFormat':'Keep tag', 'pasteTextFormat':'Keep Text only', 'more':'More', 'autoTypeSet':{ 'mergeLine':"Merge empty line", 'delLine':"Del empty line", 'removeFormat':"Remove format", 'indent':"Indent", 'alignment':"Alignment", 'imageFloat':"Image float", 'removeFontsize':"Remove font size", 'removeFontFamily':"Remove fontFamily", 'removeHtml':"Remove redundant HTML code", 'pasteFilter':"Paste filter", 'run':"Done", 'symbol':'Symbol Conversion', 'bdc2sb':'Full-width to Half-width', 'tobdc':'Half-width to Full-width' }, 'background':{ 'static':{ 'lang_background_normal':'Normal', 'lang_background_local':'Online', 'lang_background_set':'Background Set', 'lang_background_none':'No Background', 'lang_background_colored':'Colored Background', 'lang_background_color':'Color Set', 'lang_background_netimg':'Net-Image', 'lang_background_align':'Align Type', 'lang_background_position':'Position', 'repeatType':{'options':["Center", "Repeat-x", "Repeat-y", "Tile","Custom"]} }, 'noUploadImage':"No pictures has been uploaded!", 'toggleSelect':'Change the active state by click!\n Image Size: ' }, //===============dialog i18N======================= 'insertimage':{ 'static':{ 'lang_tab_remote':"Insert", 'lang_tab_upload':"Local", 'lang_tab_online':"Manager", 'lang_tab_search':"Search", 'lang_input_url':"Address:", 'lang_input_size':"Size:", 'lang_input_width':"Width", 'lang_input_height':"Height", 'lang_input_border':"Border:", 'lang_input_vhspace':"Margins:", 'lang_input_title':"Title:", 'lang_input_align':'Image Float Style:', 'lang_imgLoading':"Loading...", 'lang_start_upload':"Start Upload", 'lock':{'title':"Lock rate"}, 'searchType':{'title':"ImageType", 'options':["News", "Wallpaper", "emotions", "photo"]}, 'searchTxt':{'value':"Enter the search keyword!"}, 'searchBtn':{'value':"Search"}, 'searchReset':{'value':"Clear"}, 'noneAlign':{'title':'None Float'}, 'leftAlign':{'title':'Left Float'}, 'rightAlign':{'title':'Right Float'}, 'centerAlign':{'title':'Center In A Line'} }, 'uploadSelectFile':'Select File', 'uploadAddFile':'Add File', 'uploadStart':'Start Upload', 'uploadPause':'Pause Upload', 'uploadContinue':'Continue Upload', 'uploadRetry':'Retry Upload', 'uploadDelete':'Delete', 'uploadTurnLeft':'Turn Left', 'uploadTurnRight':'Turn Right', 'uploadPreview':'Doing Preview', 'uploadNoPreview':'Can Not Preview', 'updateStatusReady': 'Selected _ pictures, total _KB.', 'updateStatusConfirm': '_ uploaded successfully and _ upload failed', 'updateStatusFinish': 'Total _ pictures (_KB), _ uploaded successfully', 'updateStatusError': ' and _ upload failed', 'errorNotSupport': 'WebUploader does not support the browser you are using. Please upgrade your browser or flash player', 'errorLoadConfig': 'Server config not loaded, upload can not work.', 'errorExceedSize':'File Size Exceed', 'errorFileType':'File Type Not Allow', 'errorInterrupt':'File Upload Interrupted', 'errorUploadRetry':'Upload Error, Please Retry.', 'errorHttp':'Http Error', 'errorServerUpload':'Server Result Error.', 'remoteLockError':"Cannot Lock the Proportion between width and height", 'numError':"Please enter the correct Num. e.g 123,400", 'imageUrlError':"The image format may be wrong!", 'imageLoadError':"Error,please check the network or URL!", 'searchRemind':"Enter the search keyword!", 'searchLoading':"Image is loading,please wait...", 'searchRetry':" Sorry,can't find the image,please try again!" }, 'attachment':{ 'static':{ 'lang_tab_upload': 'Upload', 'lang_tab_online': 'Online', 'lang_start_upload':"Start upload", 'lang_drop_remind':"You can drop files here, a single maximum of 300 files" }, 'uploadSelectFile':'Select File', 'uploadAddFile':'Add File', 'uploadStart':'Start Upload', 'uploadPause':'Pause Upload', 'uploadContinue':'Continue Upload', 'uploadRetry':'Retry Upload', 'uploadDelete':'Delete', 'uploadTurnLeft':'Turn Left', 'uploadTurnRight':'Turn Right', 'uploadPreview':'Doing Preview', 'updateStatusReady': 'Selected _ files, total _KB.', 'updateStatusConfirm': '_ uploaded successfully and _ upload failed', 'updateStatusFinish': 'Total _ files (_KB), _ uploaded successfully', 'updateStatusError': ' and _ upload failed', 'errorNotSupport': 'WebUploader does not support the browser you are using. Please upgrade your browser or flash player', 'errorLoadConfig': 'Server config not loaded, upload can not work.', 'errorExceedSize':'File Size Exceed', 'errorFileType':'File Type Not Allow', 'errorInterrupt':'File Upload Interrupted', 'errorUploadRetry':'Upload Error, Please Retry.', 'errorHttp':'Http Error', 'errorServerUpload':'Server Result Error.' }, 'insertvideo':{ 'static':{ 'lang_tab_insertV':"Video", 'lang_tab_searchV':"Search", 'lang_tab_uploadV':"Upload", 'lang_video_url':" URL ", 'lang_video_size':"Video Size", 'lang_videoW':"Width", 'lang_videoH':"Height", 'lang_alignment':"Alignment", 'videoSearchTxt':{'value':"Enter the search keyword!"}, 'videoType':{'options':["All", "Hot", "Entertainment", "Funny", "Sports", "Science", "variety"]}, 'videoSearchBtn':{'value':"Search in Baidu"}, 'videoSearchReset':{'value':"Clear result"}, 'lang_input_fileStatus':' No file uploaded!', 'startUpload':{'style':"background:url(upload.png) no-repeat;"}, 'lang_upload_size':"Video Size", 'lang_upload_width':"Width", 'lang_upload_height':"Height", 'lang_upload_alignment':"Alignment", 'lang_format_advice':"Recommends mp4 format." }, 'numError':"Please enter the correct Num. e.g 123,400", 'floatLeft':"Float left", 'floatRight':"Float right", 'default':"Default", 'block':"Display in block", 'urlError':"The video url format may be wrong!", 'loading':"  The video is loading, please wait…", 'clickToSelect':"Click to select", 'goToSource':'Visit source video ', 'noVideo':"    Sorry,can't find the video,please try again!", 'browseFiles':'Open files', 'uploadSuccess':'Upload Successful!', 'delSuccessFile':'Remove from the success of the queue', 'delFailSaveFile':'Remove the save failed file', 'statusPrompt':' file(s) uploaded! ', 'flashVersionError':'The current Flash version is too low, please update FlashPlayer,then try again!', 'flashLoadingError':'The Flash failed loading! Please check the path or network state', 'fileUploadReady':'Wait for uploading...', 'delUploadQueue':'Remove from the uploading queue ', 'limitPrompt1':'Can not choose more than single', 'limitPrompt2':'file(s)!Please choose again!', 'delFailFile':'Remove failure file', 'fileSizeLimit':'File size exceeds the limit!', 'emptyFile':'Can not upload an empty file!', 'fileTypeError':'File type error!', 'unknownError':'Unknown error!', 'fileUploading':'Uploading,please wait...', 'cancelUpload':'Cancel upload', 'netError':'Network error', 'failUpload':'Upload failed', 'serverIOError':'Server IO error!', 'noAuthority':'No Permission!', 'fileNumLimit':'Upload limit to the number', 'failCheck':'Authentication fails, the upload is skipped!', 'fileCanceling':'Cancel, please wait...', 'stopUploading':'Upload has stopped...', 'uploadSelectFile':'Select File', 'uploadAddFile':'Add File', 'uploadStart':'Start Upload', 'uploadPause':'Pause Upload', 'uploadContinue':'Continue Upload', 'uploadRetry':'Retry Upload', 'uploadDelete':'Delete', 'uploadTurnLeft':'Turn Left', 'uploadTurnRight':'Turn Right', 'uploadPreview':'Doing Preview', 'updateStatusReady': 'Selected _ files, total _KB.', 'updateStatusConfirm': '_ uploaded successfully and _ upload failed', 'updateStatusFinish': 'Total _ files (_KB), _ uploaded successfully', 'updateStatusError': ' and _ upload failed', 'errorNotSupport': 'WebUploader does not support the browser you are using. Please upgrade your browser or flash player', 'errorLoadConfig': 'Server config not loaded, upload can not work.', 'errorExceedSize':'File Size Exceed', 'errorFileType':'File Type Not Allow', 'errorInterrupt':'File Upload Interrupted', 'errorUploadRetry':'Upload Error, Please Retry.', 'errorHttp':'Http Error', 'errorServerUpload':'Server Result Error.' }, 'webapp':{ 'tip1':"This function provided by Baidu APP,please apply for baidu APPKey webmaster first!", 'tip2':"And then open the file ueditor.config.js to set it! ", 'applyFor':"APPLY FOR", 'anthorApi':"Baidu API" }, 'template':{ 'static':{ 'lang_template_bkcolor':'Background Color', 'lang_template_clear' : 'Keep Content', 'lang_template_select':'Select Template' }, 'blank':"Blank", 'blog':"Blog", 'resume':"Resume", 'richText':"Rich Text", 'scrPapers':"Scientific Papers" }, scrawl:{ 'static':{ 'lang_input_previousStep':"Previous", 'lang_input_nextsStep':"Next", 'lang_input_clear':'Clear', 'lang_input_addPic':'AddImage', 'lang_input_ScalePic':'ScaleImage', 'lang_input_removePic':'RemoveImage', 'J_imgTxt':{title:'Add background image'} }, 'noScarwl':"No paint, a white paper...", 'scrawlUpLoading':"Image is uploading, please wait...", 'continueBtn':"Try again", 'imageError':"Image failed to load!", 'backgroundUploading':'Image is uploading,please wait...' }, 'music':{ 'static':{ 'lang_input_tips':"Input singer/song/album, search you interested in music!", 'J_searchBtn':{value:'Search songs'} }, 'emptyTxt':'Not search to the relevant music results, please change a keyword try.', 'chapter':'Songs', 'singer':'Singer', 'special':'Album', 'listenTest':'Audition' }, anchor:{ 'static':{ 'lang_input_anchorName':'Anchor Name:' } }, 'charts':{ 'static':{ 'lang_data_source':'Data source:', 'lang_chart_format': 'Chart format:', 'lang_data_align': 'Align', 'lang_chart_align_same': 'Consistent with the X-axis Y-axis', 'lang_chart_align_reverse': 'X-axis Y-axis opposite', 'lang_chart_title': 'Title', 'lang_chart_main_title': 'main title:', 'lang_chart_sub_title': 'sub title:', 'lang_chart_x_title': 'X-axis title:', 'lang_chart_y_title': 'Y-axis title:', 'lang_chart_tip': 'Prompt', 'lang_cahrt_tip_prefix': 'prefix:', 'lang_cahrt_tip_description': '仅饼图有效, 当鼠标移动到饼图中相应的块上时,提示框内的文字的前缀', 'lang_chart_data_unit': 'Unit', 'lang_chart_data_unit_title': 'unit:', 'lang_chart_data_unit_description': '显示在每个数据点上的数据的单位, 比如: 温度的单位 ℃', 'lang_chart_type': 'Chart type:', 'lang_prev_btn': 'Previous', 'lang_next_btn': 'Next' } }, emotion:{ 'static':{ 'lang_input_choice':'Choice', 'lang_input_Tuzki':'Tuzki', 'lang_input_lvdouwa':'LvDouWa', 'lang_input_BOBO':'BOBO', 'lang_input_babyCat':'BabyCat', 'lang_input_bubble':'Bubble', 'lang_input_youa':'YouA' } }, gmap:{ 'static':{ 'lang_input_address':'Address:', 'lang_input_search':'Search', 'address':{value:"Beijing"} }, searchError:'Unable to locate the address!' }, help:{ 'static':{ 'lang_input_about':'About', 'lang_input_shortcuts':'Shortcuts', 'lang_input_introduction':"UEditor is developed by Baidu Co.ltd. It is lightweight, customizable , focusing on user experience and etc. , UEditor is based on open source BSD license , allowing free use and redistribution.", 'lang_Txt_shortcuts':'Shortcuts', 'lang_Txt_func':'Function', 'lang_Txt_bold':'Bold', 'lang_Txt_copy':'Copy', 'lang_Txt_cut':'Cut', 'lang_Txt_Paste':'Paste', 'lang_Txt_undo':'Undo', 'lang_Txt_redo':'Redo', 'lang_Txt_italic':'Italic', 'lang_Txt_underline':'Underline', 'lang_Txt_selectAll':'Select All', 'lang_Txt_visualEnter':'Submit', 'lang_Txt_fullscreen':'Fullscreen' } }, insertframe:{ 'static':{ 'lang_input_address':'Address:', 'lang_input_width':'Width:', 'lang_input_height':'height:', 'lang_input_isScroll':'Enable scrollbars:', 'lang_input_frameborder':'Show frame border:', 'lang_input_alignMode':'Alignment:', 'align':{title:"Alignment", options:["Default", "Left", "Right", "Center"]} }, 'enterAddress':'Please enter an address!' }, link:{ 'static':{ 'lang_input_text':'Text:', 'lang_input_url':'URL:', 'lang_input_title':'Title:', 'lang_input_target':'open in new window:' }, 'validLink':'Supports only effective when a link is selected', 'httpPrompt':'The hyperlink you enter should start with "http|https|ftp://"!' }, map:{ 'static':{ lang_city:"City", lang_address:"Address", city:{value:"Beijing"}, lang_search:"Search", lang_dynamicmap:"Dynamic map" }, cityMsg:"Please enter the city name!", errorMsg:"Can't find the place!" }, searchreplace:{ 'static':{ lang_tab_search:"Search", lang_tab_replace:"Replace", lang_search1:"Search", lang_search2:"Search", lang_replace:"Replace", lang_searchReg:'Support regular expression ,which starts and ends with a slash ,for example "/expression/"', lang_searchReg1:'Support regular expression ,which starts and ends with a slash ,for example "/expression/"', lang_case_sensitive1:"Case sense", lang_case_sensitive2:"Case sense", nextFindBtn:{value:"Next"}, preFindBtn:{value:"Preview"}, nextReplaceBtn:{value:"Next"}, preReplaceBtn:{value:"Preview"}, repalceBtn:{value:"Replace"}, repalceAllBtn:{value:"Replace all"} }, getEnd:"Has the search to the bottom!", getStart:"Has the search to the top!", countMsg:"Altogether replaced {#count} character(s)!" }, snapscreen:{ 'static':{ lang_showMsg:"You should install the UEditor screenshots program first!", lang_download:"Download!", lang_step1:"Step1:Download the program and then run it", lang_step2:"Step2:After complete install,try to click the button again" } }, spechars:{ 'static':{}, tsfh:"Special", lmsz:"Roman", szfh:"Numeral", rwfh:"Japanese", xlzm:"The Greek", ewzm:"Russian", pyzm:"Phonetic", yyyb:"English", zyzf:"Others" }, 'edittable':{ 'static':{ 'lang_tableStyle':'Table style', 'lang_insertCaption':'Add table header row', 'lang_insertTitle':'Add table title row', 'lang_insertTitleCol':'Add table title col', 'lang_tableSize':'Automatically adjust table size', 'lang_autoSizeContent':'Adaptive by form text', 'lang_orderbycontent':"Table of contents sortable", 'lang_autoSizePage':'Page width adaptive', 'lang_example':'Example', 'lang_borderStyle':'Table Border', 'lang_color':'Color:' }, captionName:'Caption', titleName:'Title', cellsName:'text', errorMsg:'There are merged cells, can not sort.' }, 'edittip':{ 'static':{ lang_delRow:'Delete entire row', lang_delCol:'Delete entire col' } }, 'edittd':{ 'static':{ lang_tdBkColor:'Background Color:' } }, 'formula':{ 'static':{ } }, wordimage:{ 'static':{ lang_resave:"The re-save step", uploadBtn:{src:"upload.png", alt:"Upload"}, clipboard:{style:"background: url(copy.png) -153px -1px no-repeat;"}, lang_step:" 1. Click top button to copy the url and then open the dialog to paste it. 2. Open after choose photos uploaded process." }, fileType:"Image", flashError:"Flash initialization failed!", netError:"Network error! Please try again!", copySuccess:"URL has been copied!", 'flashI18n':{ lang:encodeURI( '{"UploadingState":"totalNum: ${a},uploadComplete: ${b}", "BeforeUpload":"waitingNum: ${a}", "ExceedSize":"Size exceed${a}", "ErrorInPreview":"Preview failed", "DefaultDescription":"Description", "LoadingImage":"Loading..."}' ), uploadingTF:encodeURI( '{"font":"Arial", "size":12, "color":"0x000", "bold":"true", "italic":"false", "underline":"false"}' ), imageTF:encodeURI( '{"font":"Arial", "size":11, "color":"red", "bold":"false", "italic":"false", "underline":"false"}' ), textEncoding:"utf-8", addImageSkinURL:"addImage.png", allDeleteBtnUpSkinURL:"allDeleteBtnUpSkin.png", allDeleteBtnHoverSkinURL:"allDeleteBtnHoverSkin.png", rotateLeftBtnEnableSkinURL:"rotateLeftEnable.png", rotateLeftBtnDisableSkinURL:"rotateLeftDisable.png", rotateRightBtnEnableSkinURL:"rotateRightEnable.png", rotateRightBtnDisableSkinURL:"rotateRightDisable.png", deleteBtnEnableSkinURL:"deleteEnable.png", deleteBtnDisableSkinURL:"deleteDisable.png", backgroundURL:'', listBackgroundURL:'', buttonURL:'button.png' } }, 'autosave': { 'success':'Local conservation success' } }; ================================================ FILE: yshop-drink-vue3/public/UEditor22/lang/zh-cn/zh-cn.js ================================================ /** * Created with JetBrains PhpStorm. * User: taoqili * Date: 12-6-12 * Time: 下午5:02 * To change this template use File | Settings | File Templates. */ UE.I18N['zh-cn'] = { 'labelMap':{ 'anchor':'锚点', 'undo':'撤销', 'redo':'重做', 'bold':'加粗', 'indent':'首行缩进', 'snapscreen':'截图', 'italic':'斜体', 'underline':'下划线', 'strikethrough':'删除线', 'subscript':'下标','fontborder':'字符边框', 'superscript':'上标', 'formatmatch':'格式刷', 'source':'源代码', 'blockquote':'引用', 'pasteplain':'纯文本粘贴模式', 'selectall':'全选', 'print':'打印', 'preview':'预览', 'horizontal':'分隔线', 'removeformat':'清除格式', 'time':'时间', 'date':'日期', 'unlink':'取消链接', 'insertrow':'前插入行', 'insertcol':'前插入列', 'mergeright':'右合并单元格', 'mergedown':'下合并单元格', 'deleterow':'删除行', 'deletecol':'删除列', 'splittorows':'拆分成行', 'splittocols':'拆分成列', 'splittocells':'完全拆分单元格','deletecaption':'删除表格标题','inserttitle':'插入标题', 'mergecells':'合并多个单元格', 'deletetable':'删除表格', 'cleardoc':'清空文档','insertparagraphbeforetable':"表格前插入行",'insertcode':'代码语言', 'fontfamily':'字体', 'fontsize':'字号', 'paragraph':'段落格式', 'simpleupload':'单图上传', 'insertimage':'多图上传','edittable':'表格属性','edittd':'单元格属性', 'link':'超链接', 'emotion':'表情', 'spechars':'特殊字符', 'searchreplace':'查询替换', 'map':'Baidu地图', 'gmap':'Google地图', 'insertvideo':'视频', 'help':'帮助', 'justifyleft':'居左对齐', 'justifyright':'居右对齐', 'justifycenter':'居中对齐', 'justifyjustify':'两端对齐', 'forecolor':'字体颜色', 'backcolor':'背景色', 'insertorderedlist':'有序列表', 'insertunorderedlist':'无序列表', 'fullscreen':'全屏', 'directionalityltr':'从左向右输入', 'directionalityrtl':'从右向左输入', 'rowspacingtop':'段前距', 'rowspacingbottom':'段后距', 'pagebreak':'分页', 'insertframe':'插入Iframe', 'imagenone':'默认', 'imageleft':'左浮动', 'imageright':'右浮动', 'attachment':'附件', 'imagecenter':'居中', 'wordimage':'图片转存', 'lineheight':'行间距','edittip' :'编辑提示','customstyle':'自定义标题', 'autotypeset':'自动排版', 'webapp':'百度应用','touppercase':'字母大写', 'tolowercase':'字母小写','background':'背景','template':'模板','scrawl':'涂鸦', 'music':'音乐','inserttable':'插入表格','drafts': '从草稿箱加载', 'charts': '图表' }, 'insertorderedlist':{ 'num':'1,2,3...', 'num1':'1),2),3)...', 'num2':'(1),(2),(3)...', 'cn':'一,二,三....', 'cn1':'一),二),三)....', 'cn2':'(一),(二),(三)....', 'decimal':'1,2,3...', 'lower-alpha':'a,b,c...', 'lower-roman':'i,ii,iii...', 'upper-alpha':'A,B,C...', 'upper-roman':'I,II,III...' }, 'insertunorderedlist':{ 'circle':'○ 大圆圈', 'disc':'● 小黑点', 'square':'■ 小方块 ', 'dash' :'— 破折号', 'dot':' 。 小圆圈' }, 'paragraph':{'p':'段落', 'h1':'标题 1', 'h2':'标题 2', 'h3':'标题 3', 'h4':'标题 4', 'h5':'标题 5', 'h6':'标题 6'}, 'fontfamily':{ 'songti':'宋体', 'kaiti':'楷体', 'heiti':'黑体', 'lishu':'隶书', 'yahei':'微软雅黑', 'andaleMono':'andale mono', 'arial': 'arial', 'arialBlack':'arial black', 'comicSansMs':'comic sans ms', 'impact':'impact', 'timesNewRoman':'times new roman' }, 'customstyle':{ 'tc':'标题居中', 'tl':'标题居左', 'im':'强调', 'hi':'明显强调' }, 'autoupload': { 'exceedSizeError': '文件大小超出限制', 'exceedTypeError': '文件格式不允许', 'jsonEncodeError': '服务器返回格式错误', 'loading':"正在上传...", 'loadError':"上传错误", 'errorLoadConfig': '后端配置项没有正常加载,上传插件不能正常使用!' }, 'simpleupload':{ 'exceedSizeError': '文件大小超出限制', 'exceedTypeError': '文件格式不允许', 'jsonEncodeError': '服务器返回格式错误', 'loading':"正在上传...", 'loadError':"上传错误", 'errorLoadConfig': '后端配置项没有正常加载,上传插件不能正常使用!' }, 'elementPathTip':"元素路径", 'wordCountTip':"字数统计", 'wordCountMsg':'当前已输入{#count}个字符, 您还可以输入{#leave}个字符。 ', 'wordOverFlowMsg':'字数超出最大允许值,服务器可能拒绝保存!', 'ok':"确认", 'cancel':"取消", 'closeDialog':"关闭对话框", 'tableDrag':"表格拖动必须引入uiUtils.js文件!", 'autofloatMsg':"工具栏浮动依赖编辑器UI,您首先需要引入UI文件!", 'loadconfigError': '获取后台配置项请求出错,上传功能将不能正常使用!', 'loadconfigFormatError': '后台配置项返回格式出错,上传功能将不能正常使用!', 'loadconfigHttpError': '请求后台配置项http错误,上传功能将不能正常使用!', 'snapScreen_plugin':{ 'browserMsg':"仅支持IE浏览器!", 'callBackErrorMsg':"服务器返回数据有误,请检查配置项之后重试。", 'uploadErrorMsg':"截图上传失败,请检查服务器端环境! " }, 'insertcode':{ 'as3':'ActionScript 3', 'bash':'Bash/Shell', 'cpp':'C/C++', 'css':'CSS', 'cf':'ColdFusion', 'c#':'C#', 'delphi':'Delphi', 'diff':'Diff', 'erlang':'Erlang', 'groovy':'Groovy', 'html':'HTML', 'java':'Java', 'jfx':'JavaFX', 'js':'JavaScript', 'pl':'Perl', 'php':'PHP', 'plain':'Plain Text', 'ps':'PowerShell', 'python':'Python', 'ruby':'Ruby', 'scala':'Scala', 'sql':'SQL', 'vb':'Visual Basic', 'xml':'XML' }, 'confirmClear':"确定清空当前文档么?", 'contextMenu':{ 'delete':"删除", 'selectall':"全选", 'deletecode':"删除代码", 'cleardoc':"清空文档", 'confirmclear':"确定清空当前文档么?", 'unlink':"删除超链接", 'paragraph':"段落格式", 'edittable':"表格属性", 'aligntd':"单元格对齐方式", 'aligntable':'表格对齐方式', 'tableleft':'左浮动', 'tablecenter':'居中显示', 'tableright':'右浮动', 'edittd':"单元格属性", 'setbordervisible':'设置表格边线可见', 'justifyleft':'左对齐', 'justifyright':'右对齐', 'justifycenter':'居中对齐', 'justifyjustify':'两端对齐', 'table':"表格", 'inserttable':'插入表格', 'deletetable':"删除表格", 'insertparagraphbefore':"前插入段落", 'insertparagraphafter':'后插入段落', 'deleterow':"删除当前行", 'deletecol':"删除当前列", 'insertrow':"前插入行", 'insertcol':"左插入列", 'insertrownext':'后插入行', 'insertcolnext':'右插入列', 'insertcaption':'插入表格名称', 'deletecaption':'删除表格名称', 'inserttitle':'插入表格标题行', 'deletetitle':'删除表格标题行', 'inserttitlecol':'插入表格标题列', 'deletetitlecol':'删除表格标题列', 'averageDiseRow':'平均分布各行', 'averageDisCol':'平均分布各列', 'mergeright':"向右合并", 'mergeleft':"向左合并", 'mergedown':"向下合并", 'mergecells':"合并单元格", 'splittocells':"完全拆分单元格", 'splittocols':"拆分成列", 'splittorows':"拆分成行", 'tablesort':'表格排序', 'enablesort':'设置表格可排序', 'disablesort':'取消表格可排序', 'reversecurrent':'逆序当前', 'orderbyasc':'按ASCII字符升序', 'reversebyasc':'按ASCII字符降序', 'orderbynum':'按数值大小升序', 'reversebynum':'按数值大小降序', 'borderbk':'边框底纹', 'setcolor':'表格隔行变色', 'unsetcolor':'取消表格隔行变色', 'setbackground':'选区背景隔行', 'unsetbackground':'取消选区背景', 'redandblue':'红蓝相间', 'threecolorgradient':'三色渐变', 'copy':"复制(Ctrl + c)", 'copymsg': "浏览器不支持,请使用 'Ctrl + c'", 'paste':"粘贴(Ctrl + v)", 'pastemsg': "浏览器不支持,请使用 'Ctrl + v'" }, 'copymsg': "浏览器不支持,请使用 'Ctrl + c'", 'pastemsg': "浏览器不支持,请使用 'Ctrl + v'", 'anthorMsg':"链接", 'clearColor':'清空颜色', 'standardColor':'标准颜色', 'themeColor':'主题颜色', 'property':'属性', 'default':'默认', 'modify':'修改', 'justifyleft':'左对齐', 'justifyright':'右对齐', 'justifycenter':'居中', 'justify':'默认', 'clear':'清除', 'anchorMsg':'锚点', 'delete':'删除', 'clickToUpload':"点击上传", 'unset':'尚未设置语言文件', 't_row':'行', 't_col':'列', 'more':'更多', 'pasteOpt':'粘贴选项', 'pasteSourceFormat':"保留源格式", 'tagFormat':'只保留标签', 'pasteTextFormat':'只保留文本', 'autoTypeSet':{ 'mergeLine':"合并空行", 'delLine':"清除空行", 'removeFormat':"清除格式", 'indent':"首行缩进", 'alignment':"对齐方式", 'imageFloat':"图片浮动", 'removeFontsize':"清除字号", 'removeFontFamily':"清除字体", 'removeHtml':"清除冗余HTML代码", 'pasteFilter':"粘贴过滤", 'run':"执行", 'symbol':'符号转换', 'bdc2sb':'全角转半角', 'tobdc':'半角转全角' }, 'background':{ 'static':{ 'lang_background_normal':'背景设置', 'lang_background_local':'在线图片', 'lang_background_set':'选项', 'lang_background_none':'无背景色', 'lang_background_colored':'有背景色', 'lang_background_color':'颜色设置', 'lang_background_netimg':'网络图片', 'lang_background_align':'对齐方式', 'lang_background_position':'精确定位', 'repeatType':{'options':["居中", "横向重复", "纵向重复", "平铺","自定义"]} }, 'noUploadImage':"当前未上传过任何图片!", 'toggleSelect':"单击可切换选中状态\n原图尺寸: " }, //===============dialog i18N======================= 'insertimage':{ 'static':{ 'lang_tab_remote':"插入图片", //节点 'lang_tab_upload':"本地上传", 'lang_tab_online':"在线管理", 'lang_tab_search':"图片搜索", 'lang_input_url':"地 址:", 'lang_input_size':"大 小:", 'lang_input_width':"宽度", 'lang_input_height':"高度", 'lang_input_border':"边 框:", 'lang_input_vhspace':"边 距:", 'lang_input_title':"描 述:", 'lang_input_align':'图片浮动方式:', 'lang_imgLoading':" 图片加载中……", 'lang_start_upload':"开始上传", 'lock':{'title':"锁定宽高比例"}, //属性 'searchType':{'title':"图片类型", 'options':["新闻", "壁纸", "表情", "头像"]}, //select的option 'searchTxt':{'value':"请输入搜索关键词"}, 'searchBtn':{'value':"百度一下"}, 'searchReset':{'value':"清空搜索"}, 'noneAlign':{'title':'无浮动'}, 'leftAlign':{'title':'左浮动'}, 'rightAlign':{'title':'右浮动'}, 'centerAlign':{'title':'居中独占一行'} }, 'uploadSelectFile':'点击选择图片', 'uploadAddFile':'继续添加', 'uploadStart':'开始上传', 'uploadPause':'暂停上传', 'uploadContinue':'继续上传', 'uploadRetry':'重试上传', 'uploadDelete':'删除', 'uploadTurnLeft':'向左旋转', 'uploadTurnRight':'向右旋转', 'uploadPreview':'预览中', 'uploadNoPreview':'不能预览', 'updateStatusReady': '选中_张图片,共_KB。', 'updateStatusConfirm': '已成功上传_张照片,_张照片上传失败', 'updateStatusFinish': '共_张(_KB),_张上传成功', 'updateStatusError': ',_张上传失败。', 'errorNotSupport': 'WebUploader 不支持您的浏览器!如果你使用的是IE浏览器,请尝试升级 flash 播放器。', 'errorLoadConfig': '后端配置项没有正常加载,上传插件不能正常使用!', 'errorExceedSize':'文件大小超出', 'errorFileType':'文件格式不允许', 'errorInterrupt':'文件传输中断', 'errorUploadRetry':'上传失败,请重试', 'errorHttp':'http请求错误', 'errorServerUpload':'服务器返回出错', 'remoteLockError':"宽高不正确,不能所定比例", 'numError':"请输入正确的长度或者宽度值!例如:123,400", 'imageUrlError':"不允许的图片格式或者图片域!", 'imageLoadError':"图片加载失败!请检查链接地址或网络状态!", 'searchRemind':"请输入搜索关键词", 'searchLoading':"图片加载中,请稍后……", 'searchRetry':" :( ,抱歉,没有找到图片!请重试一次!" }, 'attachment':{ 'static':{ 'lang_tab_upload': '上传附件', 'lang_tab_online': '在线附件', 'lang_start_upload':"开始上传", 'lang_drop_remind':"可以将文件拖到这里,单次最多可选100个文件" }, 'uploadSelectFile':'点击选择文件', 'uploadAddFile':'继续添加', 'uploadStart':'开始上传', 'uploadPause':'暂停上传', 'uploadContinue':'继续上传', 'uploadRetry':'重试上传', 'uploadDelete':'删除', 'uploadTurnLeft':'向左旋转', 'uploadTurnRight':'向右旋转', 'uploadPreview':'预览中', 'updateStatusReady': '选中_个文件,共_KB。', 'updateStatusConfirm': '已成功上传_个文件,_个文件上传失败', 'updateStatusFinish': '共_个(_KB),_个上传成功', 'updateStatusError': ',_张上传失败。', 'errorNotSupport': 'WebUploader 不支持您的浏览器!如果你使用的是IE浏览器,请尝试升级 flash 播放器。', 'errorLoadConfig': '后端配置项没有正常加载,上传插件不能正常使用!', 'errorExceedSize':'文件大小超出', 'errorFileType':'文件格式不允许', 'errorInterrupt':'文件传输中断', 'errorUploadRetry':'上传失败,请重试', 'errorHttp':'http请求错误', 'errorServerUpload':'服务器返回出错' }, 'insertvideo':{ 'static':{ 'lang_tab_insertV':"插入视频", 'lang_tab_searchV':"搜索视频", 'lang_tab_uploadV':"上传视频", 'lang_video_url':"视频网址", 'lang_video_size':"视频尺寸", 'lang_videoW':"宽度", 'lang_videoH':"高度", 'lang_alignment':"对齐方式", 'videoSearchTxt':{'value':"请输入搜索关键字!"}, 'videoType':{'options':["全部", "热门", "娱乐", "搞笑", "体育", "科技", "综艺"]}, 'videoSearchBtn':{'value':"百度一下"}, 'videoSearchReset':{'value':"清空结果"}, 'lang_input_fileStatus':' 当前未上传文件', 'startUpload':{'style':"background:url(upload.png) no-repeat;"}, 'lang_upload_size':"视频尺寸", 'lang_upload_width':"宽度", 'lang_upload_height':"高度", 'lang_upload_alignment':"对齐方式", 'lang_format_advice':"建议使用mp4格式." }, 'numError':"请输入正确的数值,如123,400", 'floatLeft':"左浮动", 'floatRight':"右浮动", '"default"':"默认", 'block':"独占一行", 'urlError':"输入的视频地址有误,请检查后再试!", 'loading':"  视频加载中,请等待……", 'clickToSelect':"点击选中", 'goToSource':'访问源视频', 'noVideo':"    抱歉,找不到对应的视频,请重试!", 'browseFiles':'浏览文件', 'uploadSuccess':'上传成功!', 'delSuccessFile':'从成功队列中移除', 'delFailSaveFile':'移除保存失败文件', 'statusPrompt':' 个文件已上传! ', 'flashVersionError':'当前Flash版本过低,请更新FlashPlayer后重试!', 'flashLoadingError':'Flash加载失败!请检查路径或网络状态', 'fileUploadReady':'等待上传……', 'delUploadQueue':'从上传队列中移除', 'limitPrompt1':'单次不能选择超过', 'limitPrompt2':'个文件!请重新选择!', 'delFailFile':'移除失败文件', 'fileSizeLimit':'文件大小超出限制!', 'emptyFile':'空文件无法上传!', 'fileTypeError':'文件类型不允许!', 'unknownError':'未知错误!', 'fileUploading':'上传中,请等待……', 'cancelUpload':'取消上传', 'netError':'网络错误', 'failUpload':'上传失败!', 'serverIOError':'服务器IO错误!', 'noAuthority':'无权限!', 'fileNumLimit':'上传个数限制', 'failCheck':'验证失败,本次上传被跳过!', 'fileCanceling':'取消中,请等待……', 'stopUploading':'上传已停止……', 'uploadSelectFile':'点击选择文件', 'uploadAddFile':'继续添加', 'uploadStart':'开始上传', 'uploadPause':'暂停上传', 'uploadContinue':'继续上传', 'uploadRetry':'重试上传', 'uploadDelete':'删除', 'uploadTurnLeft':'向左旋转', 'uploadTurnRight':'向右旋转', 'uploadPreview':'预览中', 'updateStatusReady': '选中_个文件,共_KB。', 'updateStatusConfirm': '成功上传_个,_个失败', 'updateStatusFinish': '共_个(_KB),_个成功上传', 'updateStatusError': ',_张上传失败。', 'errorNotSupport': 'WebUploader 不支持您的浏览器!如果你使用的是IE浏览器,请尝试升级 flash 播放器。', 'errorLoadConfig': '后端配置项没有正常加载,上传插件不能正常使用!', 'errorExceedSize':'文件大小超出', 'errorFileType':'文件格式不允许', 'errorInterrupt':'文件传输中断', 'errorUploadRetry':'上传失败,请重试', 'errorHttp':'http请求错误', 'errorServerUpload':'服务器返回出错' }, 'webapp':{ 'tip1':"本功能由百度APP提供,如看到此页面,请各位站长首先申请百度APPKey!", 'tip2':"申请完成之后请至ueditor.config.js中配置获得的appkey! ", 'applyFor':"点此申请", 'anthorApi':"百度API" }, 'template':{ 'static':{ 'lang_template_bkcolor':'背景颜色', 'lang_template_clear' : '保留原有内容', 'lang_template_select' : '选择模板' }, 'blank':"空白文档", 'blog':"博客文章", 'resume':"个人简历", 'richText':"图文混排", 'sciPapers':"科技论文" }, 'scrawl':{ 'static':{ 'lang_input_previousStep':"上一步", 'lang_input_nextsStep':"下一步", 'lang_input_clear':'清空', 'lang_input_addPic':'添加背景', 'lang_input_ScalePic':'缩放背景', 'lang_input_removePic':'删除背景', 'J_imgTxt':{title:'添加背景图片'} }, 'noScarwl':"尚未作画,白纸一张~", 'scrawlUpLoading':"涂鸦上传中,别急哦~", 'continueBtn':"继续", 'imageError':"糟糕,图片读取失败了!", 'backgroundUploading':'背景图片上传中,别急哦~' }, 'music':{ 'static':{ 'lang_input_tips':"输入歌手/歌曲/专辑,搜索您感兴趣的音乐!", 'J_searchBtn':{value:'搜索歌曲'} }, 'emptyTxt':'未搜索到相关音乐结果,请换一个关键词试试。', 'chapter':'歌曲', 'singer':'歌手', 'special':'专辑', 'listenTest':'试听' }, 'anchor':{ 'static':{ 'lang_input_anchorName':'锚点名字:' } }, 'charts':{ 'static':{ 'lang_data_source':'数据源:', 'lang_chart_format': '图表格式:', 'lang_data_align': '数据对齐方式', 'lang_chart_align_same': '数据源与图表X轴Y轴一致', 'lang_chart_align_reverse': '数据源与图表X轴Y轴相反', 'lang_chart_title': '图表标题', 'lang_chart_main_title': '主标题:', 'lang_chart_sub_title': '子标题:', 'lang_chart_x_title': 'X轴标题:', 'lang_chart_y_title': 'Y轴标题:', 'lang_chart_tip': '提示文字', 'lang_cahrt_tip_prefix': '提示文字前缀:', 'lang_cahrt_tip_description': '仅饼图有效, 当鼠标移动到饼图中相应的块上时,提示框内的文字的前缀', 'lang_chart_data_unit': '数据单位', 'lang_chart_data_unit_title': '单位:', 'lang_chart_data_unit_description': '显示在每个数据点上的数据的单位, 比如: 温度的单位 ℃', 'lang_chart_type': '图表类型:', 'lang_prev_btn': '上一个', 'lang_next_btn': '下一个' } }, 'emotion':{ 'static':{ 'lang_input_choice':'精选', 'lang_input_Tuzki':'兔斯基', 'lang_input_BOBO':'BOBO', 'lang_input_lvdouwa':'绿豆蛙', 'lang_input_babyCat':'baby猫', 'lang_input_bubble':'泡泡', 'lang_input_youa':'有啊' } }, 'gmap':{ 'static':{ 'lang_input_address':'地址', 'lang_input_search':'搜索', 'address':{value:"北京"} }, searchError:'无法定位到该地址!' }, 'help':{ 'static':{ 'lang_input_about':'关于UEditor', 'lang_input_shortcuts':'快捷键', 'lang_input_introduction':'UEditor是由百度web前端研发部开发的所见即所得富文本web编辑器,具有轻量,可定制,注重用户体验等特点。开源基于BSD协议,允许自由使用和修改代码。', 'lang_Txt_shortcuts':'快捷键', 'lang_Txt_func':'功能', 'lang_Txt_bold':'给选中字设置为加粗', 'lang_Txt_copy':'复制选中内容', 'lang_Txt_cut':'剪切选中内容', 'lang_Txt_Paste':'粘贴', 'lang_Txt_undo':'重新执行上次操作', 'lang_Txt_redo':'撤销上一次操作', 'lang_Txt_italic':'给选中字设置为斜体', 'lang_Txt_underline':'给选中字加下划线', 'lang_Txt_selectAll':'全部选中', 'lang_Txt_visualEnter':'软回车', 'lang_Txt_fullscreen':'全屏' } }, 'insertframe':{ 'static':{ 'lang_input_address':'地址:', 'lang_input_width':'宽度:', 'lang_input_height':'高度:', 'lang_input_isScroll':'允许滚动条:', 'lang_input_frameborder':'显示框架边框:', 'lang_input_alignMode':'对齐方式:', 'align':{title:"对齐方式", options:["默认", "左对齐", "右对齐", "居中"]} }, 'enterAddress':'请输入地址!' }, 'link':{ 'static':{ 'lang_input_text':'文本内容:', 'lang_input_url':'链接地址:', 'lang_input_title':'标题:', 'lang_input_target':'是否在新窗口打开:' }, 'validLink':'只支持选中一个链接时生效', 'httpPrompt':'您输入的超链接中不包含http等协议名称,默认将为您添加http://前缀' }, 'map':{ 'static':{ lang_city:"城市", lang_address:"地址", city:{value:"北京"}, lang_search:"搜索", lang_dynamicmap:"插入动态地图" }, cityMsg:"请选择城市", errorMsg:"抱歉,找不到该位置!" }, 'searchreplace':{ 'static':{ lang_tab_search:"查找", lang_tab_replace:"替换", lang_search1:"查找", lang_search2:"查找", lang_replace:"替换", lang_searchReg:'支持正则表达式,添加前后斜杠标示为正则表达式,例如“/表达式/”', lang_searchReg1:'支持正则表达式,添加前后斜杠标示为正则表达式,例如“/表达式/”', lang_case_sensitive1:"区分大小写", lang_case_sensitive2:"区分大小写", nextFindBtn:{value:"下一个"}, preFindBtn:{value:"上一个"}, nextReplaceBtn:{value:"下一个"}, preReplaceBtn:{value:"上一个"}, repalceBtn:{value:"替换"}, repalceAllBtn:{value:"全部替换"} }, getEnd:"已经搜索到文章末尾!", getStart:"已经搜索到文章头部", countMsg:"总共替换了{#count}处!" }, 'snapscreen':{ 'static':{ lang_showMsg:"截图功能需要首先安装UEditor截图插件! ", lang_download:"点此下载", lang_step1:"第一步,下载UEditor截图插件并运行安装。", lang_step2:"第二步,插件安装完成后即可使用,如不生效,请重启浏览器后再试!" } }, 'spechars':{ 'static':{}, tsfh:"特殊字符", lmsz:"罗马字符", szfh:"数学字符", rwfh:"日文字符", xlzm:"希腊字母", ewzm:"俄文字符", pyzm:"拼音字母", yyyb:"英语音标", zyzf:"其他" }, 'edittable':{ 'static':{ 'lang_tableStyle':'表格样式', 'lang_insertCaption':'添加表格名称行', 'lang_insertTitle':'添加表格标题行', 'lang_insertTitleCol':'添加表格标题列', 'lang_orderbycontent':"使表格内容可排序", 'lang_tableSize':'自动调整表格尺寸', 'lang_autoSizeContent':'按表格文字自适应', 'lang_autoSizePage':'按页面宽度自适应', 'lang_example':'示例', 'lang_borderStyle':'表格边框', 'lang_color':'颜色:' }, captionName:'表格名称', titleName:'标题', cellsName:'内容', errorMsg:'有合并单元格,不可排序' }, 'edittip':{ 'static':{ lang_delRow:'删除整行', lang_delCol:'删除整列' } }, 'edittd':{ 'static':{ lang_tdBkColor:'背景颜色:' } }, 'formula':{ 'static':{ } }, 'wordimage':{ 'static':{ lang_resave:"转存步骤", uploadBtn:{src:"upload.png",alt:"上传"}, clipboard:{style:"background: url(copy.png) -153px -1px no-repeat;"}, lang_step:"1、点击顶部复制按钮,将地址复制到剪贴板;2、点击添加照片按钮,在弹出的对话框中使用Ctrl+V粘贴地址;3、点击打开后选择图片上传流程。" }, 'fileType':"图片", 'flashError':"FLASH初始化失败,请检查FLASH插件是否正确安装!", 'netError':"网络连接错误,请重试!", 'copySuccess':"图片地址已经复制!", 'flashI18n':{} //留空默认中文 }, 'autosave': { 'saving':'保存中...', 'success':'本地保存成功' } }; ================================================ FILE: yshop-drink-vue3/public/UEditor22/php/Uploader.class.php ================================================ "临时文件错误", "ERROR_TMP_FILE_NOT_FOUND" => "找不到临时文件", "ERROR_SIZE_EXCEED" => "文件大小超出网站限制", "ERROR_TYPE_NOT_ALLOWED" => "文件类型不允许", "ERROR_CREATE_DIR" => "目录创建失败", "ERROR_DIR_NOT_WRITEABLE" => "目录没有写权限", "ERROR_FILE_MOVE" => "文件保存时出错", "ERROR_FILE_NOT_FOUND" => "找不到上传文件", "ERROR_WRITE_CONTENT" => "写入文件内容错误", "ERROR_UNKNOWN" => "未知错误", "ERROR_DEAD_LINK" => "链接不可用", "ERROR_HTTP_LINK" => "链接不是http链接", "ERROR_HTTP_CONTENTTYPE" => "链接contentType不正确", "INVALID_URL" => "非法 URL", "INVALID_IP" => "非法 IP" ); /** * 构造函数 * @param string $fileField 表单名称 * @param array $config 配置项 * @param bool $base64 是否解析base64编码,可省略。若开启,则$fileField代表的是base64编码的字符串表单名 */ public function __construct($fileField, $config, $type = "upload") { $this->fileField = $fileField; $this->config = $config; $this->type = $type; if ($type == "remote") { $this->saveRemote(); } else if($type == "base64") { $this->upBase64(); } else { $this->upFile(); } $this->stateMap['ERROR_TYPE_NOT_ALLOWED'] = iconv('unicode', 'utf-8', $this->stateMap['ERROR_TYPE_NOT_ALLOWED']); } /** * 上传文件的主处理方法 * @return mixed */ private function upFile() { $file = $this->file = $_FILES[$this->fileField]; if (!$file) { $this->stateInfo = $this->getStateInfo("ERROR_FILE_NOT_FOUND"); return; } if ($this->file['error']) { $this->stateInfo = $this->getStateInfo($file['error']); return; } else if (!file_exists($file['tmp_name'])) { $this->stateInfo = $this->getStateInfo("ERROR_TMP_FILE_NOT_FOUND"); return; } else if (!is_uploaded_file($file['tmp_name'])) { $this->stateInfo = $this->getStateInfo("ERROR_TMPFILE"); return; } $this->oriName = $file['name']; $this->fileSize = $file['size']; $this->fileType = $this->getFileExt(); $this->fullName = $this->getFullName(); $this->filePath = $this->getFilePath(); $this->fileName = $this->getFileName(); $dirname = dirname($this->filePath); //检查文件大小是否超出限制 if (!$this->checkSize()) { $this->stateInfo = $this->getStateInfo("ERROR_SIZE_EXCEED"); return; } //检查是否不允许的文件格式 if (!$this->checkType()) { $this->stateInfo = $this->getStateInfo("ERROR_TYPE_NOT_ALLOWED"); return; } //创建目录失败 if (!file_exists($dirname) && !mkdir($dirname, 0777, true)) { $this->stateInfo = $this->getStateInfo("ERROR_CREATE_DIR"); return; } else if (!is_writeable($dirname)) { $this->stateInfo = $this->getStateInfo("ERROR_DIR_NOT_WRITEABLE"); return; } //移动文件 if (!(move_uploaded_file($file["tmp_name"], $this->filePath) && file_exists($this->filePath))) { //移动失败 $this->stateInfo = $this->getStateInfo("ERROR_FILE_MOVE"); } else { //移动成功 $this->stateInfo = $this->stateMap[0]; } } /** * 处理base64编码的图片上传 * @return mixed */ private function upBase64() { $base64Data = $_POST[$this->fileField]; $img = base64_decode($base64Data); $this->oriName = $this->config['oriName']; $this->fileSize = strlen($img); $this->fileType = $this->getFileExt(); $this->fullName = $this->getFullName(); $this->filePath = $this->getFilePath(); $this->fileName = $this->getFileName(); $dirname = dirname($this->filePath); //检查文件大小是否超出限制 if (!$this->checkSize()) { $this->stateInfo = $this->getStateInfo("ERROR_SIZE_EXCEED"); return; } //创建目录失败 if (!file_exists($dirname) && !mkdir($dirname, 0777, true)) { $this->stateInfo = $this->getStateInfo("ERROR_CREATE_DIR"); return; } else if (!is_writeable($dirname)) { $this->stateInfo = $this->getStateInfo("ERROR_DIR_NOT_WRITEABLE"); return; } //移动文件 if (!(file_put_contents($this->filePath, $img) && file_exists($this->filePath))) { //移动失败 $this->stateInfo = $this->getStateInfo("ERROR_WRITE_CONTENT"); } else { //移动成功 $this->stateInfo = $this->stateMap[0]; } } /** * 拉取远程图片 * @return mixed */ private function saveRemote() { $imgUrl = htmlspecialchars($this->fileField); $imgUrl = str_replace("&", "&", $imgUrl); //http开头验证 if (strpos($imgUrl, "http") !== 0) { $this->stateInfo = $this->getStateInfo("ERROR_HTTP_LINK"); return; } preg_match('/(^https*:\/\/[^:\/]+)/', $imgUrl, $matches); $host_with_protocol = count($matches) > 1 ? $matches[1] : ''; // 判断是否是合法 url if (!filter_var($host_with_protocol, FILTER_VALIDATE_URL)) { $this->stateInfo = $this->getStateInfo("INVALID_URL"); return; } preg_match('/^https*:\/\/(.+)/', $host_with_protocol, $matches); $host_without_protocol = count($matches) > 1 ? $matches[1] : ''; // 此时提取出来的可能是 ip 也有可能是域名,先获取 ip $ip = gethostbyname($host_without_protocol); // 判断是否是私有 ip if(!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE)) { $this->stateInfo = $this->getStateInfo("INVALID_IP"); return; } //获取请求头并检测死链 $heads = get_headers($imgUrl, 1); if (!(stristr($heads[0], "200") && stristr($heads[0], "OK"))) { $this->stateInfo = $this->getStateInfo("ERROR_DEAD_LINK"); return; } //格式验证(扩展名验证和Content-Type验证) $fileType = strtolower(strrchr($imgUrl, '.')); if (!in_array($fileType, $this->config['allowFiles']) || !isset($heads['Content-Type']) || !stristr($heads['Content-Type'], "image")) { $this->stateInfo = $this->getStateInfo("ERROR_HTTP_CONTENTTYPE"); return; } //打开输出缓冲区并获取远程图片 ob_start(); $context = stream_context_create( array('http' => array( 'follow_location' => false // don't follow redirects )) ); readfile($imgUrl, false, $context); $img = ob_get_contents(); ob_end_clean(); preg_match("/[\/]([^\/]*)[\.]?[^\.\/]*$/", $imgUrl, $m); $this->oriName = $m ? $m[1]:""; $this->fileSize = strlen($img); $this->fileType = $this->getFileExt(); $this->fullName = $this->getFullName(); $this->filePath = $this->getFilePath(); $this->fileName = $this->getFileName(); $dirname = dirname($this->filePath); //检查文件大小是否超出限制 if (!$this->checkSize()) { $this->stateInfo = $this->getStateInfo("ERROR_SIZE_EXCEED"); return; } //创建目录失败 if (!file_exists($dirname) && !mkdir($dirname, 0777, true)) { $this->stateInfo = $this->getStateInfo("ERROR_CREATE_DIR"); return; } else if (!is_writeable($dirname)) { $this->stateInfo = $this->getStateInfo("ERROR_DIR_NOT_WRITEABLE"); return; } //移动文件 if (!(file_put_contents($this->filePath, $img) && file_exists($this->filePath))) { //移动失败 $this->stateInfo = $this->getStateInfo("ERROR_WRITE_CONTENT"); } else { //移动成功 $this->stateInfo = $this->stateMap[0]; } } /** * 上传错误检查 * @param $errCode * @return string */ private function getStateInfo($errCode) { return !$this->stateMap[$errCode] ? $this->stateMap["ERROR_UNKNOWN"] : $this->stateMap[$errCode]; } /** * 获取文件扩展名 * @return string */ private function getFileExt() { return strtolower(strrchr($this->oriName, '.')); } /** * 重命名文件 * @return string */ private function getFullName() { //替换日期事件 $t = time(); $d = explode('-', date("Y-y-m-d-H-i-s")); $format = $this->config["pathFormat"]; $format = str_replace("{yyyy}", $d[0], $format); $format = str_replace("{yy}", $d[1], $format); $format = str_replace("{mm}", $d[2], $format); $format = str_replace("{dd}", $d[3], $format); $format = str_replace("{hh}", $d[4], $format); $format = str_replace("{ii}", $d[5], $format); $format = str_replace("{ss}", $d[6], $format); $format = str_replace("{time}", $t, $format); //过滤文件名的非法自负,并替换文件名 $oriName = substr($this->oriName, 0, strrpos($this->oriName, '.')); $oriName = preg_replace("/[\|\?\"\<\>\/\*\\\\]+/", '', $oriName); $format = str_replace("{filename}", $oriName, $format); //替换随机字符串 $randNum = rand(1, 10000000000) . rand(1, 10000000000); if (preg_match("/\{rand\:([\d]*)\}/i", $format, $matches)) { $format = preg_replace("/\{rand\:[\d]*\}/i", substr($randNum, 0, $matches[1]), $format); } $ext = $this->getFileExt(); return $format . $ext; } /** * 获取文件名 * @return string */ private function getFileName () { return substr($this->filePath, strrpos($this->filePath, '/') + 1); } /** * 获取文件完整路径 * @return string */ private function getFilePath() { $fullname = $this->fullName; $rootPath = $_SERVER['DOCUMENT_ROOT']; if (substr($fullname, 0, 1) != '/') { $fullname = '/' . $fullname; } return $rootPath . $fullname; } /** * 文件类型检测 * @return bool */ private function checkType() { return in_array($this->getFileExt(), $this->config["allowFiles"]); } /** * 文件大小检测 * @return bool */ private function checkSize() { return $this->fileSize <= ($this->config["maxSize"]); } /** * 获取当前上传成功文件的各项信息 * @return array */ public function getFileInfo() { return array( "state" => $this->stateInfo, "url" => $this->fullName, "title" => $this->fileName, "original" => $this->oriName, "type" => $this->fileType, "size" => $this->fileSize ); } } ================================================ FILE: yshop-drink-vue3/public/UEditor22/php/action_crawler.php ================================================ $CONFIG['catcherPathFormat'], "maxSize" => $CONFIG['catcherMaxSize'], "allowFiles" => $CONFIG['catcherAllowFiles'], "oriName" => "remote.png" ); $fieldName = $CONFIG['catcherFieldName']; /* 抓取远程图片 */ $list = array(); if (isset($_POST[$fieldName])) { $source = $_POST[$fieldName]; } else { $source = $_GET[$fieldName]; } foreach ($source as $imgUrl) { $item = new Uploader($imgUrl, $config, "remote"); $info = $item->getFileInfo(); array_push($list, array( "state" => $info["state"], "url" => $info["url"], "size" => $info["size"], "title" => htmlspecialchars($info["title"]), "original" => htmlspecialchars($info["original"]), "source" => htmlspecialchars($imgUrl) )); } /* 返回抓取数据 */ return json_encode(array( 'state'=> count($list) ? 'SUCCESS':'ERROR', 'list'=> $list )); ================================================ FILE: yshop-drink-vue3/public/UEditor22/php/action_list.php ================================================ "no match file", "list" => array(), "start" => $start, "total" => count($files) )); } /* 获取指定范围的列表 */ $len = count($files); for ($i = min($end, $len) - 1, $list = array(); $i < $len && $i >= 0 && $i >= $start; $i--){ $list[] = $files[$i]; } //倒序 //for ($i = $end, $list = array(); $i < $len && $i < $end; $i++){ // $list[] = $files[$i]; //} /* 返回数据 */ $result = json_encode(array( "state" => "SUCCESS", "list" => $list, "start" => $start, "total" => count($files) )); return $result; /** * 遍历获取目录下的指定类型的文件 * @param $path * @param array $files * @return array */ function getfiles($path, $allowFiles, &$files = array()) { if (!is_dir($path)) return null; if(substr($path, strlen($path) - 1) != '/') $path .= '/'; $handle = opendir($path); while (false !== ($file = readdir($handle))) { if ($file != '.' && $file != '..') { $path2 = $path . $file; if (is_dir($path2)) { getfiles($path2, $allowFiles, $files); } else { if (preg_match("/\.(".$allowFiles.")$/i", $file)) { $files[] = array( 'url'=> substr($path2, strlen($_SERVER['DOCUMENT_ROOT'])), 'mtime'=> filemtime($path2) ); } } } } return $files; } ================================================ FILE: yshop-drink-vue3/public/UEditor22/php/action_upload.php ================================================ $CONFIG['imagePathFormat'], "maxSize" => $CONFIG['imageMaxSize'], "allowFiles" => $CONFIG['imageAllowFiles'] ); $fieldName = $CONFIG['imageFieldName']; break; case 'uploadscrawl': $config = array( "pathFormat" => $CONFIG['scrawlPathFormat'], "maxSize" => $CONFIG['scrawlMaxSize'], "allowFiles" => $CONFIG['scrawlAllowFiles'], "oriName" => "scrawl.png" ); $fieldName = $CONFIG['scrawlFieldName']; $base64 = "base64"; break; case 'uploadvideo': $config = array( "pathFormat" => $CONFIG['videoPathFormat'], "maxSize" => $CONFIG['videoMaxSize'], "allowFiles" => $CONFIG['videoAllowFiles'] ); $fieldName = $CONFIG['videoFieldName']; break; case 'uploadfile': default: $config = array( "pathFormat" => $CONFIG['filePathFormat'], "maxSize" => $CONFIG['fileMaxSize'], "allowFiles" => $CONFIG['fileAllowFiles'] ); $fieldName = $CONFIG['fileFieldName']; break; } /* 生成上传实例对象并完成上传 */ $up = new Uploader($fieldName, $config, $base64); /** * 得到上传文件所对应的各个参数,数组结构 * array( * "state" => "", //上传状态,上传成功时必须返回"SUCCESS" * "url" => "", //返回的地址 * "title" => "", //新文件名 * "original" => "", //原始文件名 * "type" => "" //文件类型 * "size" => "", //文件大小 * ) */ /* 返回数据 */ return json_encode($up->getFileInfo()); ================================================ FILE: yshop-drink-vue3/public/UEditor22/php/config.json ================================================ /* 前后端通信相关的配置,注释只允许使用多行方式 */ { /* 上传图片配置项 */ "imageActionName": "uploadimage", /* 执行上传图片的action名称 */ "imageFieldName": "upfile", /* 提交的图片表单名称 */ "imageMaxSize": 2048000, /* 上传大小限制,单位B */ "imageAllowFiles": [".png", ".jpg", ".jpeg", ".gif", ".bmp"], /* 上传图片格式显示 */ "imageCompressEnable": true, /* 是否压缩图片,默认是true */ "imageCompressBorder": 1600, /* 图片压缩最长边限制 */ "imageInsertAlign": "none", /* 插入的图片浮动方式 */ "imageUrlPrefix": "", /* 图片访问路径前缀 */ "imagePathFormat": "/ueditor/php/upload/image/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */ /* {filename} 会替换成原文件名,配置这项需要注意中文乱码问题 */ /* {rand:6} 会替换成随机数,后面的数字是随机数的位数 */ /* {time} 会替换成时间戳 */ /* {yyyy} 会替换成四位年份 */ /* {yy} 会替换成两位年份 */ /* {mm} 会替换成两位月份 */ /* {dd} 会替换成两位日期 */ /* {hh} 会替换成两位小时 */ /* {ii} 会替换成两位分钟 */ /* {ss} 会替换成两位秒 */ /* 非法字符 \ : * ? " < > | */ /* 具请体看线上文档: fex.baidu.com/ueditor/#use-format_upload_filename */ /* 涂鸦图片上传配置项 */ "scrawlActionName": "uploadscrawl", /* 执行上传涂鸦的action名称 */ "scrawlFieldName": "upfile", /* 提交的图片表单名称 */ "scrawlPathFormat": "/ueditor/php/upload/image/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */ "scrawlMaxSize": 2048000, /* 上传大小限制,单位B */ "scrawlUrlPrefix": "", /* 图片访问路径前缀 */ "scrawlInsertAlign": "none", /* 截图工具上传 */ "snapscreenActionName": "uploadimage", /* 执行上传截图的action名称 */ "snapscreenPathFormat": "/ueditor/php/upload/image/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */ "snapscreenUrlPrefix": "", /* 图片访问路径前缀 */ "snapscreenInsertAlign": "none", /* 插入的图片浮动方式 */ /* 抓取远程图片配置 */ "catcherLocalDomain": ["127.0.0.1", "localhost", "img.baidu.com"], "catcherActionName": "catchimage", /* 执行抓取远程图片的action名称 */ "catcherFieldName": "source", /* 提交的图片列表表单名称 */ "catcherPathFormat": "/ueditor/php/upload/image/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */ "catcherUrlPrefix": "", /* 图片访问路径前缀 */ "catcherMaxSize": 2048000, /* 上传大小限制,单位B */ "catcherAllowFiles": [".png", ".jpg", ".jpeg", ".gif", ".bmp"], /* 抓取图片格式显示 */ /* 上传视频配置 */ "videoActionName": "uploadvideo", /* 执行上传视频的action名称 */ "videoFieldName": "upfile", /* 提交的视频表单名称 */ "videoPathFormat": "/ueditor/php/upload/video/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */ "videoUrlPrefix": "", /* 视频访问路径前缀 */ "videoMaxSize": 102400000, /* 上传大小限制,单位B,默认100MB */ "videoAllowFiles": [ ".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg", ".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid"], /* 上传视频格式显示 */ /* 上传文件配置 */ "fileActionName": "uploadfile", /* controller里,执行上传视频的action名称 */ "fileFieldName": "upfile", /* 提交的文件表单名称 */ "filePathFormat": "/ueditor/php/upload/file/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */ "fileUrlPrefix": "", /* 文件访问路径前缀 */ "fileMaxSize": 51200000, /* 上传大小限制,单位B,默认50MB */ "fileAllowFiles": [ ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg", ".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid", ".rar", ".zip", ".tar", ".gz", ".7z", ".bz2", ".cab", ".iso", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".txt", ".md", ".xml" ], /* 上传文件格式显示 */ /* 列出指定目录下的图片 */ "imageManagerActionName": "listimage", /* 执行图片管理的action名称 */ "imageManagerListPath": "/ueditor/php/upload/image/", /* 指定要列出图片的目录 */ "imageManagerListSize": 20, /* 每次列出文件数量 */ "imageManagerUrlPrefix": "", /* 图片访问路径前缀 */ "imageManagerInsertAlign": "none", /* 插入的图片浮动方式 */ "imageManagerAllowFiles": [".png", ".jpg", ".jpeg", ".gif", ".bmp"], /* 列出的文件类型 */ /* 列出指定目录下的文件 */ "fileManagerActionName": "listfile", /* 执行文件管理的action名称 */ "fileManagerListPath": "/ueditor/php/upload/file/", /* 指定要列出文件的目录 */ "fileManagerUrlPrefix": "", /* 文件访问路径前缀 */ "fileManagerListSize": 20, /* 每次列出文件数量 */ "fileManagerAllowFiles": [ ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg", ".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid", ".rar", ".zip", ".tar", ".gz", ".7z", ".bz2", ".cab", ".iso", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".txt", ".md", ".xml" ] /* 列出的文件类型 */ } ================================================ FILE: yshop-drink-vue3/public/UEditor22/php/controller.php ================================================ '请求地址出错' )); break; } /* 输出结果 */ if (isset($_GET["callback"])) { if (preg_match("/^[\w_]+$/", $_GET["callback"])) { echo htmlspecialchars($_GET["callback"]) . '(' . $result . ')'; } else { echo json_encode(array( 'state'=> 'callback参数不合法' )); } } else { echo $result; } ================================================ FILE: yshop-drink-vue3/public/UEditor22/themes/default/css/ueditor.css ================================================ /*基础UI构建 */ /* common layer */ .edui-default .edui-box { border: none; padding: 0; margin: 0; overflow: hidden; } .edui-default a.edui-box { display: block; text-decoration: none; color: black; } .edui-default a.edui-box:hover { text-decoration: none; } .edui-default a.edui-box:active { text-decoration: none; } .edui-default table.edui-box { border-collapse: collapse; } .edui-default ul.edui-box { list-style-type: none; } div.edui-box { position: relative; display: -moz-inline-box !important; display: inline-block !important; vertical-align: top; } .edui-default .edui-clearfix { zoom: 1 } .edui-default .edui-clearfix:after { content: '\20'; display: block; clear: both; } * html div.edui-box { display: inline !important; } *:first-child+html div.edui-box { display: inline !important; } /* control layout */ .edui-default .edui-button-body, .edui-splitbutton-body, .edui-menubutton-body, .edui-combox-body { position: relative; } .edui-default .edui-popup { position: absolute; -webkit-user-select: none; -moz-user-select: none; } .edui-default .edui-popup .edui-shadow { position: absolute; z-index: -1; } .edui-default .edui-popup .edui-bordereraser { position: absolute; overflow: hidden; } .edui-default .edui-tablepicker .edui-canvas { position: relative; } .edui-default .edui-tablepicker .edui-canvas .edui-overlay { position: absolute; } .edui-default .edui-dialog-modalmask, .edui-dialog-dragmask { position: absolute; left: 0; top: 0; width: 100%; height: 100%; } .edui-default .edui-toolbar { position: relative; } /* * default theme */ .edui-default .edui-label { cursor: default; } .edui-default span.edui-clickable { color: blue; cursor: pointer; text-decoration: underline; } .edui-default span.edui-unclickable { color: gray; cursor: default; } /* 工具栏 */ .edui-default .edui-toolbar { cursor: default; -webkit-user-select: none; -moz-user-select: none; padding: 1px; overflow: hidden; /*全屏下单独一行不占位*/ zoom: 1; width:auto; height:auto; } .edui-default .edui-toolbar .edui-button, .edui-default .edui-toolbar .edui-splitbutton, .edui-default .edui-toolbar .edui-menubutton, .edui-default .edui-toolbar .edui-combox { margin: 1px; } /*UI工具栏、编辑区域、底部*/ .edui-default .edui-editor { border: 1px solid #d4d4d4; background-color: white; position: relative; overflow: visible; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; } .edui-editor div{ width:auto; height:auto; } .edui-default .edui-editor-toolbarbox { position: relative; zoom: 1; -webkit-box-shadow:0 1px 4px rgba(204, 204, 204, 0.6); -moz-box-shadow:0 1px 4px rgba(204, 204, 204, 0.6); box-shadow:0 1px 4px rgba(204, 204, 204, 0.6); border-top-left-radius:2px; border-top-right-radius:2px; } .edui-default .edui-editor-toolbarboxouter { border-bottom: 1px solid #d4d4d4; background-color: #fafafa; background-image: -moz-linear-gradient(top, #ffffff, #f2f2f2); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f2f2f2)); background-image: -webkit-linear-gradient(top, #ffffff, #f2f2f2); background-image: -o-linear-gradient(top, #ffffff, #f2f2f2); background-image: linear-gradient(to bottom, #ffffff, #f2f2f2); background-repeat: repeat-x; /*border: 1px solid #d4d4d4;*/ -webkit-border-radius: 4px 4px 0 0; -moz-border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0); *zoom: 1; -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); } .edui-default .edui-editor-toolbarboxinner { padding: 2px; } .edui-default .edui-editor-iframeholder { position: relative; /*for fix ie6 toolbarmsg under iframe bug. relative -> static */ /*_position: static !important;* } .edui-default .edui-editor-iframeholder textarea { font-family: consolas, "Courier New", "lucida console", monospace; font-size: 12px; line-height: 18px; } .edui-default .edui-editor-bottombar { /*border-top: 1px solid #ccc;*/ /*height: 20px;*/ /*width: 40%;*/ /*float: left;*/ /*overflow: hidden;*/ } .edui-default .edui-editor-bottomContainer { overflow: hidden; } .edui-default .edui-editor-bottomContainer table { width: 100%; height: 0; overflow: hidden; border-spacing: 0; } .edui-default .edui-editor-bottomContainer td { white-space: nowrap; border-top: 1px solid #ccc; line-height: 20px; font-size: 12px; font-family: Arial, Helvetica, Tahoma, Verdana, Sans-Serif; } .edui-default .edui-editor-wordcount { text-align: right; margin-right: 5px; color: #aaa; } .edui-default .edui-editor-scale { width: 12px; } .edui-default .edui-editor-scale .edui-editor-icon { float: right; width: 100%; height: 12px; margin-top: 10px; background: url(../images/scale.png) no-repeat; cursor: se-resize; } .edui-default .edui-editor-breadcrumb { margin: 2px 0 0 3px; } .edui-default .edui-editor-breadcrumb span { cursor: pointer; text-decoration: underline; color: blue; } .edui-default .edui-toolbar .edui-for-fullscreen { float: right; } .edui-default .edui-bubble .edui-popup-content { border: 1px solid #DCAC6C; background-color: #fff6d9; padding: 5px; font-size: 10pt; font-family: "宋体"; } .edui-default .edui-bubble .edui-shadow { /*box-shadow: 1px 1px 3px #818181;*/ /*-webkit-box-shadow: 2px 2px 3px #818181;*/ /*-moz-box-shadow: 2px 2px 3px #818181;*/ /*filter: progid:DXImageTransform.Microsoft.Blur(PixelRadius = '2', MakeShadow = 'true', ShadowOpacity = '0.5');*/ } .edui-default .edui-editor-toolbarmsg { background-color: #FFF6D9; border-bottom: 1px solid #ccc; position: absolute; bottom: -25px; left: 0; z-index: 1009; width: 99.9%; } .edui-default .edui-editor-toolbarmsg-upload { font-size: 14px; color: blue; width: 100px; height: 16px; line-height: 16px; cursor: pointer; position: absolute; top: 5px; left: 350px; } .edui-default .edui-editor-toolbarmsg-label { font-size: 12px; line-height: 16px; padding: 4px; } .edui-default .edui-editor-toolbarmsg-close { float: right; width: 20px; height: 16px; line-height: 16px; cursor: pointer; color: red; } /*可选中菜单按钮*/ .edui-default .edui-list .edui-bordereraser { display: none; } .edui-default .edui-listitem { padding: 1px; white-space: nowrap; } .edui-default .edui-list .edui-state-hover { position: relative; background-color: #fff5d4; border: 1px solid #dcac6c; padding: 0; } .edui-default .edui-for-fontfamily .edui-listitem-label { min-width: 130px; _width: 120px; font-size: 12px; height: 22px; line-height: 22px; padding-left: 5px; } .edui-default .edui-for-insertcode .edui-listitem-label { min-width: 120px; _width: 120px; font-size: 12px; height: 22px; line-height: 22px; padding-left: 5px; } .edui-default .edui-for-underline .edui-listitem-label { min-width: 120px; _width: 120px; padding: 3px 5px; font-size: 12px; } .edui-default .edui-for-fontsize .edui-listitem-label { min-width: 120px; _width: 120px; padding: 3px 5px; } .edui-default .edui-for-paragraph .edui-listitem-label { min-width: 200px; _width: 200px; padding: 2px 5px; } .edui-default .edui-for-rowspacingtop .edui-listitem-label, .edui-default .edui-for-rowspacingbottom .edui-listitem-label { min-width: 53px; _width: 53px; padding: 2px 5px; } .edui-default .edui-for-lineheight .edui-listitem-label { min-width: 53px; _width: 53px; padding: 2px 5px; } .edui-default .edui-for-customstyle .edui-listitem-label { min-width: 200px; _width: 200px; width: 200px !important; padding: 2px 5px; } /* 可选中按钮弹出菜单*/ .edui-default .edui-menu { z-index: 3000; } .edui-default .edui-menu .edui-popup-content { padding: 3px; } .edui-default .edui-menu-body { _width: 150px; min-width: 170px; background: url("../images/sparator_v.png") repeat-y 25px; } .edui-default .edui-menuitem-body { } .edui-default .edui-menuitem { height: 20px; cursor: default; vertical-align: top; } .edui-default .edui-menuitem .edui-icon { width: 20px !important; height: 20px !important; background: url(../images/icons.png) 0 -4000px; background: url(../images/icons.gif) 0 -4000px\9; } .edui-default .edui-menuitem .edui-label { font-size: 12px; line-height: 20px; height: 20px; padding-left: 10px; } .edui-default .edui-state-checked .edui-menuitem-body { background: url("../images/icons-all.gif") no-repeat 6px -205px; } .edui-default .edui-state-disabled .edui-menuitem-label { color: gray; } /*不可选中菜单按钮 */ .edui-default .edui-toolbar .edui-combox-body .edui-button-body { width: 60px; font-size: 12px; height: 20px; line-height: 20px; padding-left: 5px; white-space: nowrap; margin: 0 3px 0 0; } .edui-default .edui-toolbar .edui-combox-body .edui-arrow { background: url(../images/icons.png) -741px 0; _background: url(../images/icons.gif) -741px 0; height: 20px; width: 9px; } .edui-default .edui-toolbar .edui-combox .edui-combox-body { border: 1px solid #CCC; background-color: white; border-radius: 2px; -webkit-border-radius: 2px; -moz-border-radius: 2px; } .edui-default .edui-toolbar .edui-combox-body .edui-splitborder { display: none; } .edui-default .edui-toolbar .edui-combox-body .edui-arrow { border-left: 1px solid #CCC; } .edui-default .edui-toolbar .edui-state-hover .edui-combox-body { background-color: #fff5d4; border: 1px solid #dcac6c; } .edui-default .edui-toolbar .edui-state-hover .edui-combox-body .edui-arrow { border-left: 1px solid #dcac6c; } .edui-default .edui-toolbar .edui-state-checked .edui-combox-body { background-color: #FFE69F; border: 1px solid #DCAC6C; } .edui-toolbar .edui-state-checked .edui-combox-body .edui-arrow { border-left: 1px solid #DCAC6C; } .edui-toolbar .edui-state-disabled .edui-combox-body { background-color: #F0F0EE; opacity: 0.3; filter: alpha(opacity = 30); } .edui-toolbar .edui-state-opened .edui-combox-body { background-color: white; border: 1px solid gray; } /*普通按钮样式及状态*/ .edui-default .edui-toolbar .edui-button .edui-icon, .edui-default .edui-toolbar .edui-menubutton .edui-icon, .edui-default .edui-toolbar .edui-splitbutton .edui-icon { height: 20px !important; width: 20px !important; background-image: url(../images/icons.png); background-image: url(../images/icons.gif) \9; } .edui-default .edui-toolbar .edui-button .edui-button-wrap { padding: 1px; position: relative; } .edui-default .edui-toolbar .edui-button .edui-state-hover .edui-button-wrap { background-color: #fff5d4; padding: 0; border: 1px solid #dcac6c; } .edui-default .edui-toolbar .edui-button .edui-state-checked .edui-button-wrap { background-color: #ffe69f; padding: 0; border: 1px solid #dcac6c; border-radius: 2px; -webkit-border-radius: 2px; -moz-border-radius: 2px; } .edui-default .edui-toolbar .edui-button .edui-state-active .edui-button-wrap { background-color: #ffffff; padding: 0; border: 1px solid gray; } .edui-default .edui-toolbar .edui-state-disabled .edui-label { color: #ccc; } .edui-default .edui-toolbar .edui-state-disabled .edui-icon { opacity: 0.3; filter: alpha(opacity = 30); } /* toolbar icons */ .edui-default .edui-for-undo .edui-icon { background-position: -160px 0; } .edui-default .edui-for-redo .edui-icon { background-position: -100px 0; } .edui-default .edui-for-bold .edui-icon { background-position: 0 0; } .edui-default .edui-for-italic .edui-icon { background-position: -60px 0; } .edui-default .edui-for-fontborder .edui-icon { background-position:-160px -40px; } .edui-default .edui-for-underline .edui-icon { background-position: -140px 0; } .edui-default .edui-for-strikethrough .edui-icon { background-position: -120px 0; } .edui-default .edui-for-subscript .edui-icon { background-position: -600px 0; } .edui-default .edui-for-superscript .edui-icon { background-position: -620px 0; } .edui-default .edui-for-blockquote .edui-icon { background-position: -220px 0; } .edui-default .edui-for-forecolor .edui-icon { background-position: -720px 0; } .edui-default .edui-for-backcolor .edui-icon { background-position: -760px 0; } .edui-default .edui-for-inserttable .edui-icon { background-position: -580px -20px; } .edui-default .edui-for-autotypeset .edui-icon { background-position: -640px -40px; } .edui-default .edui-for-justifyleft .edui-icon { background-position: -460px 0; } .edui-default .edui-for-justifycenter .edui-icon { background-position: -420px 0; } .edui-default .edui-for-justifyright .edui-icon { background-position: -480px 0; } .edui-default .edui-for-justifyjustify .edui-icon { background-position: -440px 0; } .edui-default .edui-for-insertorderedlist .edui-icon { background-position: -80px 0; } .edui-default .edui-for-insertunorderedlist .edui-icon { background-position: -20px 0; } .edui-default .edui-for-lineheight .edui-icon { background-position: -725px -40px; } .edui-default .edui-for-rowspacingbottom .edui-icon { background-position: -745px -40px; } .edui-default .edui-for-rowspacingtop .edui-icon { background-position: -765px -40px; } .edui-default .edui-for-horizontal .edui-icon { background-position: -360px 0; } .edui-default .edui-for-link .edui-icon { background-position: -500px 0; } .edui-default .edui-for-code .edui-icon { background-position: -440px -40px; } .edui-default .edui-for-insertimage .edui-icon { background-position: -726px -77px; } .edui-default .edui-for-insertframe .edui-icon { background-position: -240px -40px; } .edui-default .edui-for-emoticon .edui-icon { background-position: -60px -20px; } .edui-default .edui-for-spechars .edui-icon { background-position: -240px 0; } .edui-default .edui-for-help .edui-icon { background-position: -340px 0; } .edui-default .edui-for-print .edui-icon { background-position: -440px -20px; } .edui-default .edui-for-preview .edui-icon { background-position: -420px -20px; } .edui-default .edui-for-selectall .edui-icon { background-position: -400px -20px; } .edui-default .edui-for-searchreplace .edui-icon { background-position: -520px -20px; } .edui-default .edui-for-map .edui-icon { background-position: -40px -40px; } .edui-default .edui-for-gmap .edui-icon { background-position: -260px -40px; } .edui-default .edui-for-insertvideo .edui-icon { background-position: -320px -20px; } .edui-default .edui-for-time .edui-icon { background-position: -160px -20px; } .edui-default .edui-for-date .edui-icon { background-position: -140px -20px; } .edui-default .edui-for-cut .edui-icon { background-position: -680px 0; } .edui-default .edui-for-copy .edui-icon { background-position: -700px 0; } .edui-default .edui-for-paste .edui-icon { background-position: -560px 0; } .edui-default .edui-for-formatmatch .edui-icon { background-position: -40px 0; } .edui-default .edui-for-pasteplain .edui-icon { background-position: -360px -20px; } .edui-default .edui-for-directionalityltr .edui-icon { background-position: -20px -20px; } .edui-default .edui-for-directionalityrtl .edui-icon { background-position: -40px -20px; } .edui-default .edui-for-source .edui-icon { background-position: -261px -0px; } .edui-default .edui-for-removeformat .edui-icon { background-position: -580px 0; } .edui-default .edui-for-unlink .edui-icon { background-position: -640px 0; } .edui-default .edui-for-touppercase .edui-icon { background-position: -786px 0; } .edui-default .edui-for-tolowercase .edui-icon { background-position: -806px 0; } .edui-default .edui-for-insertrow .edui-icon { background-position: -478px -76px; } .edui-default .edui-for-insertrownext .edui-icon { background-position: -498px -76px; } .edui-default .edui-for-insertcol .edui-icon { background-position: -455px -76px; } .edui-default .edui-for-insertcolnext .edui-icon { background-position: -429px -76px; } .edui-default .edui-for-mergeright .edui-icon { background-position: -60px -40px; } .edui-default .edui-for-mergedown .edui-icon { background-position: -80px -40px; } .edui-default .edui-for-splittorows .edui-icon { background-position: -100px -40px; } .edui-default .edui-for-splittocols .edui-icon { background-position: -120px -40px; } .edui-default .edui-for-insertparagraphbeforetable .edui-icon { background-position: -140px -40px; } .edui-default .edui-for-deleterow .edui-icon { background-position: -660px -20px; } .edui-default .edui-for-deletecol .edui-icon { background-position: -640px -20px; } .edui-default .edui-for-splittocells .edui-icon { background-position: -800px -20px; } .edui-default .edui-for-mergecells .edui-icon { background-position: -760px -20px; } .edui-default .edui-for-deletetable .edui-icon { background-position: -620px -20px; } .edui-default .edui-for-cleardoc .edui-icon { background-position: -520px 0; } .edui-default .edui-for-fullscreen .edui-icon { background-position: -100px -20px; } .edui-default .edui-for-anchor .edui-icon { background-position: -200px 0; } .edui-default .edui-for-pagebreak .edui-icon { background-position: -460px -40px; } .edui-default .edui-for-imagenone .edui-icon { background-position: -480px -40px; } .edui-default .edui-for-imageleft .edui-icon { background-position: -500px -40px; } .edui-default .edui-for-wordimage .edui-icon { background-position: -660px -40px; } .edui-default .edui-for-imageright .edui-icon { background-position: -520px -40px; } .edui-default .edui-for-imagecenter .edui-icon { background-position: -540px -40px; } .edui-default .edui-for-indent .edui-icon { background-position: -400px 0; } .edui-default .edui-for-outdent .edui-icon { background-position: -540px 0; } .edui-default .edui-for-webapp .edui-icon { background-position: -601px -40px } .edui-default .edui-for-table .edui-icon { background-position: -580px -20px; } .edui-default .edui-for-edittable .edui-icon { background-position: -420px -40px; } .edui-default .edui-for-template .edui-icon { background-position: -339px -40px; } .edui-default .edui-for-delete .edui-icon { background-position: -360px -40px; } .edui-default .edui-for-attachment .edui-icon { background-position: -620px -40px; } .edui-default .edui-for-edittd .edui-icon { background-position: -700px -40px; } .edui-default .edui-for-snapscreen .edui-icon { background-position: -581px -40px } .edui-default .edui-for-scrawl .edui-icon { background-position: -801px -41px } .edui-default .edui-for-background .edui-icon { background-position: -680px -40px; } .edui-default .edui-for-music .edui-icon { background-position: -18px -40px } .edui-default .edui-for-formula .edui-icon { background-position: -200px -40px } .edui-default .edui-for-aligntd .edui-icon { background-position: -236px -76px; } .edui-default .edui-for-insertparagraphtrue .edui-icon { background-position: -625px -76px; } .edui-default .edui-for-insertparagraph .edui-icon { background-position: -602px -76px; } .edui-default .edui-for-insertcaption .edui-icon { background-position: -336px -76px; } .edui-default .edui-for-deletecaption .edui-icon { background-position: -362px -76px; } .edui-default .edui-for-inserttitle .edui-icon { background-position: -286px -76px; } .edui-default .edui-for-deletetitle .edui-icon { background-position: -311px -76px; } .edui-default .edui-for-aligntable .edui-icon { background-position: -440px 0; } .edui-default .edui-for-tablealignment-left .edui-icon { background-position: -460px 0; } .edui-default .edui-for-tablealignment-center .edui-icon { background-position: -420px 0; } .edui-default .edui-for-tablealignment-right .edui-icon { background-position: -480px 0; } .edui-default .edui-for-drafts .edui-icon { background-position: -560px 0; } .edui-default .edui-for-charts .edui-icon { background: url( ../images/charts.png ) no-repeat 2px 3px!important; } .edui-default .edui-for-inserttitlecol .edui-icon { background-position: -673px -76px; } .edui-default .edui-for-deletetitlecol .edui-icon { background-position: -698px -76px; } .edui-default .edui-for-simpleupload .edui-icon { background-position: -380px 0px; } /*splitbutton*/ .edui-default .edui-toolbar .edui-splitbutton-body .edui-arrow, .edui-default .edui-toolbar .edui-menubutton-body .edui-arrow { background: url(../images/icons.png) -741px 0; _background: url(../images/icons.gif) -741px 0; height: 20px; width: 9px; } .edui-default .edui-toolbar .edui-splitbutton .edui-splitbutton-body, .edui-default .edui-toolbar .edui-menubutton .edui-menubutton-body { padding: 1px; } .edui-default .edui-toolbar .edui-splitborder { width: 1px; height: 20px; } .edui-default .edui-toolbar .edui-state-hover .edui-splitborder { width: 1px; border-left: 0px solid #dcac6c; } .edui-default .edui-toolbar .edui-state-active .edui-splitborder { width: 0; border-left: 1px solid gray; } .edui-default .edui-toolbar .edui-state-opened .edui-splitborder { width: 1px; border: 0; } .edui-default .edui-toolbar .edui-splitbutton .edui-state-hover .edui-splitbutton-body, .edui-default .edui-toolbar .edui-menubutton .edui-state-hover .edui-menubutton-body { background-color: #fff5d4; border: 1px solid #dcac6c; padding: 0; } .edui-default .edui-toolbar .edui-splitbutton .edui-state-checked .edui-splitbutton-body, .edui-default .edui-toolbar .edui-menubutton .edui-state-checked .edui-menubutton-body { background-color: #FFE69F; border: 1px solid #DCAC6C; padding: 0; } .edui-default .edui-toolbar .edui-splitbutton .edui-state-active .edui-splitbutton-body, .edui-default .edui-toolbar .edui-menubutton .edui-state-active .edui-menubutton-body { background-color: #ffffff; border: 1px solid gray; padding: 0; } .edui-default .edui-state-disabled .edui-arrow { opacity: 0.3; _filter: alpha(opacity = 30); } .edui-default .edui-toolbar .edui-splitbutton .edui-state-opened .edui-splitbutton-body, .edui-default .edui-toolbar .edui-menubutton .edui-state-opened .edui-menubutton-body { background-color: white; border: 1px solid gray; padding: 0; } .edui-default .edui-for-insertorderedlist .edui-bordereraser, .edui-default .edui-for-lineheight .edui-bordereraser, .edui-default .edui-for-rowspacingtop .edui-bordereraser, .edui-default .edui-for-rowspacingbottom .edui-bordereraser, .edui-default .edui-for-insertunorderedlist .edui-bordereraser { background-color: white; } /* 解决嵌套导致的图标问题 */ .edui-default .edui-for-insertorderedlist .edui-popup-body .edui-icon, .edui-default .edui-for-lineheight .edui-popup-body .edui-icon, .edui-default .edui-for-rowspacingtop .edui-popup-body .edui-icon, .edui-default .edui-for-rowspacingbottom .edui-popup-body .edui-icon, .edui-default .edui-for-insertunorderedlist .edui-popup-body .edui-icon { /*background-position: 0 -40px;*/ background-image: none ; } /* 弹出菜单 */ .edui-default .edui-popup { z-index: 3000; background-color: #ffffff; width:auto; height:auto; } .edui-default .edui-popup .edui-shadow { left: 0; top: 0; width: 100%; height: 100%; } .edui-default .edui-popup-content { border:1px solid #ccc; border: 1px solid rgba(0, 0, 0, 0.2); *border-right-width: 2px; *border-bottom-width: 2px; -webkit-border-radius: 6px; -moz-border-radius: 6px; border-radius: 6px; -webkit-box-shadow: 0 3px 4px rgba(0, 0, 0, 0.2); -moz-box-shadow: 0 3px 4px rgba(0, 0, 0, 0.2); box-shadow: 0 3px 4px rgba(0, 0, 0, 0.2); -webkit-background-clip: padding-box; -moz-background-clip: padding; background-clip: padding-box; padding: 5px; background:#ffffff; } .edui-default .edui-popup .edui-bordereraser { background-color: white; height: 3px; } .edui-default .edui-menu .edui-bordereraser { height: 3px; } .edui-default .edui-anchor-topleft .edui-bordereraser { left: 1px; top: -2px; } .edui-default .edui-anchor-topright .edui-bordereraser { right: 1px; top: -2px; } .edui-default .edui-anchor-bottomleft .edui-bordereraser { left: 0; bottom: -6px; height: 7px; border-left: 1px solid gray; border-right: 1px solid gray; } .edui-default .edui-anchor-bottomright .edui-bordereraser { right: 0; bottom: -6px; height: 7px; border-left: 1px solid gray; border-right: 1px solid gray; } .edui-popup div{ width:auto; height:auto; } .edui-default .edui-editor-messageholder { display: block; width: 150px; height: auto; border: 0; margin: 0; padding: 0; position: absolute; top: 28px; right: 3px; } .edui-default .edui-message{ min-height: 10px; text-shadow: 0 1px 0 rgba(255,255,255,0.5); padding: 0; margin-bottom: 3px; position: relative; } .edui-default .edui-message-body{ border-radius: 3px; padding: 8px 15px 8px 8px; color: #c09853; background-color: #fcf8e3; border: 1px solid #fbeed5; } .edui-default .edui-message-type-info{ color: #3a87ad; background-color: #d9edf7; border-color: #bce8f1 } .edui-default .edui-message-type-success{ color: #468847; background-color: #dff0d8; border-color: #d6e9c6 } .edui-default .edui-message-type-danger, .edui-default .edui-message-type-error{ color: #b94a48; background-color: #f2dede; border-color: #eed3d7 } .edui-default .edui-message .edui-message-closer { display: block; width: 16px; height: 16px; line-height: 16px; position: absolute; top: 0; right: 0; padding: 0; cursor: pointer; background: transparent; border: 0; float: right; font-size: 20px; font-weight: bold; color: #999; text-shadow: 0 1px 0 #fff; font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; } .edui-default .edui-message .edui-message-content { font-size: 10pt; word-wrap: break-word; word-break: normal; } /* 弹出对话框按钮和对话框大小 */ .edui-default .edui-dialog { z-index: 2000; position: absolute; } .edui-dialog div{ width:auto; } .edui-default .edui-dialog-wrap { margin-right: 6px; margin-bottom: 6px; } .edui-default .edui-dialog-fullscreen-flag { margin-right: 0; margin-bottom: 0; } .edui-default .edui-dialog-body { position: relative; padding:2px 0 0 2px; _zoom: 1; } .edui-default .edui-dialog-fullscreen-flag .edui-dialog-body { padding: 0; } .edui-default .edui-dialog-shadow { position: absolute; z-index: -1; left: 0; top: 0; width: 100%; height: 100%; background-color: #ffffff; border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, 0.2); *border-right-width: 2px; *border-bottom-width: 2px; -webkit-border-radius: 6px; -moz-border-radius: 6px; border-radius: 6px; -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); -webkit-background-clip: padding-box; -moz-background-clip: padding; background-clip: padding-box; } .edui-default .edui-dialog-foot { background-color: white; } .edui-default .edui-dialog-titlebar { height: 26px; border-bottom: 1px solid #c6c6c6; background: url(../images/dialog-title-bg.png) repeat-x bottom; position: relative; cursor: move; } .edui-default .edui-dialog-caption { font-weight: bold; font-size: 12px; line-height: 26px; padding-left: 5px; } .edui-default .edui-dialog-draghandle { height: 26px; } .edui-default .edui-dialog-closebutton { position: absolute !important; right: 5px; top: 3px; } .edui-default .edui-dialog-closebutton .edui-button-body { height: 20px; width: 20px; cursor: pointer; background: url("../images/icons-all.gif") no-repeat 0 -59px; } .edui-default .edui-dialog-closebutton .edui-state-hover .edui-button-body { background: url("../images/icons-all.gif") no-repeat 0 -89px; } .edui-default .edui-dialog-foot { height: 40px; } .edui-default .edui-dialog-buttons { position: absolute; right: 0; } .edui-default .edui-dialog-buttons .edui-button { margin-right: 10px; } .edui-default .edui-dialog-buttons .edui-button .edui-button-body { background: url("../images/icons-all.gif") no-repeat; height: 24px; width: 96px; font-size: 12px; line-height: 24px; text-align: center; cursor: default; } .edui-default .edui-dialog-buttons .edui-button .edui-state-hover .edui-button-body { background: url("../images/icons-all.gif") no-repeat 0 -30px; } .edui-default .edui-dialog iframe { border: 0; padding: 0; margin: 0; vertical-align: top; } .edui-default .edui-dialog-modalmask { opacity: 0.3; filter: alpha(opacity = 30); background-color: #ccc; position: absolute; /*z-index: 1999;*/ } .edui-default .edui-dialog-dragmask { position: absolute; /*z-index: 2001;*/ background-color: transparent; cursor: move; } .edui-default .edui-dialog-content { position: relative; } .edui-default .dialogcontmask { cursor: move; visibility: hidden; display: block; position: absolute; width: 100%; height: 100%; opacity: 0; filter: alpha(opacity = 0); } /*link-dialog*/ .edui-default .edui-for-link .edui-dialog-content { width: 420px; height: 200px; overflow: hidden; } /*background-dialog*/ .edui-default .edui-for-background .edui-dialog-content { width: 440px; height: 280px; overflow: hidden; } /*template-dialog*/ .edui-default .edui-for-template .edui-dialog-content { width: 630px; height: 390px; overflow: hidden; } /*scrawl-dialog*/ .edui-default .edui-for-scrawl .edui-dialog-content { width: 515px; *width: 506px; height: 360px; } /*spechars-dialog*/ .edui-default .edui-for-spechars .edui-dialog-content { width: 620px; height: 500px; *width: 630px; *height: 570px; } /*image-dialog*/ .edui-default .edui-for-insertimage .edui-dialog-content { width: 650px; height: 400px; overflow: hidden; } /*webapp-dialog*/ .edui-default .edui-for-webapp .edui-dialog-content { width: 560px; _width: 565px; height: 450px; overflow: hidden; } /*image-insertframe*/ .edui-default .edui-for-insertframe .edui-dialog-content { width: 350px; height: 200px; overflow: hidden; } /*wordImage-dialog*/ .edui-default .edui-for-wordimage .edui-dialog-content { width: 620px; height: 380px; overflow: hidden; } /*attachment-dialog*/ .edui-default .edui-for-attachment .edui-dialog-content { width: 650px; height: 400px; overflow: hidden; } /*map-dialog*/ .edui-default .edui-for-map .edui-dialog-content { width: 550px; height: 400px; } /*gmap-dialog*/ .edui-default .edui-for-gmap .edui-dialog-content { width: 550px; height: 400px; } /*video-dialog*/ .edui-default .edui-for-insertvideo .edui-dialog-content { width: 590px; height: 390px; } /*anchor-dialog*/ .edui-default .edui-for-anchor .edui-dialog-content { width: 320px; height: 60px; overflow: hidden; } /*searchreplace-dialog*/ .edui-default .edui-for-searchreplace .edui-dialog-content { width: 400px; height: 220px; } /*help-dialog*/ .edui-default .edui-for-help .edui-dialog-content { width: 400px; height: 420px; } /*edittable-dialog*/ .edui-default .edui-for-edittable .edui-dialog-content { width: 540px; _width:590px; height: 335px; } /*edittip-dialog*/ .edui-default .edui-for-edittip .edui-dialog-content { width: 225px; height: 60px; } /*edittd-dialog*/ .edui-default .edui-for-edittd .edui-dialog-content { width: 240px; height: 50px; } /*snapscreen-dialog*/ .edui-default .edui-for-snapscreen .edui-dialog-content { width: 400px; height: 220px; } /*music-dialog*/ .edui-default .edui-for-music .edui-dialog-content { width: 515px; height: 360px; } /*段落弹出菜单*/ .edui-default .edui-for-paragraph .edui-listitem-label { font-family: Tahoma, Verdana, Arial, Helvetica; } .edui-default .edui-for-paragraph .edui-listitem-label .edui-for-p { font-size: 22px; line-height: 27px; } .edui-default .edui-for-paragraph .edui-listitem-label .edui-for-h1 { font-weight: bolder; font-size: 32px; line-height: 36px; } .edui-default .edui-for-paragraph .edui-listitem-label .edui-for-h2 { font-weight: bolder; font-size: 27px; line-height: 29px; } .edui-default .edui-for-paragraph .edui-listitem-label .edui-for-h3 { font-weight: bolder; font-size: 19px; line-height: 23px; } .edui-default .edui-for-paragraph .edui-listitem-label .edui-for-h4 { font-weight: bolder; font-size: 16px; line-height: 19px } .edui-default .edui-for-paragraph .edui-listitem-label .edui-for-h5 { font-weight: bolder; font-size: 13px; line-height: 16px; } .edui-default .edui-for-paragraph .edui-listitem-label .edui-for-h6 { font-weight: bolder; font-size: 12px; line-height: 14px; } /* 表格弹出菜单 */ .edui-default .edui-for-inserttable .edui-splitborder { display: none } .edui-default .edui-for-inserttable .edui-splitbutton-body .edui-arrow { width: 0 } .edui-default .edui-toolbar .edui-for-inserttable .edui-state-active .edui-splitborder{ border-left: 1px solid transparent; } .edui-default .edui-tablepicker .edui-infoarea { height: 14px; line-height: 14px; font-size: 12px; width: 220px; margin-bottom: 3px; clear: both; } .edui-default .edui-tablepicker .edui-infoarea .edui-label { float: left; } .edui-default .edui-dialog-buttons .edui-label { line-height: 24px; } .edui-default .edui-tablepicker .edui-infoarea .edui-clickable { float: right; } .edui-default .edui-tablepicker .edui-pickarea { background: url("../images/unhighlighted.gif") repeat; height: 220px; width: 220px; } .edui-default .edui-tablepicker .edui-pickarea .edui-overlay { background: url("../images/highlighted.gif") repeat; } /* 颜色弹出菜单 */ .edui-default .edui-colorpicker-topbar { height: 27px; width: 200px; /*border-bottom: 1px gray dashed;*/ } .edui-default .edui-colorpicker-preview { height: 20px; border: 1px inset black; margin-left: 1px; width: 128px; float: left; } .edui-default .edui-colorpicker-nocolor { float: right; margin-right: 1px; font-size: 12px; line-height: 14px; height: 14px; border: 1px solid #333; padding: 3px 5px; cursor: pointer; } .edui-default .edui-colorpicker-tablefirstrow { height: 30px; } .edui-default .edui-colorpicker-colorcell { width: 14px; height: 14px; display: block; margin: 0; cursor: pointer; } .edui-default .edui-colorpicker-colorcell:hover { width: 14px; height: 14px; margin: 0; } .edui-default .edui-colorpicker-advbtn{ display: block; text-align: center; cursor: pointer; height:20px; } .arrow_down{ background: white url('../images/arrow_down.png') no-repeat center; } .arrow_up{ background: white url('../images/arrow_up.png') no-repeat center; } /*高级的样式*/ .edui-colorpicker-adv{ position: relative; overflow: hidden; height: 180px; display: none; } .edui-colorpicker-plant, .edui-colorpicker-hue { border: solid 1px #666; } .edui-colorpicker-pad { width: 150px; height: 150px; left: 14px; top: 13px; position: absolute; background: red; overflow: hidden; cursor: crosshair; } .edui-colorpicker-cover{ position: absolute; top: 0; left: 0; width: 150px; height: 150px; background: url("../images/tangram-colorpicker.png") -160px -200px; } .edui-colorpicker-padDot{ position: absolute; top: 0; left: 0; width: 11px; height: 11px; overflow: hidden; background: url(../images/tangram-colorpicker.png) 0px -200px repeat-x; z-index: 1000; } .edui-colorpicker-sliderMain { position: absolute; left: 171px; top: 13px; width: 19px; height: 152px; background: url(../images/tangram-colorpicker.png) -179px -12px no-repeat; } .edui-colorpicker-slider { width: 100%; height: 100%; cursor: pointer; } .edui-colorpicker-thumb{ position: absolute; top: 0; cursor: pointer; height: 3px; left: -1px; right: -1px; border: 1px solid black; background: white; opacity: .8; } /*自动排版弹出菜单*/ .edui-default .edui-autotypesetpicker .edui-autotypesetpicker-body { font-size: 12px; margin-bottom: 3px; clear: both; } .edui-default .edui-autotypesetpicker-body table { border-collapse: separate; border-spacing: 2px; } .edui-default .edui-autotypesetpicker-body td { font-size: 12px; word-wrap:break-word; } .edui-default .edui-autotypesetpicker-body td input { margin: 3px 3px 3px 4px; *margin: 1px 0 0 0; } /*自动排版弹出菜单*/ .edui-default .edui-cellalignpicker .edui-cellalignpicker-body { width: 70px; font-size: 12px; cursor: default; } .edui-default .edui-cellalignpicker-body table { border-collapse: separate; border-spacing: 0; } .edui-default .edui-cellalignpicker-body td{ padding: 1px; } .edui-default .edui-cellalignpicker-body .edui-icon{ height: 20px; width: 20px; padding: 1px; background-image: url(../images/table-cell-align.png); } .edui-default .edui-cellalignpicker-body .edui-left{ background-position: 0 0; } .edui-default .edui-cellalignpicker-body .edui-center{ background-position: -25px 0; } .edui-default .edui-cellalignpicker-body .edui-right{ background-position: -51px 0; } .edui-default .edui-cellalignpicker-body td.edui-state-hover .edui-left{ background-position: -73px 0; } .edui-default .edui-cellalignpicker-body td.edui-state-hover .edui-center{ background-position: -98px 0; } .edui-default .edui-cellalignpicker-body td.edui-state-hover .edui-right{ background-position: -124px 0; } .edui-default .edui-cellalignpicker-body td.edui-cellalign-selected .edui-left { background-position: -146px 0; background-color: #f1f4f5; } .edui-default .edui-cellalignpicker-body td.edui-cellalign-selected .edui-center { background-position: -245px 0; } .edui-default .edui-cellalignpicker-body td.edui-cellalign-selected .edui-right { background-position: -271px 0; } /*分隔线*/ .edui-default .edui-toolbar .edui-separator { width: 2px; height: 20px; margin: 2px 4px 2px 3px; background: url(../images/icons.png) -181px 0; background: url(../images/icons.gif) -181px 0 \9; } /*颜色按钮 */ .edui-default .edui-toolbar .edui-colorbutton .edui-colorlump { position: absolute; overflow: hidden; bottom: 1px; left: 1px; width: 18px; height: 4px; } /*表情按钮及弹出菜单*/ /*去除了表情的下拉箭头*/ .edui-default .edui-for-emotion .edui-icon { background-position: -60px -20px; } .edui-default .edui-for-emotion .edui-popup-content iframe { width: 514px; height: 380px; overflow: hidden; } .edui-default .edui-for-emotion .edui-popup-content { position: relative; z-index: 555 } .edui-default .edui-for-emotion .edui-splitborder { display: none } .edui-default .edui-for-emotion .edui-splitbutton-body .edui-arrow { width: 0 } .edui-default .edui-toolbar .edui-for-emotion .edui-state-active .edui-splitborder { border-left: 1px solid transparent; } /*contextmenu*/ .edui-default .edui-hassubmenu .edui-arrow { height: 20px; width: 20px; float: right; background: url("../images/icons-all.gif") no-repeat 10px -233px; } .edui-default .edui-menu-body .edui-menuitem { padding: 1px; } .edui-default .edui-menuseparator { margin: 2px 0; height: 1px; overflow: hidden; } .edui-default .edui-menuseparator-inner { border-bottom: 1px solid #e2e3e3; margin-left: 29px; margin-right: 1px; } .edui-default .edui-menu-body .edui-state-hover { padding: 0 !important; background-color: #fff5d4; border: 1px solid #dcac6c; } /*弹出菜单*/ .edui-default .edui-shortcutmenu { padding: 2px; width: 190px; height: 50px; background-color: #fff; border: 1px solid #ccc; border-radius: 5px; } /*粘贴弹出菜单*/ .edui-default .edui-wordpastepop .edui-popup-content{ border: none; padding: 0; width: 54px; height: 21px; } .edui-default .edui-pasteicon { width: 100%; height: 100%; background-image: url('../images/wordpaste.png'); background-position: 0 0; } .edui-default .edui-pasteicon.edui-state-opened { background-position: 0 -34px; } .edui-default .edui-pastecontainer { position: relative; visibility: hidden; width: 97px; background: #fff; border: 1px solid #ccc; } .edui-default .edui-pastecontainer .edui-title { font-weight: bold; background: #F8F8FF; height: 25px; line-height: 25px; font-size: 12px; padding-left: 5px; } .edui-default .edui-pastecontainer .edui-button { overflow: hidden; margin: 3px 0; } .edui-default .edui-pastecontainer .edui-button .edui-richtxticon, .edui-default .edui-pastecontainer .edui-button .edui-tagicon, .edui-default .edui-pastecontainer .edui-button .edui-plaintxticon{ float: left; cursor: pointer; width: 29px; height: 29px; margin-left: 5px; background-image: url('../images/wordpaste.png'); background-repeat: no-repeat; } .edui-default .edui-pastecontainer .edui-button .edui-richtxticon { margin-left: 0; background-position: -109px 0; } .edui-default .edui-pastecontainer .edui-button .edui-tagicon { background-position: -148px 1px; } .edui-default .edui-pastecontainer .edui-button .edui-plaintxticon { background-position: -72px 0; } .edui-default .edui-pastecontainer .edui-button .edui-state-hover .edui-richtxticon { background-position: -109px -34px; } .edui-default .edui-pastecontainer .edui-button .edui-state-hover .edui-tagicon{ background-position: -148px -34px; } .edui-default .edui-pastecontainer .edui-button .edui-state-hover .edui-plaintxticon{ background-position: -72px -34px; } ================================================ FILE: yshop-drink-vue3/public/UEditor22/themes/default/dialogbase.css ================================================ /*弹出对话框页面样式组件 */ /*reset */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; outline: 0; font-size: 100%; } body { line-height: 1; } ol, ul { list-style: none; } blockquote, q { quotes: none; } ins { text-decoration: none; } del { text-decoration: line-through; } table { border-collapse: collapse; border-spacing: 0; } /*module */ body { background-color: #fff; font: 12px/1.5 sans-serif, "宋体", "Arial Narrow", HELVETICA; color: #646464; } /*tab*/ .tabhead { position: relative; z-index: 10; } .tabhead span { display: inline-block; padding: 0 5px; height: 30px; border: 1px solid #ccc; background: url("images/dialog-title-bg.png") repeat-x; text-align: center; line-height: 30px; cursor: pointer; *margin-right: 5px; } .tabhead span.focus { height: 31px; border-bottom: none; background: #fff; } .tabbody { position: relative; top: -1px; margin: 0 auto; border: 1px solid #ccc; } /*button*/ a.button { display: block; text-align: center; line-height: 24px; text-decoration: none; height: 24px; width: 95px; border: 0; color: #838383; background: url(../../themes/default/images/icons-all.gif) no-repeat; } a.button:hover { background-position: 0 -30px; } ================================================ FILE: yshop-drink-vue3/public/UEditor22/themes/iframe.css ================================================ /*可以在这里添加你自己的css*/ ================================================ FILE: yshop-drink-vue3/public/UEditor22/third-party/SyntaxHighlighter/shCore.js ================================================ // XRegExp 1.5.1 // (c) 2007-2012 Steven Levithan // MIT License // // Provides an augmented, extensible, cross-browser implementation of regular expressions, // including support for additional syntax, flags, and methods var XRegExp; if (XRegExp) { // Avoid running twice, since that would break references to native globals throw Error("can't load XRegExp twice in the same frame"); } // Run within an anonymous function to protect variables and avoid new globals (function (undefined) { //--------------------------------- // Constructor //--------------------------------- // Accepts a pattern and flags; returns a new, extended `RegExp` object. Differs from a native // regular expression in that additional syntax and flags are supported and cross-browser // syntax inconsistencies are ameliorated. `XRegExp(/regex/)` clones an existing regex and // converts to type XRegExp XRegExp = function (pattern, flags) { var output = [], currScope = XRegExp.OUTSIDE_CLASS, pos = 0, context, tokenResult, match, chr, regex; if (XRegExp.isRegExp(pattern)) { if (flags !== undefined) throw TypeError("can't supply flags when constructing one RegExp from another"); return clone(pattern); } // Tokens become part of the regex construction process, so protect against infinite // recursion when an XRegExp is constructed within a token handler or trigger if (isInsideConstructor) throw Error("can't call the XRegExp constructor within token definition functions"); flags = flags || ""; context = { // `this` object for custom tokens hasNamedCapture: false, captureNames: [], hasFlag: function (flag) {return flags.indexOf(flag) > -1;}, setFlag: function (flag) {flags += flag;} }; while (pos < pattern.length) { // Check for custom tokens at the current position tokenResult = runTokens(pattern, pos, currScope, context); if (tokenResult) { output.push(tokenResult.output); pos += (tokenResult.match[0].length || 1); } else { // Check for native multicharacter metasequences (excluding character classes) at // the current position if (match = nativ.exec.call(nativeTokens[currScope], pattern.slice(pos))) { output.push(match[0]); pos += match[0].length; } else { chr = pattern.charAt(pos); if (chr === "[") currScope = XRegExp.INSIDE_CLASS; else if (chr === "]") currScope = XRegExp.OUTSIDE_CLASS; // Advance position one character output.push(chr); pos++; } } } regex = RegExp(output.join(""), nativ.replace.call(flags, flagClip, "")); regex._xregexp = { source: pattern, captureNames: context.hasNamedCapture ? context.captureNames : null }; return regex; }; //--------------------------------- // Public properties //--------------------------------- XRegExp.version = "1.5.1"; // Token scope bitflags XRegExp.INSIDE_CLASS = 1; XRegExp.OUTSIDE_CLASS = 2; //--------------------------------- // Private variables //--------------------------------- var replacementToken = /\$(?:(\d\d?|[$&`'])|{([$\w]+)})/g, flagClip = /[^gimy]+|([\s\S])(?=[\s\S]*\1)/g, // Nonnative and duplicate flags quantifier = /^(?:[?*+]|{\d+(?:,\d*)?})\??/, isInsideConstructor = false, tokens = [], // Copy native globals for reference ("native" is an ES3 reserved keyword) nativ = { exec: RegExp.prototype.exec, test: RegExp.prototype.test, match: String.prototype.match, replace: String.prototype.replace, split: String.prototype.split }, compliantExecNpcg = nativ.exec.call(/()??/, "")[1] === undefined, // check `exec` handling of nonparticipating capturing groups compliantLastIndexIncrement = function () { var x = /^/g; nativ.test.call(x, ""); return !x.lastIndex; }(), hasNativeY = RegExp.prototype.sticky !== undefined, nativeTokens = {}; // `nativeTokens` match native multicharacter metasequences only (including deprecated octals, // excluding character classes) nativeTokens[XRegExp.INSIDE_CLASS] = /^(?:\\(?:[0-3][0-7]{0,2}|[4-7][0-7]?|x[\dA-Fa-f]{2}|u[\dA-Fa-f]{4}|c[A-Za-z]|[\s\S]))/; nativeTokens[XRegExp.OUTSIDE_CLASS] = /^(?:\\(?:0(?:[0-3][0-7]{0,2}|[4-7][0-7]?)?|[1-9]\d*|x[\dA-Fa-f]{2}|u[\dA-Fa-f]{4}|c[A-Za-z]|[\s\S])|\(\?[:=!]|[?*+]\?|{\d+(?:,\d*)?}\??)/; //--------------------------------- // Public methods //--------------------------------- // Lets you extend or change XRegExp syntax and create custom flags. This is used internally by // the XRegExp library and can be used to create XRegExp plugins. This function is intended for // users with advanced knowledge of JavaScript's regular expression syntax and behavior. It can // be disabled by `XRegExp.freezeTokens` XRegExp.addToken = function (regex, handler, scope, trigger) { tokens.push({ pattern: clone(regex, "g" + (hasNativeY ? "y" : "")), handler: handler, scope: scope || XRegExp.OUTSIDE_CLASS, trigger: trigger || null }); }; // Accepts a pattern and flags; returns an extended `RegExp` object. If the pattern and flag // combination has previously been cached, the cached copy is returned; otherwise the newly // created regex is cached XRegExp.cache = function (pattern, flags) { var key = pattern + "/" + (flags || ""); return XRegExp.cache[key] || (XRegExp.cache[key] = XRegExp(pattern, flags)); }; // Accepts a `RegExp` instance; returns a copy with the `/g` flag set. The copy has a fresh // `lastIndex` (set to zero). If you want to copy a regex without forcing the `global` // property, use `XRegExp(regex)`. Do not use `RegExp(regex)` because it will not preserve // special properties required for named capture XRegExp.copyAsGlobal = function (regex) { return clone(regex, "g"); }; // Accepts a string; returns the string with regex metacharacters escaped. The returned string // can safely be used at any point within a regex to match the provided literal string. Escaped // characters are [ ] { } ( ) * + ? - . , \ ^ $ | # and whitespace XRegExp.escape = function (str) { return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); }; // Accepts a string to search, regex to search with, position to start the search within the // string (default: 0), and an optional Boolean indicating whether matches must start at-or- // after the position or at the specified position only. This function ignores the `lastIndex` // of the provided regex in its own handling, but updates the property for compatibility XRegExp.execAt = function (str, regex, pos, anchored) { var r2 = clone(regex, "g" + ((anchored && hasNativeY) ? "y" : "")), match; r2.lastIndex = pos = pos || 0; match = r2.exec(str); // Run the altered `exec` (required for `lastIndex` fix, etc.) if (anchored && match && match.index !== pos) match = null; if (regex.global) regex.lastIndex = match ? r2.lastIndex : 0; return match; }; // Breaks the unrestorable link to XRegExp's private list of tokens, thereby preventing // syntax and flag changes. Should be run after XRegExp and any plugins are loaded XRegExp.freezeTokens = function () { XRegExp.addToken = function () { throw Error("can't run addToken after freezeTokens"); }; }; // Accepts any value; returns a Boolean indicating whether the argument is a `RegExp` object. // Note that this is also `true` for regex literals and regexes created by the `XRegExp` // constructor. This works correctly for variables created in another frame, when `instanceof` // and `constructor` checks would fail to work as intended XRegExp.isRegExp = function (o) { return Object.prototype.toString.call(o) === "[object RegExp]"; }; // Executes `callback` once per match within `str`. Provides a simpler and cleaner way to // iterate over regex matches compared to the traditional approaches of subverting // `String.prototype.replace` or repeatedly calling `exec` within a `while` loop XRegExp.iterate = function (str, regex, callback, context) { var r2 = clone(regex, "g"), i = -1, match; while (match = r2.exec(str)) { // Run the altered `exec` (required for `lastIndex` fix, etc.) if (regex.global) regex.lastIndex = r2.lastIndex; // Doing this to follow expectations if `lastIndex` is checked within `callback` callback.call(context, match, ++i, str, regex); if (r2.lastIndex === match.index) r2.lastIndex++; } if (regex.global) regex.lastIndex = 0; }; // Accepts a string and an array of regexes; returns the result of using each successive regex // to search within the matches of the previous regex. The array of regexes can also contain // objects with `regex` and `backref` properties, in which case the named or numbered back- // references specified are passed forward to the next regex or returned. E.g.: // var xregexpImgFileNames = XRegExp.matchChain(html, [ // {regex: /]+)>/i, backref: 1}, // tag attributes // {regex: XRegExp('(?ix) \\s src=" (? [^"]+ )'), backref: "src"}, // src attribute values // {regex: XRegExp("^http://xregexp\\.com(/[^#?]+)", "i"), backref: 1}, // xregexp.com paths // /[^\/]+$/ // filenames (strip directory paths) // ]); XRegExp.matchChain = function (str, chain) { return function recurseChain (values, level) { var item = chain[level].regex ? chain[level] : {regex: chain[level]}, regex = clone(item.regex, "g"), matches = [], i; for (i = 0; i < values.length; i++) { XRegExp.iterate(values[i], regex, function (match) { matches.push(item.backref ? (match[item.backref] || "") : match[0]); }); } return ((level === chain.length - 1) || !matches.length) ? matches : recurseChain(matches, level + 1); }([str], 0); }; //--------------------------------- // New RegExp prototype methods //--------------------------------- // Accepts a context object and arguments array; returns the result of calling `exec` with the // first value in the arguments array. the context is ignored but is accepted for congruity // with `Function.prototype.apply` RegExp.prototype.apply = function (context, args) { return this.exec(args[0]); }; // Accepts a context object and string; returns the result of calling `exec` with the provided // string. the context is ignored but is accepted for congruity with `Function.prototype.call` RegExp.prototype.call = function (context, str) { return this.exec(str); }; //--------------------------------- // Overriden native methods //--------------------------------- // Adds named capture support (with backreferences returned as `result.name`), and fixes two // cross-browser issues per ES3: // - Captured values for nonparticipating capturing groups should be returned as `undefined`, // rather than the empty string. // - `lastIndex` should not be incremented after zero-length matches. RegExp.prototype.exec = function (str) { var match, name, r2, origLastIndex; if (!this.global) origLastIndex = this.lastIndex; match = nativ.exec.apply(this, arguments); if (match) { // Fix browsers whose `exec` methods don't consistently return `undefined` for // nonparticipating capturing groups if (!compliantExecNpcg && match.length > 1 && indexOf(match, "") > -1) { r2 = RegExp(this.source, nativ.replace.call(getNativeFlags(this), "g", "")); // Using `str.slice(match.index)` rather than `match[0]` in case lookahead allowed // matching due to characters outside the match nativ.replace.call((str + "").slice(match.index), r2, function () { for (var i = 1; i < arguments.length - 2; i++) { if (arguments[i] === undefined) match[i] = undefined; } }); } // Attach named capture properties if (this._xregexp && this._xregexp.captureNames) { for (var i = 1; i < match.length; i++) { name = this._xregexp.captureNames[i - 1]; if (name) match[name] = match[i]; } } // Fix browsers that increment `lastIndex` after zero-length matches if (!compliantLastIndexIncrement && this.global && !match[0].length && (this.lastIndex > match.index)) this.lastIndex--; } if (!this.global) this.lastIndex = origLastIndex; // Fix IE, Opera bug (last tested IE 9.0.5, Opera 11.61 on Windows) return match; }; // Fix browser bugs in native method RegExp.prototype.test = function (str) { // Use the native `exec` to skip some processing overhead, even though the altered // `exec` would take care of the `lastIndex` fixes var match, origLastIndex; if (!this.global) origLastIndex = this.lastIndex; match = nativ.exec.call(this, str); // Fix browsers that increment `lastIndex` after zero-length matches if (match && !compliantLastIndexIncrement && this.global && !match[0].length && (this.lastIndex > match.index)) this.lastIndex--; if (!this.global) this.lastIndex = origLastIndex; // Fix IE, Opera bug (last tested IE 9.0.5, Opera 11.61 on Windows) return !!match; }; // Adds named capture support and fixes browser bugs in native method String.prototype.match = function (regex) { if (!XRegExp.isRegExp(regex)) regex = RegExp(regex); // Native `RegExp` if (regex.global) { var result = nativ.match.apply(this, arguments); regex.lastIndex = 0; // Fix IE bug return result; } return regex.exec(this); // Run the altered `exec` }; // Adds support for `${n}` tokens for named and numbered backreferences in replacement text, // and provides named backreferences to replacement functions as `arguments[0].name`. Also // fixes cross-browser differences in replacement text syntax when performing a replacement // using a nonregex search value, and the value of replacement regexes' `lastIndex` property // during replacement iterations. Note that this doesn't support SpiderMonkey's proprietary // third (`flags`) parameter String.prototype.replace = function (search, replacement) { var isRegex = XRegExp.isRegExp(search), captureNames, result, str, origLastIndex; // There are too many combinations of search/replacement types/values and browser bugs that // preclude passing to native `replace`, so don't try //if (...) // return nativ.replace.apply(this, arguments); if (isRegex) { if (search._xregexp) captureNames = search._xregexp.captureNames; // Array or `null` if (!search.global) origLastIndex = search.lastIndex; } else { search = search + ""; // Type conversion } if (Object.prototype.toString.call(replacement) === "[object Function]") { result = nativ.replace.call(this + "", search, function () { if (captureNames) { // Change the `arguments[0]` string primitive to a String object which can store properties arguments[0] = new String(arguments[0]); // Store named backreferences on `arguments[0]` for (var i = 0; i < captureNames.length; i++) { if (captureNames[i]) arguments[0][captureNames[i]] = arguments[i + 1]; } } // Update `lastIndex` before calling `replacement` (fix browsers) if (isRegex && search.global) search.lastIndex = arguments[arguments.length - 2] + arguments[0].length; return replacement.apply(null, arguments); }); } else { str = this + ""; // Type conversion, so `args[args.length - 1]` will be a string (given nonstring `this`) result = nativ.replace.call(str, search, function () { var args = arguments; // Keep this function's `arguments` available through closure return nativ.replace.call(replacement + "", replacementToken, function ($0, $1, $2) { // Numbered backreference (without delimiters) or special variable if ($1) { switch ($1) { case "$": return "$"; case "&": return args[0]; case "`": return args[args.length - 1].slice(0, args[args.length - 2]); case "'": return args[args.length - 1].slice(args[args.length - 2] + args[0].length); // Numbered backreference default: // What does "$10" mean? // - Backreference 10, if 10 or more capturing groups exist // - Backreference 1 followed by "0", if 1-9 capturing groups exist // - Otherwise, it's the string "$10" // Also note: // - Backreferences cannot be more than two digits (enforced by `replacementToken`) // - "$01" is equivalent to "$1" if a capturing group exists, otherwise it's the string "$01" // - There is no "$0" token ("$&" is the entire match) var literalNumbers = ""; $1 = +$1; // Type conversion; drop leading zero if (!$1) // `$1` was "0" or "00" return $0; while ($1 > args.length - 3) { literalNumbers = String.prototype.slice.call($1, -1) + literalNumbers; $1 = Math.floor($1 / 10); // Drop the last digit } return ($1 ? args[$1] || "" : "$") + literalNumbers; } // Named backreference or delimited numbered backreference } else { // What does "${n}" mean? // - Backreference to numbered capture n. Two differences from "$n": // - n can be more than two digits // - Backreference 0 is allowed, and is the entire match // - Backreference to named capture n, if it exists and is not a number overridden by numbered capture // - Otherwise, it's the string "${n}" var n = +$2; // Type conversion; drop leading zeros if (n <= args.length - 3) return args[n]; n = captureNames ? indexOf(captureNames, $2) : -1; return n > -1 ? args[n + 1] : $0; } }); }); } if (isRegex) { if (search.global) search.lastIndex = 0; // Fix IE, Safari bug (last tested IE 9.0.5, Safari 5.1.2 on Windows) else search.lastIndex = origLastIndex; // Fix IE, Opera bug (last tested IE 9.0.5, Opera 11.61 on Windows) } return result; }; // A consistent cross-browser, ES3 compliant `split` String.prototype.split = function (s /* separator */, limit) { // If separator `s` is not a regex, use the native `split` if (!XRegExp.isRegExp(s)) return nativ.split.apply(this, arguments); var str = this + "", // Type conversion output = [], lastLastIndex = 0, match, lastLength; // Behavior for `limit`: if it's... // - `undefined`: No limit // - `NaN` or zero: Return an empty array // - A positive number: Use `Math.floor(limit)` // - A negative number: No limit // - Other: Type-convert, then use the above rules if (limit === undefined || +limit < 0) { limit = Infinity; } else { limit = Math.floor(+limit); if (!limit) return []; } // This is required if not `s.global`, and it avoids needing to set `s.lastIndex` to zero // and restore it to its original value when we're done using the regex s = XRegExp.copyAsGlobal(s); while (match = s.exec(str)) { // Run the altered `exec` (required for `lastIndex` fix, etc.) if (s.lastIndex > lastLastIndex) { output.push(str.slice(lastLastIndex, match.index)); if (match.length > 1 && match.index < str.length) Array.prototype.push.apply(output, match.slice(1)); lastLength = match[0].length; lastLastIndex = s.lastIndex; if (output.length >= limit) break; } if (s.lastIndex === match.index) s.lastIndex++; } if (lastLastIndex === str.length) { if (!nativ.test.call(s, "") || lastLength) output.push(""); } else { output.push(str.slice(lastLastIndex)); } return output.length > limit ? output.slice(0, limit) : output; }; //--------------------------------- // Private helper functions //--------------------------------- // Supporting function for `XRegExp`, `XRegExp.copyAsGlobal`, etc. Returns a copy of a `RegExp` // instance with a fresh `lastIndex` (set to zero), preserving properties required for named // capture. Also allows adding new flags in the process of copying the regex function clone (regex, additionalFlags) { if (!XRegExp.isRegExp(regex)) throw TypeError("type RegExp expected"); var x = regex._xregexp; regex = XRegExp(regex.source, getNativeFlags(regex) + (additionalFlags || "")); if (x) { regex._xregexp = { source: x.source, captureNames: x.captureNames ? x.captureNames.slice(0) : null }; } return regex; } function getNativeFlags (regex) { return (regex.global ? "g" : "") + (regex.ignoreCase ? "i" : "") + (regex.multiline ? "m" : "") + (regex.extended ? "x" : "") + // Proposed for ES4; included in AS3 (regex.sticky ? "y" : ""); } function runTokens (pattern, index, scope, context) { var i = tokens.length, result, match, t; // Protect against constructing XRegExps within token handler and trigger functions isInsideConstructor = true; // Must reset `isInsideConstructor`, even if a `trigger` or `handler` throws try { while (i--) { // Run in reverse order t = tokens[i]; if ((scope & t.scope) && (!t.trigger || t.trigger.call(context))) { t.pattern.lastIndex = index; match = t.pattern.exec(pattern); // Running the altered `exec` here allows use of named backreferences, etc. if (match && match.index === index) { result = { output: t.handler.call(context, match, scope), match: match }; break; } } } } catch (err) { throw err; } finally { isInsideConstructor = false; } return result; } function indexOf (array, item, from) { if (Array.prototype.indexOf) // Use the native array method if available return array.indexOf(item, from); for (var i = from || 0; i < array.length; i++) { if (array[i] === item) return i; } return -1; } //--------------------------------- // Built-in tokens //--------------------------------- // Augment XRegExp's regular expression syntax and flags. Note that when adding tokens, the // third (`scope`) argument defaults to `XRegExp.OUTSIDE_CLASS` // Comment pattern: (?# ) XRegExp.addToken( /\(\?#[^)]*\)/, function (match) { // Keep tokens separated unless the following token is a quantifier return nativ.test.call(quantifier, match.input.slice(match.index + match[0].length)) ? "" : "(?:)"; } ); // Capturing group (match the opening parenthesis only). // Required for support of named capturing groups XRegExp.addToken( /\((?!\?)/, function () { this.captureNames.push(null); return "("; } ); // Named capturing group (match the opening delimiter only): (? XRegExp.addToken( /\(\?<([$\w]+)>/, function (match) { this.captureNames.push(match[1]); this.hasNamedCapture = true; return "("; } ); // Named backreference: \k XRegExp.addToken( /\\k<([\w$]+)>/, function (match) { var index = indexOf(this.captureNames, match[1]); // Keep backreferences separate from subsequent literal numbers. Preserve back- // references to named groups that are undefined at this point as literal strings return index > -1 ? "\\" + (index + 1) + (isNaN(match.input.charAt(match.index + match[0].length)) ? "" : "(?:)") : match[0]; } ); // Empty character class: [] or [^] XRegExp.addToken( /\[\^?]/, function (match) { // For cross-browser compatibility with ES3, convert [] to \b\B and [^] to [\s\S]. // (?!) should work like \b\B, but is unreliable in Firefox return match[0] === "[]" ? "\\b\\B" : "[\\s\\S]"; } ); // Mode modifier at the start of the pattern only, with any combination of flags imsx: (?imsx) // Does not support x(?i), (?-i), (?i-m), (?i: ), (?i)(?m), etc. XRegExp.addToken( /^\(\?([imsx]+)\)/, function (match) { this.setFlag(match[1]); return ""; } ); // Whitespace and comments, in free-spacing (aka extended) mode only XRegExp.addToken( /(?:\s+|#.*)+/, function (match) { // Keep tokens separated unless the following token is a quantifier return nativ.test.call(quantifier, match.input.slice(match.index + match[0].length)) ? "" : "(?:)"; }, XRegExp.OUTSIDE_CLASS, function () {return this.hasFlag("x");} ); // Dot, in dotall (aka singleline) mode only XRegExp.addToken( /\./, function () {return "[\\s\\S]";}, XRegExp.OUTSIDE_CLASS, function () {return this.hasFlag("s");} ); //--------------------------------- // Backward compatibility //--------------------------------- // Uncomment the following block for compatibility with XRegExp 1.0-1.2: /* XRegExp.matchWithinChain = XRegExp.matchChain; RegExp.prototype.addFlags = function (s) {return clone(this, s);}; RegExp.prototype.execAll = function (s) {var r = []; XRegExp.iterate(s, this, function (m) {r.push(m);}); return r;}; RegExp.prototype.forEachExec = function (s, f, c) {return XRegExp.iterate(s, this, f, c);}; RegExp.prototype.validate = function (s) {var r = RegExp("^(?:" + this.source + ")$(?!\\s)", getNativeFlags(this)); if (this.global) this.lastIndex = 0; return s.search(r) === 0;}; */ })(); // // Begin anonymous function. This is used to contain local scope variables without polutting global scope. // if (typeof(SyntaxHighlighter) == 'undefined') var SyntaxHighlighter = function() { // CommonJS if (typeof(require) != 'undefined' && typeof(XRegExp) == 'undefined') { XRegExp = require('XRegExp').XRegExp; } // Shortcut object which will be assigned to the SyntaxHighlighter variable. // This is a shorthand for local reference in order to avoid long namespace // references to SyntaxHighlighter.whatever... var sh = { defaults : { /** Additional CSS class names to be added to highlighter elements. */ 'class-name' : '', /** First line number. */ 'first-line' : 1, /** * Pads line numbers. Possible values are: * * false - don't pad line numbers. * true - automaticaly pad numbers with minimum required number of leading zeroes. * [int] - length up to which pad line numbers. */ 'pad-line-numbers' : false, /** Lines to highlight. */ 'highlight' : false, /** Title to be displayed above the code block. */ 'title' : null, /** Enables or disables smart tabs. */ 'smart-tabs' : true, /** Gets or sets tab size. */ 'tab-size' : 4, /** Enables or disables gutter. */ 'gutter' : true, /** Enables or disables toolbar. */ 'toolbar' : true, /** Enables quick code copy and paste from double click. */ 'quick-code' : true, /** Forces code view to be collapsed. */ 'collapse' : false, /** Enables or disables automatic links. */ 'auto-links' : false, /** Gets or sets light mode. Equavalent to turning off gutter and toolbar. */ 'light' : false, 'unindent' : true, 'html-script' : false }, config : { space : ' ', /** Enables use of * * ``` */ findParent:function (node, filterFn, includeSelf) { if (node && !domUtils.isBody(node)) { node = includeSelf ? node : node.parentNode; while (node) { if (!filterFn || filterFn(node) || domUtils.isBody(node)) { return filterFn && !filterFn(node) && domUtils.isBody(node) ? null : node; } node = node.parentNode; } } return null; }, /** * 查找node的节点名为tagName的第一个祖先节点, 查找的起点是node节点的父节点。 * @method findParentByTagName * @param { Node } node 需要查找的节点对象 * @param { Array } tagNames 需要查找的父节点的名称数组 * @warning 查找的终点是到body节点为止 * @return { Node | NULL } 如果找到符合条件的节点, 则返回该节点, 否则返回NULL * @example * ```javascript * var node = UE.dom.domUtils.findParentByTagName( document.getElementsByTagName("div")[0], [ "BODY" ] ); * //output: BODY * console.log( node.tagName ); * ``` */ /** * 查找node的节点名为tagName的祖先节点, 如果includeSelf的值为true,则查找的起点是给定的节点node, * 否则, 起点是node的父节点。 * @method findParentByTagName * @param { Node } node 需要查找的节点对象 * @param { Array } tagNames 需要查找的父节点的名称数组 * @param { Boolean } includeSelf 查找过程是否包含node节点自身 * @warning 查找的终点是到body节点为止 * @return { Node | NULL } 如果找到符合条件的节点, 则返回该节点, 否则返回NULL * @example * ```javascript * var queryTarget = document.getElementsByTagName("div")[0]; * var node = UE.dom.domUtils.findParentByTagName( queryTarget, [ "DIV" ], true ); * //output: true * console.log( queryTarget === node ); * ``` */ findParentByTagName:function (node, tagNames, includeSelf, excludeFn) { tagNames = utils.listToMap(utils.isArray(tagNames) ? tagNames : [tagNames]); return domUtils.findParent(node, function (node) { return tagNames[node.tagName] && !(excludeFn && excludeFn(node)); }, includeSelf); }, /** * 查找节点node的祖先节点集合, 查找的起点是给定节点的父节点,结果集中不包含给定的节点。 * @method findParents * @param { Node } node 需要查找的节点对象 * @return { Array } 给定节点的祖先节点数组 * @grammar UE.dom.domUtils.findParents(node) => Array //返回一个祖先节点数组集合,不包含自身 * @grammar UE.dom.domUtils.findParents(node,includeSelf) => Array //返回一个祖先节点数组集合,includeSelf指定是否包含自身 * @grammar UE.dom.domUtils.findParents(node,includeSelf,filterFn) => Array //返回一个祖先节点数组集合,filterFn指定过滤条件,返回true的node将被选取 * @grammar UE.dom.domUtils.findParents(node,includeSelf,filterFn,closerFirst) => Array //返回一个祖先节点数组集合,closerFirst为true的话,node的直接父亲节点是数组的第0个 */ /** * 查找节点node的祖先节点集合, 如果includeSelf的值为true, * 则返回的结果集中允许出现当前给定的节点, 否则, 该节点不会出现在其结果集中。 * @method findParents * @param { Node } node 需要查找的节点对象 * @param { Boolean } includeSelf 查找的结果中是否允许包含当前查找的节点对象 * @return { Array } 给定节点的祖先节点数组 */ findParents:function (node, includeSelf, filterFn, closerFirst) { var parents = includeSelf && ( filterFn && filterFn(node) || !filterFn ) ? [node] : []; while (node = domUtils.findParent(node, filterFn)) { parents.push(node); } return closerFirst ? parents : parents.reverse(); }, /** * 在节点node后面插入新节点newNode * @method insertAfter * @param { Node } node 目标节点 * @param { Node } newNode 新插入的节点, 该节点将置于目标节点之后 * @return { Node } 新插入的节点 */ insertAfter:function (node, newNode) { return node.nextSibling ? node.parentNode.insertBefore(newNode, node.nextSibling): node.parentNode.appendChild(newNode); }, /** * 删除节点node及其下属的所有节点 * @method remove * @param { Node } node 需要删除的节点对象 * @return { Node } 返回刚删除的节点对象 * @example * ```html *
    *
    你好
    *
    * * ``` */ /** * 删除节点node,并根据keepChildren的值决定是否保留子节点 * @method remove * @param { Node } node 需要删除的节点对象 * @param { Boolean } keepChildren 是否需要保留子节点 * @return { Node } 返回刚删除的节点对象 * @example * ```html *
    *
    你好
    *
    * * ``` */ remove:function (node, keepChildren) { var parent = node.parentNode, child; if (parent) { if (keepChildren && node.hasChildNodes()) { while (child = node.firstChild) { parent.insertBefore(child, node); } } parent.removeChild(node); } return node; }, /** * 取得node节点的下一个兄弟节点, 如果该节点其后没有兄弟节点, 则递归查找其父节点之后的第一个兄弟节点, * 直到找到满足条件的节点或者递归到BODY节点之后才会结束。 * @method getNextDomNode * @param { Node } node 需要获取其后的兄弟节点的节点对象 * @return { Node | NULL } 如果找满足条件的节点, 则返回该节点, 否则返回NULL * @example * ```html * *
    * *
    * xxx * * * ``` * @example * ```html * *
    * * xxx *
    * xxx * * * ``` */ /** * 取得node节点的下一个兄弟节点, 如果startFromChild的值为ture,则先获取其子节点, * 如果有子节点则直接返回第一个子节点;如果没有子节点或者startFromChild的值为false, * 则执行getNextDomNode(Node node)的查找过程。 * @method getNextDomNode * @param { Node } node 需要获取其后的兄弟节点的节点对象 * @param { Boolean } startFromChild 查找过程是否从其子节点开始 * @return { Node | NULL } 如果找满足条件的节点, 则返回该节点, 否则返回NULL * @see UE.dom.domUtils.getNextDomNode(Node) */ getNextDomNode:function (node, startFromChild, filterFn, guard) { return getDomNode(node, 'firstChild', 'nextSibling', startFromChild, filterFn, guard); }, getPreDomNode:function (node, startFromChild, filterFn, guard) { return getDomNode(node, 'lastChild', 'previousSibling', startFromChild, filterFn, guard); }, /** * 检测节点node是否属是UEditor定义的bookmark节点 * @method isBookmarkNode * @private * @param { Node } node 需要检测的节点对象 * @return { Boolean } 是否是bookmark节点 * @example * ```html * * * ``` */ isBookmarkNode:function (node) { return node.nodeType == 1 && node.id && /^_baidu_bookmark_/i.test(node.id); }, /** * 获取节点node所属的window对象 * @method getWindow * @param { Node } node 节点对象 * @return { Window } 当前节点所属的window对象 * @example * ```javascript * //output: true * console.log( UE.dom.domUtils.getWindow( document.body ) === window ); * ``` */ getWindow:function (node) { var doc = node.ownerDocument || node; return doc.defaultView || doc.parentWindow; }, /** * 获取离nodeA与nodeB最近的公共的祖先节点 * @method getCommonAncestor * @param { Node } nodeA 第一个节点 * @param { Node } nodeB 第二个节点 * @remind 如果给定的两个节点是同一个节点, 将直接返回该节点。 * @return { Node | NULL } 如果未找到公共节点, 返回NULL, 否则返回最近的公共祖先节点。 * @example * ```javascript * var commonAncestor = UE.dom.domUtils.getCommonAncestor( document.body, document.body.firstChild ); * //output: true * console.log( commonAncestor.tagName.toLowerCase() === 'body' ); * ``` */ getCommonAncestor:function (nodeA, nodeB) { if (nodeA === nodeB) return nodeA; var parentsA = [nodeA] , parentsB = [nodeB], parent = nodeA, i = -1; while (parent = parent.parentNode) { if (parent === nodeB) { return parent; } parentsA.push(parent); } parent = nodeB; while (parent = parent.parentNode) { if (parent === nodeA) return parent; parentsB.push(parent); } parentsA.reverse(); parentsB.reverse(); while (i++, parentsA[i] === parentsB[i]) { } return i == 0 ? null : parentsA[i - 1]; }, /** * 清除node节点左右连续为空的兄弟inline节点 * @method clearEmptySibling * @param { Node } node 执行的节点对象, 如果该节点的左右连续的兄弟节点是空的inline节点, * 则这些兄弟节点将被删除 * @grammar UE.dom.domUtils.clearEmptySibling(node,ignoreNext) //ignoreNext指定是否忽略右边空节点 * @grammar UE.dom.domUtils.clearEmptySibling(node,ignoreNext,ignorePre) //ignorePre指定是否忽略左边空节点 * @example * ```html * *
    * * * * xxx * * * * ``` */ /** * 清除node节点左右连续为空的兄弟inline节点, 如果ignoreNext的值为true, * 则忽略对右边兄弟节点的操作。 * @method clearEmptySibling * @param { Node } node 执行的节点对象, 如果该节点的左右连续的兄弟节点是空的inline节点, * @param { Boolean } ignoreNext 是否忽略忽略对右边的兄弟节点的操作 * 则这些兄弟节点将被删除 * @see UE.dom.domUtils.clearEmptySibling(Node) */ /** * 清除node节点左右连续为空的兄弟inline节点, 如果ignoreNext的值为true, * 则忽略对右边兄弟节点的操作, 如果ignorePre的值为true,则忽略对左边兄弟节点的操作。 * @method clearEmptySibling * @param { Node } node 执行的节点对象, 如果该节点的左右连续的兄弟节点是空的inline节点, * @param { Boolean } ignoreNext 是否忽略忽略对右边的兄弟节点的操作 * @param { Boolean } ignorePre 是否忽略忽略对左边的兄弟节点的操作 * 则这些兄弟节点将被删除 * @see UE.dom.domUtils.clearEmptySibling(Node) */ clearEmptySibling:function (node, ignoreNext, ignorePre) { function clear(next, dir) { var tmpNode; while (next && !domUtils.isBookmarkNode(next) && (domUtils.isEmptyInlineElement(next) //这里不能把空格算进来会吧空格干掉,出现文字间的空格丢掉了 || !new RegExp('[^\t\n\r' + domUtils.fillChar + ']').test(next.nodeValue) )) { tmpNode = next[dir]; domUtils.remove(next); next = tmpNode; } } !ignoreNext && clear(node.nextSibling, 'nextSibling'); !ignorePre && clear(node.previousSibling, 'previousSibling'); }, /** * 将一个文本节点textNode拆分成两个文本节点,offset指定拆分位置 * @method split * @param { Node } textNode 需要拆分的文本节点对象 * @param { int } offset 需要拆分的位置, 位置计算从0开始 * @return { Node } 拆分后形成的新节点 * @example * ```html *
    abcdef
    * * ``` */ split:function (node, offset) { var doc = node.ownerDocument; if (browser.ie && offset == node.nodeValue.length) { var next = doc.createTextNode(''); return domUtils.insertAfter(node, next); } var retval = node.splitText(offset); //ie8下splitText不会跟新childNodes,我们手动触发他的更新 if (browser.ie8) { var tmpNode = doc.createTextNode(''); domUtils.insertAfter(retval, tmpNode); domUtils.remove(tmpNode); } return retval; }, /** * 检测文本节点textNode是否为空节点(包括空格、换行、占位符等字符) * @method isWhitespace * @param { Node } node 需要检测的节点对象 * @return { Boolean } 检测的节点是否为空 * @example * ```html *
    * *
    * * ``` */ isWhitespace:function (node) { return !new RegExp('[^ \t\n\r' + domUtils.fillChar + ']').test(node.nodeValue); }, /** * 获取元素element相对于viewport的位置坐标 * @method getXY * @param { Node } element 需要计算位置的节点对象 * @return { Object } 返回形如{x:left,y:top}的一个key-value映射对象, 其中键x代表水平偏移距离, * y代表垂直偏移距离。 * * @example * ```javascript * var location = UE.dom.domUtils.getXY( document.getElementById("test") ); * //output: test的坐标为: 12, 24 * console.log( 'test的坐标为: ', location.x, ',', location.y ); * ``` */ getXY:function (element) { var x = 0, y = 0; while (element.offsetParent) { y += element.offsetTop; x += element.offsetLeft; element = element.offsetParent; } return { 'x':x, 'y':y}; }, /** * 为元素element绑定原生DOM事件,type为事件类型,handler为处理函数 * @method on * @param { Node } element 需要绑定事件的节点对象 * @param { String } type 绑定的事件类型 * @param { Function } handler 事件处理器 * @example * ```javascript * UE.dom.domUtils.on(document.body,"click",function(e){ * //e为事件对象,this为被点击元素对戏那个 * }); * ``` */ /** * 为元素element绑定原生DOM事件,type为事件类型,handler为处理函数 * @method on * @param { Node } element 需要绑定事件的节点对象 * @param { Array } type 绑定的事件类型数组 * @param { Function } handler 事件处理器 * @example * ```javascript * UE.dom.domUtils.on(document.body,["click","mousedown"],function(evt){ * //evt为事件对象,this为被点击元素对象 * }); * ``` */ on:function (element, type, handler) { var types = utils.isArray(type) ? type : utils.trim(type).split(/\s+/), k = types.length; if (k) while (k--) { type = types[k]; if (element.addEventListener) { element.addEventListener(type, handler, false); } else { if (!handler._d) { handler._d = { els : [] }; } var key = type + handler.toString(),index = utils.indexOf(handler._d.els,element); if (!handler._d[key] || index == -1) { if(index == -1){ handler._d.els.push(element); } if(!handler._d[key]){ handler._d[key] = function (evt) { return handler.call(evt.srcElement, evt || window.event); }; } element.attachEvent('on' + type, handler._d[key]); } } } element = null; }, /** * 解除DOM事件绑定 * @method un * @param { Node } element 需要解除事件绑定的节点对象 * @param { String } type 需要接触绑定的事件类型 * @param { Function } handler 对应的事件处理器 * @example * ```javascript * UE.dom.domUtils.un(document.body,"click",function(evt){ * //evt为事件对象,this为被点击元素对象 * }); * ``` */ /** * 解除DOM事件绑定 * @method un * @param { Node } element 需要解除事件绑定的节点对象 * @param { Array } type 需要接触绑定的事件类型数组 * @param { Function } handler 对应的事件处理器 * @example * ```javascript * UE.dom.domUtils.un(document.body, ["click","mousedown"],function(evt){ * //evt为事件对象,this为被点击元素对象 * }); * ``` */ un:function (element, type, handler) { var types = utils.isArray(type) ? type : utils.trim(type).split(/\s+/), k = types.length; if (k) while (k--) { type = types[k]; if (element.removeEventListener) { element.removeEventListener(type, handler, false); } else { var key = type + handler.toString(); try{ element.detachEvent('on' + type, handler._d ? handler._d[key] : handler); }catch(e){} if (handler._d && handler._d[key]) { var index = utils.indexOf(handler._d.els,element); if(index!=-1){ handler._d.els.splice(index,1); } handler._d.els.length == 0 && delete handler._d[key]; } } } }, /** * 比较节点nodeA与节点nodeB是否具有相同的标签名、属性名以及属性值 * @method isSameElement * @param { Node } nodeA 需要比较的节点 * @param { Node } nodeB 需要比较的节点 * @return { Boolean } 两个节点是否具有相同的标签名、属性名以及属性值 * @example * ```html * ssss * bbbbb * ssss * bbbbb * * * ``` */ isSameElement:function (nodeA, nodeB) { if (nodeA.tagName != nodeB.tagName) { return false; } var thisAttrs = nodeA.attributes, otherAttrs = nodeB.attributes; if (!ie && thisAttrs.length != otherAttrs.length) { return false; } var attrA, attrB, al = 0, bl = 0; for (var i = 0; attrA = thisAttrs[i++];) { if (attrA.nodeName == 'style') { if (attrA.specified) { al++; } if (domUtils.isSameStyle(nodeA, nodeB)) { continue; } else { return false; } } if (ie) { if (attrA.specified) { al++; attrB = otherAttrs.getNamedItem(attrA.nodeName); } else { continue; } } else { attrB = nodeB.attributes[attrA.nodeName]; } if (!attrB.specified || attrA.nodeValue != attrB.nodeValue) { return false; } } // 有可能attrB的属性包含了attrA的属性之外还有自己的属性 if (ie) { for (i = 0; attrB = otherAttrs[i++];) { if (attrB.specified) { bl++; } } if (al != bl) { return false; } } return true; }, /** * 判断节点nodeA与节点nodeB的元素的style属性是否一致 * @method isSameStyle * @param { Node } nodeA 需要比较的节点 * @param { Node } nodeB 需要比较的节点 * @return { Boolean } 两个节点是否具有相同的style属性值 * @example * ```html * ssss * bbbbb * ssss * bbbbb * * * ``` */ isSameStyle:function (nodeA, nodeB) { var styleA = nodeA.style.cssText.replace(/( ?; ?)/g, ';').replace(/( ?: ?)/g, ':'), styleB = nodeB.style.cssText.replace(/( ?; ?)/g, ';').replace(/( ?: ?)/g, ':'); if (browser.opera) { styleA = nodeA.style; styleB = nodeB.style; if (styleA.length != styleB.length) return false; for (var p in styleA) { if (/^(\d+|csstext)$/i.test(p)) { continue; } if (styleA[p] != styleB[p]) { return false; } } return true; } if (!styleA || !styleB) { return styleA == styleB; } styleA = styleA.split(';'); styleB = styleB.split(';'); if (styleA.length != styleB.length) { return false; } for (var i = 0, ci; ci = styleA[i++];) { if (utils.indexOf(styleB, ci) == -1) { return false; } } return true; }, /** * 检查节点node是否为block元素 * @method isBlockElm * @param { Node } node 需要检测的节点对象 * @return { Boolean } 是否是block元素节点 * @warning 该方法的判断规则如下: 如果该元素原本是block元素, 则不论该元素当前的css样式是什么都会返回true; * 否则,检测该元素的css样式, 如果该元素当前是block元素, 则返回true。 其余情况下都返回false。 * @example * ```html * * *
    * * * ``` */ isBlockElm:function (node) { return node.nodeType == 1 && (dtd.$block[node.tagName] || styleBlock[domUtils.getComputedStyle(node, 'display')]) && !dtd.$nonChild[node.tagName]; }, /** * 检测node节点是否为body节点 * @method isBody * @param { Element } node 需要检测的dom元素 * @return { Boolean } 给定的元素是否是body元素 * @example * ```javascript * //output: true * console.log( UE.dom.domUtils.isBody( document.body ) ); * ``` */ isBody:function (node) { return node && node.nodeType == 1 && node.tagName.toLowerCase() == 'body'; }, /** * 以node节点为分界,将该节点的指定祖先节点parent拆分成两个独立的节点, * 拆分形成的两个节点之间是node节点 * @method breakParent * @param { Node } node 作为分界的节点对象 * @param { Node } parent 该节点必须是node节点的祖先节点, 且是block节点。 * @return { Node } 给定的node分界节点 * @example * ```javascript * * var node = document.createElement("span"), * wrapNode = document.createElement( "div" ), * parent = document.createElement("p"); * * parent.appendChild( node ); * wrapNode.appendChild( parent ); * * //拆分前 * //output:

    * console.log( wrapNode.innerHTML ); * * * UE.dom.domUtils.breakParent( node, parent ); * //拆分后 * //output:

    * console.log( wrapNode.innerHTML ); * * ``` */ breakParent:function (node, parent) { var tmpNode, parentClone = node, clone = node, leftNodes, rightNodes; do { parentClone = parentClone.parentNode; if (leftNodes) { tmpNode = parentClone.cloneNode(false); tmpNode.appendChild(leftNodes); leftNodes = tmpNode; tmpNode = parentClone.cloneNode(false); tmpNode.appendChild(rightNodes); rightNodes = tmpNode; } else { leftNodes = parentClone.cloneNode(false); rightNodes = leftNodes.cloneNode(false); } while (tmpNode = clone.previousSibling) { leftNodes.insertBefore(tmpNode, leftNodes.firstChild); } while (tmpNode = clone.nextSibling) { rightNodes.appendChild(tmpNode); } clone = parentClone; } while (parent !== parentClone); tmpNode = parent.parentNode; tmpNode.insertBefore(leftNodes, parent); tmpNode.insertBefore(rightNodes, parent); tmpNode.insertBefore(node, rightNodes); domUtils.remove(parent); return node; }, /** * 检查节点node是否是空inline节点 * @method isEmptyInlineElement * @param { Node } node 需要检测的节点对象 * @return { Number } 如果给定的节点是空的inline节点, 则返回1, 否则返回0。 * @example * ```html * => 1 * => 1 * => 1 * xx => 0 * ``` */ isEmptyInlineElement:function (node) { if (node.nodeType != 1 || !dtd.$removeEmpty[ node.tagName ]) { return 0; } node = node.firstChild; while (node) { //如果是创建的bookmark就跳过 if (domUtils.isBookmarkNode(node)) { return 0; } if (node.nodeType == 1 && !domUtils.isEmptyInlineElement(node) || node.nodeType == 3 && !domUtils.isWhitespace(node) ) { return 0; } node = node.nextSibling; } return 1; }, /** * 删除node节点下首尾两端的空白文本子节点 * @method trimWhiteTextNode * @param { Element } node 需要执行删除操作的元素对象 * @example * ```javascript * var node = document.createElement("div"); * * node.appendChild( document.createTextNode( "" ) ); * * node.appendChild( document.createElement("div") ); * * node.appendChild( document.createTextNode( "" ) ); * * //3 * console.log( node.childNodes.length ); * * UE.dom.domUtils.trimWhiteTextNode( node ); * * //1 * console.log( node.childNodes.length ); * ``` */ trimWhiteTextNode:function (node) { function remove(dir) { var child; while ((child = node[dir]) && child.nodeType == 3 && domUtils.isWhitespace(child)) { node.removeChild(child); } } remove('firstChild'); remove('lastChild'); }, /** * 合并node节点下相同的子节点 * @name mergeChild * @desc * UE.dom.domUtils.mergeChild(node,tagName) //tagName要合并的子节点的标签 * @example *

    xxaaxx

    * ==> UE.dom.domUtils.mergeChild(node,'span') *

    xxaaxx

    */ mergeChild:function (node, tagName, attrs) { var list = domUtils.getElementsByTagName(node, node.tagName.toLowerCase()); for (var i = 0, ci; ci = list[i++];) { if (!ci.parentNode || domUtils.isBookmarkNode(ci)) { continue; } //span单独处理 if (ci.tagName.toLowerCase() == 'span') { if (node === ci.parentNode) { domUtils.trimWhiteTextNode(node); if (node.childNodes.length == 1) { node.style.cssText = ci.style.cssText + ";" + node.style.cssText; domUtils.remove(ci, true); continue; } } ci.style.cssText = node.style.cssText + ';' + ci.style.cssText; if (attrs) { var style = attrs.style; if (style) { style = style.split(';'); for (var j = 0, s; s = style[j++];) { ci.style[utils.cssStyleToDomStyle(s.split(':')[0])] = s.split(':')[1]; } } } if (domUtils.isSameStyle(ci, node)) { domUtils.remove(ci, true); } continue; } if (domUtils.isSameElement(node, ci)) { domUtils.remove(ci, true); } } }, /** * 原生方法getElementsByTagName的封装 * @method getElementsByTagName * @param { Node } node 目标节点对象 * @param { String } tagName 需要查找的节点的tagName, 多个tagName以空格分割 * @return { Array } 符合条件的节点集合 */ getElementsByTagName:function (node, name,filter) { if(filter && utils.isString(filter)){ var className = filter; filter = function(node){return domUtils.hasClass(node,className)} } name = utils.trim(name).replace(/[ ]{2,}/g,' ').split(' '); var arr = []; for(var n = 0,ni;ni=name[n++];){ var list = node.getElementsByTagName(ni); for (var i = 0, ci; ci = list[i++];) { if(!filter || filter(ci)) arr.push(ci); } } return arr; }, /** * 将节点node提取到父节点上 * @method mergeToParent * @param { Element } node 需要提取的元素对象 * @example * ```html *
    *
    * *
    *
    * * * ``` */ mergeToParent:function (node) { var parent = node.parentNode; while (parent && dtd.$removeEmpty[parent.tagName]) { if (parent.tagName == node.tagName || parent.tagName == 'A') {//针对a标签单独处理 domUtils.trimWhiteTextNode(parent); //span需要特殊处理 不处理这样的情况 xxxxxxxxx if (parent.tagName == 'SPAN' && !domUtils.isSameStyle(parent, node) || (parent.tagName == 'A' && node.tagName == 'SPAN')) { if (parent.childNodes.length > 1 || parent !== node.parentNode) { node.style.cssText = parent.style.cssText + ";" + node.style.cssText; parent = parent.parentNode; continue; } else { parent.style.cssText += ";" + node.style.cssText; //trace:952 a标签要保持下划线 if (parent.tagName == 'A') { parent.style.textDecoration = 'underline'; } } } if (parent.tagName != 'A') { parent === node.parentNode && domUtils.remove(node, true); break; } } parent = parent.parentNode; } }, /** * 合并节点node的左右兄弟节点 * @method mergeSibling * @param { Element } node 需要合并的目标节点 * @example * ```html * xxxxoooxxxx * * * ``` */ /** * 合并节点node的左右兄弟节点, 可以根据给定的条件选择是否忽略合并左节点。 * @method mergeSibling * @param { Element } node 需要合并的目标节点 * @param { Boolean } ignorePre 是否忽略合并左节点 * @example * ```html * xxxxoooxxxx * * * ``` */ /** * 合并节点node的左右兄弟节点,可以根据给定的条件选择是否忽略合并左右节点。 * @method mergeSibling * @param { Element } node 需要合并的目标节点 * @param { Boolean } ignorePre 是否忽略合并左节点 * @param { Boolean } ignoreNext 是否忽略合并右节点 * @remind 如果同时忽略左右节点, 则该操作什么也不会做 * @example * ```html * xxxxoooxxxx * * * ``` */ mergeSibling:function (node, ignorePre, ignoreNext) { function merge(rtl, start, node) { var next; if ((next = node[rtl]) && !domUtils.isBookmarkNode(next) && next.nodeType == 1 && domUtils.isSameElement(node, next)) { while (next.firstChild) { if (start == 'firstChild') { node.insertBefore(next.lastChild, node.firstChild); } else { node.appendChild(next.firstChild); } } domUtils.remove(next); } } !ignorePre && merge('previousSibling', 'firstChild', node); !ignoreNext && merge('nextSibling', 'lastChild', node); }, /** * 设置节点node及其子节点不会被选中 * @method unSelectable * @param { Element } node 需要执行操作的dom元素 * @remind 执行该操作后的节点, 将不能被鼠标选中 * @example * ```javascript * UE.dom.domUtils.unSelectable( document.body ); * ``` */ unSelectable:ie && browser.ie9below || browser.opera ? function (node) { //for ie9 node.onselectstart = function () { return false; }; node.onclick = node.onkeyup = node.onkeydown = function () { return false; }; node.unselectable = 'on'; node.setAttribute("unselectable", "on"); for (var i = 0, ci; ci = node.all[i++];) { switch (ci.tagName.toLowerCase()) { case 'iframe' : case 'textarea' : case 'input' : case 'select' : break; default : ci.unselectable = 'on'; node.setAttribute("unselectable", "on"); } } } : function (node) { node.style.MozUserSelect = node.style.webkitUserSelect = node.style.msUserSelect = node.style.KhtmlUserSelect = 'none'; }, /** * 删除节点node上的指定属性名称的属性 * @method removeAttributes * @param { Node } node 需要删除属性的节点对象 * @param { String } attrNames 可以是空格隔开的多个属性名称,该操作将会依次删除相应的属性 * @example * ```html *
    * xxxxx *
    * * * ``` */ /** * 删除节点node上的指定属性名称的属性 * @method removeAttributes * @param { Node } node 需要删除属性的节点对象 * @param { Array } attrNames 需要删除的属性名数组 * @example * ```html *
    * xxxxx *
    * * * ``` */ removeAttributes:function (node, attrNames) { attrNames = utils.isArray(attrNames) ? attrNames : utils.trim(attrNames).replace(/[ ]{2,}/g,' ').split(' '); for (var i = 0, ci; ci = attrNames[i++];) { ci = attrFix[ci] || ci; switch (ci) { case 'className': node[ci] = ''; break; case 'style': node.style.cssText = ''; var val = node.getAttributeNode('style'); !browser.ie && val && node.removeAttributeNode(val); } node.removeAttribute(ci); } }, /** * 在doc下创建一个标签名为tag,属性为attrs的元素 * @method createElement * @param { DomDocument } doc 新创建的元素属于该document节点创建 * @param { String } tagName 需要创建的元素的标签名 * @param { Object } attrs 新创建的元素的属性key-value集合 * @return { Element } 新创建的元素对象 * @example * ```javascript * var ele = UE.dom.domUtils.createElement( document, 'div', { * id: 'test' * } ); * * //output: DIV * console.log( ele.tagName ); * * //output: test * console.log( ele.id ); * * ``` */ createElement:function (doc, tag, attrs) { return domUtils.setAttributes(doc.createElement(tag), attrs) }, /** * 为节点node添加属性attrs,attrs为属性键值对 * @method setAttributes * @param { Element } node 需要设置属性的元素对象 * @param { Object } attrs 需要设置的属性名-值对 * @return { Element } 设置属性的元素对象 * @example * ```html * * * * */ setAttributes:function (node, attrs) { for (var attr in attrs) { if(attrs.hasOwnProperty(attr)){ var value = attrs[attr]; switch (attr) { case 'class': //ie下要这样赋值,setAttribute不起作用 node.className = value; break; case 'style' : node.style.cssText = node.style.cssText + ";" + value; break; case 'innerHTML': node[attr] = value; break; case 'value': node.value = value; break; default: node.setAttribute(attrFix[attr] || attr, value); } } } return node; }, /** * 获取元素element经过计算后的样式值 * @method getComputedStyle * @param { Element } element 需要获取样式的元素对象 * @param { String } styleName 需要获取的样式名 * @return { String } 获取到的样式值 * @example * ```html * * * * * * ``` */ getComputedStyle:function (element, styleName) { //一下的属性单独处理 var pros = 'width height top left'; if(pros.indexOf(styleName) > -1){ return element['offset' + styleName.replace(/^\w/,function(s){return s.toUpperCase()})] + 'px'; } //忽略文本节点 if (element.nodeType == 3) { element = element.parentNode; } //ie下font-size若body下定义了font-size,则从currentStyle里会取到这个font-size. 取不到实际值,故此修改. if (browser.ie && browser.version < 9 && styleName == 'font-size' && !element.style.fontSize && !dtd.$empty[element.tagName] && !dtd.$nonChild[element.tagName]) { var span = element.ownerDocument.createElement('span'); span.style.cssText = 'padding:0;border:0;font-family:simsun;'; span.innerHTML = '.'; element.appendChild(span); var result = span.offsetHeight; element.removeChild(span); span = null; return result + 'px'; } try { var value = domUtils.getStyle(element, styleName) || (window.getComputedStyle ? domUtils.getWindow(element).getComputedStyle(element, '').getPropertyValue(styleName) : ( element.currentStyle || element.style )[utils.cssStyleToDomStyle(styleName)]); } catch (e) { return ""; } return utils.transUnitToPx(utils.fixColor(styleName, value)); }, /** * 删除元素element指定的className * @method removeClasses * @param { Element } ele 需要删除class的元素节点 * @param { String } classNames 需要删除的className, 多个className之间以空格分开 * @example * ```html * xxx * * * ``` */ /** * 删除元素element指定的className * @method removeClasses * @param { Element } ele 需要删除class的元素节点 * @param { Array } classNames 需要删除的className数组 * @example * ```html * xxx * * * ``` */ removeClasses:function (elm, classNames) { classNames = utils.isArray(classNames) ? classNames : utils.trim(classNames).replace(/[ ]{2,}/g,' ').split(' '); for(var i = 0,ci,cls = elm.className;ci=classNames[i++];){ cls = cls.replace(new RegExp('\\b' + ci + '\\b'),'') } cls = utils.trim(cls).replace(/[ ]{2,}/g,' '); if(cls){ elm.className = cls; }else{ domUtils.removeAttributes(elm,['class']); } }, /** * 给元素element添加className * @method addClass * @param { Node } ele 需要增加className的元素 * @param { String } classNames 需要添加的className, 多个className之间以空格分割 * @remind 相同的类名不会被重复添加 * @example * ```html * * * * ``` */ /** * 判断元素element是否包含给定的样式类名className * @method hasClass * @param { Node } ele 需要检测的元素 * @param { Array } classNames 需要检测的className数组 * @return { Boolean } 元素是否包含所有给定的className * @example * ```html * * * * ``` */ hasClass:function (element, className) { if(utils.isRegExp(className)){ return className.test(element.className) } className = utils.trim(className).replace(/[ ]{2,}/g,' ').split(' '); for(var i = 0,ci,cls = element.className;ci=className[i++];){ if(!new RegExp('\\b' + ci + '\\b','i').test(cls)){ return false; } } return i - 1 == className.length; }, /** * 阻止事件默认行为 * @method preventDefault * @param { Event } evt 需要阻止默认行为的事件对象 * @example * ```javascript * UE.dom.domUtils.preventDefault( evt ); * ``` */ preventDefault:function (evt) { evt.preventDefault ? evt.preventDefault() : (evt.returnValue = false); }, /** * 删除元素element指定的样式 * @method removeStyle * @param { Element } element 需要删除样式的元素 * @param { String } styleName 需要删除的样式名 * @example * ```html * * * * ``` */ removeStyle:function (element, name) { if(browser.ie ){ //针对color先单独处理一下 if(name == 'color'){ name = '(^|;)' + name; } element.style.cssText = element.style.cssText.replace(new RegExp(name + '[^:]*:[^;]+;?','ig'),'') }else{ if (element.style.removeProperty) { element.style.removeProperty (name); }else { element.style.removeAttribute (utils.cssStyleToDomStyle(name)); } } if (!element.style.cssText) { domUtils.removeAttributes(element, ['style']); } }, /** * 获取元素element的style属性的指定值 * @method getStyle * @param { Element } element 需要获取属性值的元素 * @param { String } styleName 需要获取的style的名称 * @warning 该方法仅获取元素style属性中所标明的值 * @return { String } 该元素包含指定的style属性值 * @example * ```html *
    * * * ``` */ getStyle:function (element, name) { var value = element.style[ utils.cssStyleToDomStyle(name) ]; return utils.fixColor(name, value); }, /** * 为元素element设置样式属性值 * @method setStyle * @param { Element } element 需要设置样式的元素 * @param { String } styleName 样式名 * @param { String } styleValue 样式值 * @example * ```html *
    * * * ``` */ setStyle:function (element, name, value) { element.style[utils.cssStyleToDomStyle(name)] = value; if(!utils.trim(element.style.cssText)){ this.removeAttributes(element,'style') } }, /** * 为元素element设置多个样式属性值 * @method setStyles * @param { Element } element 需要设置样式的元素 * @param { Object } styles 样式名值对 * @example * ```html *
    * * * ``` */ setStyles:function (element, styles) { for (var name in styles) { if (styles.hasOwnProperty(name)) { domUtils.setStyle(element, name, styles[name]); } } }, /** * 删除_moz_dirty属性 * @private * @method removeDirtyAttr */ removeDirtyAttr:function (node) { for (var i = 0, ci, nodes = node.getElementsByTagName('*'); ci = nodes[i++];) { ci.removeAttribute('_moz_dirty'); } node.removeAttribute('_moz_dirty'); }, /** * 获取子节点的数量 * @method getChildCount * @param { Element } node 需要检测的元素 * @return { Number } 给定的node元素的子节点数量 * @example * ```html *
    * *
    * * * ``` */ /** * 根据给定的过滤规则, 获取符合条件的子节点的数量 * @method getChildCount * @param { Element } node 需要检测的元素 * @param { Function } fn 过滤器, 要求对符合条件的子节点返回true, 反之则要求返回false * @return { Number } 符合过滤条件的node元素的子节点数量 * @example * ```html *
    * *
    * * * ``` */ getChildCount:function (node, fn) { var count = 0, first = node.firstChild; fn = fn || function () { return 1; }; while (first) { if (fn(first)) { count++; } first = first.nextSibling; } return count; }, /** * 判断给定节点是否为空节点 * @method isEmptyNode * @param { Node } node 需要检测的节点对象 * @return { Boolean } 节点是否为空 * @example * ```javascript * UE.dom.domUtils.isEmptyNode( document.body ); * ``` */ isEmptyNode:function (node) { return !node.firstChild || domUtils.getChildCount(node, function (node) { return !domUtils.isBr(node) && !domUtils.isBookmarkNode(node) && !domUtils.isWhitespace(node) }) == 0 }, clearSelectedArr:function (nodes) { var node; while (node = nodes.pop()) { domUtils.removeAttributes(node, ['class']); } }, /** * 将显示区域滚动到指定节点的位置 * @method scrollToView * @param {Node} node 节点 * @param {window} win window对象 * @param {Number} offsetTop 距离上方的偏移量 */ scrollToView:function (node, win, offsetTop) { var getViewPaneSize = function () { var doc = win.document, mode = doc.compatMode == 'CSS1Compat'; return { width:( mode ? doc.documentElement.clientWidth : doc.body.clientWidth ) || 0, height:( mode ? doc.documentElement.clientHeight : doc.body.clientHeight ) || 0 }; }, getScrollPosition = function (win) { if ('pageXOffset' in win) { return { x:win.pageXOffset || 0, y:win.pageYOffset || 0 }; } else { var doc = win.document; return { x:doc.documentElement.scrollLeft || doc.body.scrollLeft || 0, y:doc.documentElement.scrollTop || doc.body.scrollTop || 0 }; } }; var winHeight = getViewPaneSize().height, offset = winHeight * -1 + offsetTop; offset += (node.offsetHeight || 0); var elementPosition = domUtils.getXY(node); offset += elementPosition.y; var currentScroll = getScrollPosition(win).y; // offset += 50; if (offset > currentScroll || offset < currentScroll - winHeight) { win.scrollTo(0, offset + (offset < 0 ? -20 : 20)); } }, /** * 判断给定节点是否为br * @method isBr * @param { Node } node 需要判断的节点对象 * @return { Boolean } 给定的节点是否是br节点 */ isBr:function (node) { return node.nodeType == 1 && node.tagName == 'BR'; }, /** * 判断给定的节点是否是一个“填充”节点 * @private * @method isFillChar * @param { Node } node 需要判断的节点 * @param { Boolean } isInStart 是否从节点内容的开始位置匹配 * @returns { Boolean } 节点是否是填充节点 */ isFillChar:function (node,isInStart) { if(node.nodeType != 3) return false; var text = node.nodeValue; if(isInStart){ return new RegExp('^' + domUtils.fillChar).test(text) } return !text.replace(new RegExp(domUtils.fillChar,'g'), '').length }, isStartInblock:function (range) { var tmpRange = range.cloneRange(), flag = 0, start = tmpRange.startContainer, tmp; if(start.nodeType == 1 && start.childNodes[tmpRange.startOffset]){ start = start.childNodes[tmpRange.startOffset]; var pre = start.previousSibling; while(pre && domUtils.isFillChar(pre)){ start = pre; pre = pre.previousSibling; } } if(this.isFillChar(start,true) && tmpRange.startOffset == 1){ tmpRange.setStartBefore(start); start = tmpRange.startContainer; } while (start && domUtils.isFillChar(start)) { tmp = start; start = start.previousSibling } if (tmp) { tmpRange.setStartBefore(tmp); start = tmpRange.startContainer; } if (start.nodeType == 1 && domUtils.isEmptyNode(start) && tmpRange.startOffset == 1) { tmpRange.setStart(start, 0).collapse(true); } while (!tmpRange.startOffset) { start = tmpRange.startContainer; if (domUtils.isBlockElm(start) || domUtils.isBody(start)) { flag = 1; break; } var pre = tmpRange.startContainer.previousSibling, tmpNode; if (!pre) { tmpRange.setStartBefore(tmpRange.startContainer); } else { while (pre && domUtils.isFillChar(pre)) { tmpNode = pre; pre = pre.previousSibling; } if (tmpNode) { tmpRange.setStartBefore(tmpNode); } else { tmpRange.setStartBefore(tmpRange.startContainer); } } } return flag && !domUtils.isBody(tmpRange.startContainer) ? 1 : 0; }, /** * 判断给定的元素是否是一个空元素 * @method isEmptyBlock * @param { Element } node 需要判断的元素 * @return { Boolean } 是否是空元素 * @example * ```html *
    * * * ``` */ /** * 根据指定的判断规则判断给定的元素是否是一个空元素 * @method isEmptyBlock * @param { Element } node 需要判断的元素 * @param { RegExp } reg 对内容执行判断的正则表达式对象 * @return { Boolean } 是否是空元素 */ isEmptyBlock:function (node,reg) { // HaoChuan9421 if(!node){ return; } if(node.nodeType != 1) return 0; reg = reg || new RegExp('[ \xa0\t\r\n' + domUtils.fillChar + ']', 'g'); if (node[browser.ie ? 'innerText' : 'textContent'].replace(reg, '').length > 0) { return 0; } for (var n in dtd.$isNotEmpty) { if (node.getElementsByTagName(n).length) { return 0; } } return 1; }, /** * 移动元素使得该元素的位置移动指定的偏移量的距离 * @method setViewportOffset * @param { Element } element 需要设置偏移量的元素 * @param { Object } offset 偏移量, 形如{ left: 100, top: 50 }的一个键值对, 表示该元素将在 * 现有的位置上向水平方向偏移offset.left的距离, 在竖直方向上偏移 * offset.top的距离 * @example * ```html *
    * * * ``` */ setViewportOffset:function (element, offset) { var left = parseInt(element.style.left) | 0; var top = parseInt(element.style.top) | 0; var rect = element.getBoundingClientRect(); var offsetLeft = offset.left - rect.left; var offsetTop = offset.top - rect.top; if (offsetLeft) { element.style.left = left + offsetLeft + 'px'; } if (offsetTop) { element.style.top = top + offsetTop + 'px'; } }, /** * 用“填充字符”填充节点 * @method fillNode * @private * @param { DomDocument } doc 填充的节点所在的docment对象 * @param { Node } node 需要填充的节点对象 * @example * ```html *
    * * * ``` */ fillNode:function (doc, node) { var tmpNode = browser.ie ? doc.createTextNode(domUtils.fillChar) : doc.createElement('br'); node.innerHTML = ''; node.appendChild(tmpNode); }, /** * 把节点src的所有子节点追加到另一个节点tag上去 * @method moveChild * @param { Node } src 源节点, 该节点下的所有子节点将被移除 * @param { Node } tag 目标节点, 从源节点移除的子节点将被追加到该节点下 * @example * ```html *
    * *
    *
    *
    *
    * * * ``` */ /** * 把节点src的所有子节点移动到另一个节点tag上去, 可以通过dir参数控制附加的行为是“追加”还是“插入顶部” * @method moveChild * @param { Node } src 源节点, 该节点下的所有子节点将被移除 * @param { Node } tag 目标节点, 从源节点移除的子节点将被附加到该节点下 * @param { Boolean } dir 附加方式, 如果为true, 则附加进去的节点将被放到目标节点的顶部, 反之,则放到末尾 * @example * ```html *
    * *
    *
    *
    *
    * * * ``` */ moveChild:function (src, tag, dir) { while (src.firstChild) { if (dir && tag.firstChild) { tag.insertBefore(src.lastChild, tag.firstChild); } else { tag.appendChild(src.firstChild); } } }, /** * 判断节点的标签上是否不存在任何属性 * @method hasNoAttributes * @private * @param { Node } node 需要检测的节点对象 * @return { Boolean } 节点是否不包含任何属性 * @example * ```html *
    xxxx
    * * * ``` */ hasNoAttributes:function (node) { return browser.ie ? /^<\w+\s*?>/.test(node.outerHTML) : node.attributes.length == 0; }, /** * 检测节点是否是UEditor所使用的辅助节点 * @method isCustomeNode * @private * @param { Node } node 需要检测的节点 * @remind 辅助节点是指编辑器要完成工作临时添加的节点, 在输出的时候将会从编辑器内移除, 不会影响最终的结果。 * @return { Boolean } 给定的节点是否是一个辅助节点 */ isCustomeNode:function (node) { return node.nodeType == 1 && node.getAttribute('_ue_custom_node_'); }, /** * 检测节点的标签是否是给定的标签 * @method isTagNode * @param { Node } node 需要检测的节点对象 * @param { String } tagName 标签 * @return { Boolean } 节点的标签是否是给定的标签 * @example * ```html *
    * * * ``` */ isTagNode:function (node, tagNames) { return node.nodeType == 1 && new RegExp('\\b' + node.tagName + '\\b','i').test(tagNames) }, /** * 给定一个节点数组,在通过指定的过滤器过滤后, 获取其中满足过滤条件的第一个节点 * @method filterNodeList * @param { Array } nodeList 需要过滤的节点数组 * @param { Function } fn 过滤器, 对符合条件的节点, 执行结果返回true, 反之则返回false * @return { Node | NULL } 如果找到符合过滤条件的节点, 则返回该节点, 否则返回NULL * @example * ```javascript * var divNodes = document.getElementsByTagName("div"); * divNodes = [].slice.call( divNodes, 0 ); * * //output: null * console.log( UE.dom.domUtils.filterNodeList( divNodes, function ( node ) { * return node.tagName.toLowerCase() !== 'div'; * } ) ); * ``` */ /** * 给定一个节点数组nodeList和一组标签名tagNames, 获取其中能够匹配标签名的节点集合中的第一个节点 * @method filterNodeList * @param { Array } nodeList 需要过滤的节点数组 * @param { String } tagNames 需要匹配的标签名, 多个标签名之间用空格分割 * @return { Node | NULL } 如果找到标签名匹配的节点, 则返回该节点, 否则返回NULL * @example * ```javascript * var divNodes = document.getElementsByTagName("div"); * divNodes = [].slice.call( divNodes, 0 ); * * //output: null * console.log( UE.dom.domUtils.filterNodeList( divNodes, 'a span' ) ); * ``` */ /** * 给定一个节点数组,在通过指定的过滤器过滤后, 如果参数forAll为true, 则会返回所有满足过滤 * 条件的节点集合, 否则, 返回满足条件的节点集合中的第一个节点 * @method filterNodeList * @param { Array } nodeList 需要过滤的节点数组 * @param { Function } fn 过滤器, 对符合条件的节点, 执行结果返回true, 反之则返回false * @param { Boolean } forAll 是否返回整个节点数组, 如果该参数为false, 则返回节点集合中的第一个节点 * @return { Array | Node | NULL } 如果找到符合过滤条件的节点, 则根据参数forAll的值决定返回满足 * 过滤条件的节点数组或第一个节点, 否则返回NULL * @example * ```javascript * var divNodes = document.getElementsByTagName("div"); * divNodes = [].slice.call( divNodes, 0 ); * * //output: 3(假定有3个div) * console.log( divNodes.length ); * * var nodes = UE.dom.domUtils.filterNodeList( divNodes, function ( node ) { * return node.tagName.toLowerCase() === 'div'; * }, true ); * * //output: 3 * console.log( nodes.length ); * * var node = UE.dom.domUtils.filterNodeList( divNodes, function ( node ) { * return node.tagName.toLowerCase() === 'div'; * }, false ); * * //output: div * console.log( node.nodeName ); * ``` */ filterNodeList : function(nodelist,filter,forAll){ var results = []; if(!utils .isFunction(filter)){ var str = filter; filter = function(n){ return utils.indexOf(utils.isArray(str) ? str:str.split(' '), n.tagName.toLowerCase()) != -1 }; } utils.each(nodelist,function(n){ filter(n) && results.push(n) }); return results.length == 0 ? null : results.length == 1 || !forAll ? results[0] : results }, /** * 查询给定的range选区是否在给定的node节点内,且在该节点的最末尾 * @method isInNodeEndBoundary * @param { UE.dom.Range } rng 需要判断的range对象, 该对象的startContainer不能为NULL * @param node 需要检测的节点对象 * @return { Number } 如果给定的选取range对象是在node内部的最末端, 则返回1, 否则返回0 */ isInNodeEndBoundary : function (rng,node){ var start = rng.startContainer; if(start.nodeType == 3 && rng.startOffset != start.nodeValue.length){ return 0; } if(start.nodeType == 1 && rng.startOffset != start.childNodes.length){ return 0; } while(start !== node){ if(start.nextSibling){ return 0 }; start = start.parentNode; } return 1; }, isBoundaryNode : function (node,dir){ var tmp; while(!domUtils.isBody(node)){ tmp = node; node = node.parentNode; if(tmp !== node[dir]){ return false; } } return true; }, fillHtml : browser.ie11below ? ' ' : '
    ' }; var fillCharReg = new RegExp(domUtils.fillChar, 'g'); // core/Range.js /** * Range封装 * @file * @module UE.dom * @class Range * @since 1.2.6.1 */ /** * dom操作封装 * @unfile * @module UE.dom */ /** * Range实现类,本类是UEditor底层核心类,封装不同浏览器之间的Range操作。 * @unfile * @module UE.dom * @class Range */ (function () { var guid = 0, fillChar = domUtils.fillChar, fillData; /** * 更新range的collapse状态 * @param {Range} range range对象 */ function updateCollapse(range) { range.collapsed = range.startContainer && range.endContainer && range.startContainer === range.endContainer && range.startOffset == range.endOffset; } function selectOneNode(rng){ return !rng.collapsed && rng.startContainer.nodeType == 1 && rng.startContainer === rng.endContainer && rng.endOffset - rng.startOffset == 1 } function setEndPoint(toStart, node, offset, range) { //如果node是自闭合标签要处理 if (node.nodeType == 1 && (dtd.$empty[node.tagName] || dtd.$nonChild[node.tagName])) { offset = domUtils.getNodeIndex(node) + (toStart ? 0 : 1); node = node.parentNode; } if (toStart) { range.startContainer = node; range.startOffset = offset; if (!range.endContainer) { range.collapse(true); } } else { range.endContainer = node; range.endOffset = offset; if (!range.startContainer) { range.collapse(false); } } updateCollapse(range); return range; } function execContentsAction(range, action) { //调整边界 //range.includeBookmark(); var start = range.startContainer, end = range.endContainer, startOffset = range.startOffset, endOffset = range.endOffset, doc = range.document, frag = doc.createDocumentFragment(), tmpStart, tmpEnd; if (start.nodeType == 1) { start = start.childNodes[startOffset] || (tmpStart = start.appendChild(doc.createTextNode(''))); } if (end.nodeType == 1) { end = end.childNodes[endOffset] || (tmpEnd = end.appendChild(doc.createTextNode(''))); } if (start === end && start.nodeType == 3) { frag.appendChild(doc.createTextNode(start.substringData(startOffset, endOffset - startOffset))); //is not clone if (action) { start.deleteData(startOffset, endOffset - startOffset); range.collapse(true); } return frag; } var current, currentLevel, clone = frag, startParents = domUtils.findParents(start, true), endParents = domUtils.findParents(end, true); for (var i = 0; startParents[i] == endParents[i];) { i++; } for (var j = i, si; si = startParents[j]; j++) { current = si.nextSibling; if (si == start) { if (!tmpStart) { if (range.startContainer.nodeType == 3) { clone.appendChild(doc.createTextNode(start.nodeValue.slice(startOffset))); //is not clone if (action) { start.deleteData(startOffset, start.nodeValue.length - startOffset); } } else { clone.appendChild(!action ? start.cloneNode(true) : start); } } } else { currentLevel = si.cloneNode(false); clone.appendChild(currentLevel); } while (current) { if (current === end || current === endParents[j]) { break; } si = current.nextSibling; clone.appendChild(!action ? current.cloneNode(true) : current); current = si; } clone = currentLevel; } clone = frag; if (!startParents[i]) { clone.appendChild(startParents[i - 1].cloneNode(false)); clone = clone.firstChild; } for (var j = i, ei; ei = endParents[j]; j++) { current = ei.previousSibling; if (ei == end) { if (!tmpEnd && range.endContainer.nodeType == 3) { clone.appendChild(doc.createTextNode(end.substringData(0, endOffset))); //is not clone if (action) { end.deleteData(0, endOffset); } } } else { currentLevel = ei.cloneNode(false); clone.appendChild(currentLevel); } //如果两端同级,右边第一次已经被开始做了 if (j != i || !startParents[i]) { while (current) { if (current === start) { break; } ei = current.previousSibling; clone.insertBefore(!action ? current.cloneNode(true) : current, clone.firstChild); current = ei; } } clone = currentLevel; } if (action) { range.setStartBefore(!endParents[i] ? endParents[i - 1] : !startParents[i] ? startParents[i - 1] : endParents[i]).collapse(true); } tmpStart && domUtils.remove(tmpStart); tmpEnd && domUtils.remove(tmpEnd); return frag; } /** * 创建一个跟document绑定的空的Range实例 * @constructor * @param { Document } document 新建的选区所属的文档对象 */ /** * @property { Node } startContainer 当前Range的开始边界的容器节点, 可以是一个元素节点或者是文本节点 */ /** * @property { Node } startOffset 当前Range的开始边界容器节点的偏移量, 如果是元素节点, * 该值就是childNodes中的第几个节点, 如果是文本节点就是文本内容的第几个字符 */ /** * @property { Node } endContainer 当前Range的结束边界的容器节点, 可以是一个元素节点或者是文本节点 */ /** * @property { Node } endOffset 当前Range的结束边界容器节点的偏移量, 如果是元素节点, * 该值就是childNodes中的第几个节点, 如果是文本节点就是文本内容的第几个字符 */ /** * @property { Boolean } collapsed 当前Range是否闭合 * @default true * @remind Range是闭合的时候, startContainer === endContainer && startOffset === endOffset */ /** * @property { Document } document 当前Range所属的Document对象 * @remind 不同range的的document属性可以是不同的 */ var Range = dom.Range = function (document) { var me = this; me.startContainer = me.startOffset = me.endContainer = me.endOffset = null; me.document = document; me.collapsed = true; }; /** * 删除fillData * @param doc * @param excludeNode */ function removeFillData(doc, excludeNode) { try { if (fillData && domUtils.inDoc(fillData, doc)) { if (!fillData.nodeValue.replace(fillCharReg, '').length) { var tmpNode = fillData.parentNode; domUtils.remove(fillData); while (tmpNode && domUtils.isEmptyInlineElement(tmpNode) && //safari的contains有bug (browser.safari ? !(domUtils.getPosition(tmpNode,excludeNode) & domUtils.POSITION_CONTAINS) : !tmpNode.contains(excludeNode)) ) { fillData = tmpNode.parentNode; domUtils.remove(tmpNode); tmpNode = fillData; } } else { fillData.nodeValue = fillData.nodeValue.replace(fillCharReg, ''); } } } catch (e) { } } /** * @param node * @param dir */ function mergeSibling(node, dir) { var tmpNode; node = node[dir]; while (node && domUtils.isFillChar(node)) { tmpNode = node[dir]; domUtils.remove(node); node = tmpNode; } } Range.prototype = { /** * 克隆选区的内容到一个DocumentFragment里 * @method cloneContents * @return { DocumentFragment | NULL } 如果选区是闭合的将返回null, 否则, 返回包含所clone内容的DocumentFragment元素 * @example * ```html * * * xx[xxx]x * * * * ``` */ cloneContents:function () { return this.collapsed ? null : execContentsAction(this, 0); }, /** * 删除当前选区范围中的所有内容 * @method deleteContents * @remind 执行完该操作后, 当前Range对象变成了闭合状态 * @return { UE.dom.Range } 当前操作的Range对象 * @example * ```html * * * xx[xxx]x * * * * ``` */ deleteContents:function () { var txt; if (!this.collapsed) { execContentsAction(this, 1); } if (browser.webkit) { txt = this.startContainer; if (txt.nodeType == 3 && !txt.nodeValue.length) { this.setStartBefore(txt).collapse(true); domUtils.remove(txt); } } return this; }, /** * 将当前选区的内容提取到一个DocumentFragment里 * @method extractContents * @remind 执行该操作后, 选区将变成闭合状态 * @warning 执行该操作后, 原来选区所选中的内容将从dom树上剥离出来 * @return { DocumentFragment } 返回包含所提取内容的DocumentFragment对象 * @example * ```html * * * xx[xxx]x * * * */ extractContents:function () { return this.collapsed ? null : execContentsAction(this, 2); }, /** * 设置Range的开始容器节点和偏移量 * @method setStart * @remind 如果给定的节点是元素节点,那么offset指的是其子元素中索引为offset的元素, * 如果是文本节点,那么offset指的是其文本内容的第offset个字符 * @remind 如果提供的容器节点是一个不能包含子元素的节点, 则该选区的开始容器将被设置 * 为该节点的父节点, 此时, 其距离开始容器的偏移量也变成了该节点在其父节点 * 中的索引 * @param { Node } node 将被设为当前选区开始边界容器的节点对象 * @param { int } offset 选区的开始位置偏移量 * @return { UE.dom.Range } 当前range对象 * @example * ```html * * xxxxxxxxxxxxx[xxx] * * * ``` * @example * ```html * * xxx[xx]x * * * ``` */ setStart:function (node, offset) { return setEndPoint(true, node, offset, this); }, /** * 设置Range的结束容器和偏移量 * @method setEnd * @param { Node } node 作为当前选区结束边界容器的节点对象 * @param { int } offset 结束边界的偏移量 * @see UE.dom.Range:setStart(Node,int) * @return { UE.dom.Range } 当前range对象 */ setEnd:function (node, offset) { return setEndPoint(false, node, offset, this); }, /** * 将Range开始位置设置到node节点之后 * @method setStartAfter * @remind 该操作将会把给定节点的父节点作为range的开始容器, 且偏移量是该节点在其父节点中的位置索引+1 * @param { Node } node 选区的开始边界将紧接着该节点之后 * @return { UE.dom.Range } 当前range对象 * @example * ```html * * xxxxxxx[xxxx] * * * ``` */ setStartAfter:function (node) { return this.setStart(node.parentNode, domUtils.getNodeIndex(node) + 1); }, /** * 将Range开始位置设置到node节点之前 * @method setStartBefore * @remind 该操作将会把给定节点的父节点作为range的开始容器, 且偏移量是该节点在其父节点中的位置索引 * @param { Node } node 新的选区开始位置在该节点之前 * @see UE.dom.Range:setStartAfter(Node) * @return { UE.dom.Range } 当前range对象 */ setStartBefore:function (node) { return this.setStart(node.parentNode, domUtils.getNodeIndex(node)); }, /** * 将Range结束位置设置到node节点之后 * @method setEndAfter * @remind 该操作将会把给定节点的父节点作为range的结束容器, 且偏移量是该节点在其父节点中的位置索引+1 * @param { Node } node 目标节点 * @see UE.dom.Range:setStartAfter(Node) * @return { UE.dom.Range } 当前range对象 * @example * ```html * * [xxxxxxx]xxxx * * * ``` */ setEndAfter:function (node) { return this.setEnd(node.parentNode, domUtils.getNodeIndex(node) + 1); }, /** * 将Range结束位置设置到node节点之前 * @method setEndBefore * @remind 该操作将会把给定节点的父节点作为range的结束容器, 且偏移量是该节点在其父节点中的位置索引 * @param { Node } node 目标节点 * @see UE.dom.Range:setEndAfter(Node) * @return { UE.dom.Range } 当前range对象 */ setEndBefore:function (node) { return this.setEnd(node.parentNode, domUtils.getNodeIndex(node)); }, /** * 设置Range的开始位置到node节点内的第一个子节点之前 * @method setStartAtFirst * @remind 选区的开始容器将变成给定的节点, 且偏移量为0 * @remind 如果给定的节点是元素节点, 则该节点必须是允许包含子节点的元素。 * @param { Node } node 目标节点 * @see UE.dom.Range:setStartBefore(Node) * @return { UE.dom.Range } 当前range对象 * @example * ```html * * xxxxx[xx]xxxx * * * ``` */ setStartAtFirst:function (node) { return this.setStart(node, 0); }, /** * 设置Range的开始位置到node节点内的最后一个节点之后 * @method setStartAtLast * @remind 选区的开始容器将变成给定的节点, 且偏移量为该节点的子节点数 * @remind 如果给定的节点是元素节点, 则该节点必须是允许包含子节点的元素。 * @param { Node } node 目标节点 * @see UE.dom.Range:setStartAtFirst(Node) * @return { UE.dom.Range } 当前range对象 */ setStartAtLast:function (node) { return this.setStart(node, node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length); }, /** * 设置Range的结束位置到node节点内的第一个节点之前 * @method setEndAtFirst * @param { Node } node 目标节点 * @remind 选区的结束容器将变成给定的节点, 且偏移量为0 * @remind node必须是一个元素节点, 且必须是允许包含子节点的元素。 * @see UE.dom.Range:setStartAtFirst(Node) * @return { UE.dom.Range } 当前range对象 */ setEndAtFirst:function (node) { return this.setEnd(node, 0); }, /** * 设置Range的结束位置到node节点内的最后一个节点之后 * @method setEndAtLast * @param { Node } node 目标节点 * @remind 选区的结束容器将变成给定的节点, 且偏移量为该节点的子节点数量 * @remind node必须是一个元素节点, 且必须是允许包含子节点的元素。 * @see UE.dom.Range:setStartAtFirst(Node) * @return { UE.dom.Range } 当前range对象 */ setEndAtLast:function (node) { return this.setEnd(node, node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length); }, /** * 选中给定节点 * @method selectNode * @remind 此时, 选区的开始容器和结束容器都是该节点的父节点, 其startOffset是该节点在父节点中的位置索引, * 而endOffset为startOffset+1 * @param { Node } node 需要选中的节点 * @return { UE.dom.Range } 当前range对象,此时的range仅包含当前给定的节点对象 * @example * ```html * * xxxxx[xx]xxxx * * * ``` */ selectNode:function (node) { return this.setStartBefore(node).setEndAfter(node); }, /** * 选中给定节点内部的所有节点 * @method selectNodeContents * @remind 此时, 选区的开始容器和结束容器都是该节点, 其startOffset为0, * 而endOffset是该节点的子节点数。 * @param { Node } node 目标节点, 当前range将包含该节点内的所有节点 * @return { UE.dom.Range } 当前range对象, 此时range仅包含给定节点的所有子节点 * @example * ```html * * xxxxx[xx]xxxx * * * ``` */ selectNodeContents:function (node) { return this.setStart(node, 0).setEndAtLast(node); }, /** * clone当前Range对象 * @method cloneRange * @remind 返回的range是一个全新的range对象, 其内部所有属性与当前被clone的range相同。 * @return { UE.dom.Range } 当前range对象的一个副本 */ cloneRange:function () { var me = this; return new Range(me.document).setStart(me.startContainer, me.startOffset).setEnd(me.endContainer, me.endOffset); }, /** * 向当前选区的结束处闭合选区 * @method collapse * @return { UE.dom.Range } 当前range对象 * @example * ```html * * xxxxx[xx]xxxx * * * ``` */ /** * 闭合当前选区,根据给定的toStart参数项决定是向当前选区开始处闭合还是向结束处闭合, * 如果toStart的值为true,则向开始位置闭合, 反之,向结束位置闭合。 * @method collapse * @param { Boolean } toStart 是否向选区开始处闭合 * @return { UE.dom.Range } 当前range对象,此时range对象处于闭合状态 * @see UE.dom.Range:collapse() * @example * ```html * * xxxxx[xx]xxxx * * * ``` */ collapse:function (toStart) { var me = this; if (toStart) { me.endContainer = me.startContainer; me.endOffset = me.startOffset; } else { me.startContainer = me.endContainer; me.startOffset = me.endOffset; } me.collapsed = true; return me; }, /** * 调整range的开始位置和结束位置,使其"收缩"到最小的位置 * @method shrinkBoundary * @return { UE.dom.Range } 当前range对象 * @example * ```html * xxxx[xxxxx] => xxxx[xxxxx] * ``` * * @example * ```html * * x[xx]xxx * * * ``` * * @example * ```html * [xxxxxxxxxxx] => [xxxxxxxxxxx] * ``` */ /** * 调整range的开始位置和结束位置,使其"收缩"到最小的位置, * 如果ignoreEnd的值为true,则忽略对结束位置的调整 * @method shrinkBoundary * @param { Boolean } ignoreEnd 是否忽略对结束位置的调整 * @return { UE.dom.Range } 当前range对象 * @see UE.dom.domUtils.Range:shrinkBoundary() */ shrinkBoundary:function (ignoreEnd) { var me = this, child, collapsed = me.collapsed; function check(node){ return node.nodeType == 1 && !domUtils.isBookmarkNode(node) && !dtd.$empty[node.tagName] && !dtd.$nonChild[node.tagName] } while (me.startContainer.nodeType == 1 //是element && (child = me.startContainer.childNodes[me.startOffset]) //子节点也是element && check(child)) { me.setStart(child, 0); } if (collapsed) { return me.collapse(true); } if (!ignoreEnd) { while (me.endContainer.nodeType == 1//是element && me.endOffset > 0 //如果是空元素就退出 endOffset=0那么endOffst-1为负值,childNodes[endOffset]报错 && (child = me.endContainer.childNodes[me.endOffset - 1]) //子节点也是element && check(child)) { me.setEnd(child, child.childNodes.length); } } return me; }, /** * 获取离当前选区内包含的所有节点最近的公共祖先节点, * @method getCommonAncestor * @remind 返回的公共祖先节点一定不是range自身的容器节点, 但有可能是一个文本节点 * @return { Node } 当前range对象内所有节点的公共祖先节点 * @example * ```html * //选区示例 * xxxx[xxx]xxxxxx * * ``` */ /** * 获取当前选区所包含的所有节点的公共祖先节点, 可以根据给定的参数 includeSelf 决定获取到 * 的公共祖先节点是否可以是当前选区的startContainer或endContainer节点, 如果 includeSelf * 的取值为true, 则返回的节点可以是自身的容器节点, 否则, 则不能是容器节点 * @method getCommonAncestor * @param { Boolean } includeSelf 是否允许获取到的公共祖先节点是当前range对象的容器节点 * @return { Node } 当前range对象内所有节点的公共祖先节点 * @see UE.dom.Range:getCommonAncestor() * @example * ```html * * * * xxxxxxxxx[xxx]xxxxxxxx * * * * * ``` */ /** * 获取当前选区所包含的所有节点的公共祖先节点, 可以根据给定的参数 includeSelf 决定获取到 * 的公共祖先节点是否可以是当前选区的startContainer或endContainer节点, 如果 includeSelf * 的取值为true, 则返回的节点可以是自身的容器节点, 否则, 则不能是容器节点; 同时可以根据 * ignoreTextNode 参数的取值决定是否忽略类型为文本节点的祖先节点。 * @method getCommonAncestor * @param { Boolean } includeSelf 是否允许获取到的公共祖先节点是当前range对象的容器节点 * @param { Boolean } ignoreTextNode 获取祖先节点的过程中是否忽略类型为文本节点的祖先节点 * @return { Node } 当前range对象内所有节点的公共祖先节点 * @see UE.dom.Range:getCommonAncestor() * @see UE.dom.Range:getCommonAncestor(Boolean) * @example * ```html * * * * xxxxxxxx[x]xxxxxxxxxxx * * * * * ``` */ getCommonAncestor:function (includeSelf, ignoreTextNode) { var me = this, start = me.startContainer, end = me.endContainer; if (start === end) { if (includeSelf && selectOneNode(this)) { start = start.childNodes[me.startOffset]; if(start.nodeType == 1) return start; } //只有在上来就相等的情况下才会出现是文本的情况 return ignoreTextNode && start.nodeType == 3 ? start.parentNode : start; } return domUtils.getCommonAncestor(start, end); }, /** * 调整当前Range的开始和结束边界容器,如果是容器节点是文本节点,就调整到包含该文本节点的父节点上 * @method trimBoundary * @remind 该操作有可能会引起文本节点被切开 * @return { UE.dom.Range } 当前range对象 * @example * ```html * * //选区示例 * xxx[xxxxx]xxx * * * ``` */ /** * 调整当前Range的开始和结束边界容器,如果是容器节点是文本节点,就调整到包含该文本节点的父节点上, * 可以根据 ignoreEnd 参数的值决定是否调整对结束边界的调整 * @method trimBoundary * @param { Boolean } ignoreEnd 是否忽略对结束边界的调整 * @return { UE.dom.Range } 当前range对象 * @example * ```html * * //选区示例 * xxx[xxxxx]xxx * * * ``` */ trimBoundary:function (ignoreEnd) { this.txtToElmBoundary(); var start = this.startContainer, offset = this.startOffset, collapsed = this.collapsed, end = this.endContainer; if (start.nodeType == 3) { if (offset == 0) { this.setStartBefore(start); } else { if (offset >= start.nodeValue.length) { this.setStartAfter(start); } else { var textNode = domUtils.split(start, offset); //跟新结束边界 if (start === end) { this.setEnd(textNode, this.endOffset - offset); } else if (start.parentNode === end) { this.endOffset += 1; } this.setStartBefore(textNode); } } if (collapsed) { return this.collapse(true); } } if (!ignoreEnd) { offset = this.endOffset; end = this.endContainer; if (end.nodeType == 3) { if (offset == 0) { this.setEndBefore(end); } else { offset < end.nodeValue.length && domUtils.split(end, offset); this.setEndAfter(end); } } } return this; }, /** * 如果选区在文本的边界上,就扩展选区到文本的父节点上, 如果当前选区是闭合的, 则什么也不做 * @method txtToElmBoundary * @remind 该操作不会修改dom节点 * @return { UE.dom.Range } 当前range对象 */ /** * 如果选区在文本的边界上,就扩展选区到文本的父节点上, 如果当前选区是闭合的, 则根据参数项 * ignoreCollapsed 的值决定是否执行该调整 * @method txtToElmBoundary * @param { Boolean } ignoreCollapsed 是否忽略选区的闭合状态, 如果该参数取值为true, 则 * 不论选区是否闭合, 都会执行该操作, 反之, 则不会对闭合的选区执行该操作 * @return { UE.dom.Range } 当前range对象 */ txtToElmBoundary:function (ignoreCollapsed) { function adjust(r, c) { var container = r[c + 'Container'], offset = r[c + 'Offset']; if (container.nodeType == 3) { if (!offset) { r['set' + c.replace(/(\w)/, function (a) { return a.toUpperCase(); }) + 'Before'](container); } else if (offset >= container.nodeValue.length) { r['set' + c.replace(/(\w)/, function (a) { return a.toUpperCase(); }) + 'After' ](container); } } } if (ignoreCollapsed || !this.collapsed) { adjust(this, 'start'); adjust(this, 'end'); } return this; }, /** * 在当前选区的开始位置前插入节点,新插入的节点会被该range包含 * @method insertNode * @param { Node } node 需要插入的节点 * @remind 插入的节点可以是一个DocumentFragment依次插入多个节点 * @return { UE.dom.Range } 当前range对象 */ insertNode:function (node) { var first = node, length = 1; if (node.nodeType == 11) { first = node.firstChild; length = node.childNodes.length; } this.trimBoundary(true); var start = this.startContainer, offset = this.startOffset; var nextNode = start.childNodes[ offset ]; if (nextNode) { start.insertBefore(node, nextNode); } else { start.appendChild(node); } if (first.parentNode === this.endContainer) { this.endOffset = this.endOffset + length; } return this.setStartBefore(first); }, /** * 闭合选区到当前选区的开始位置, 并且定位光标到闭合后的位置 * @method setCursor * @return { UE.dom.Range } 当前range对象 * @see UE.dom.Range:collapse() */ /** * 闭合选区,可以根据参数toEnd的值控制选区是向前闭合还是向后闭合, 并且定位光标到闭合后的位置。 * @method setCursor * @param { Boolean } toEnd 是否向后闭合, 如果为true, 则闭合选区时, 将向结束容器方向闭合, * 反之,则向开始容器方向闭合 * @return { UE.dom.Range } 当前range对象 * @see UE.dom.Range:collapse(Boolean) */ setCursor:function (toEnd, noFillData) { return this.collapse(!toEnd).select(noFillData); }, /** * 创建当前range的一个书签,记录下当前range的位置,方便当dom树改变时,还能找回原来的选区位置 * @method createBookmark * @param { Boolean } serialize 控制返回的标记位置是对当前位置的引用还是ID,如果该值为true,则 * 返回标记位置的ID, 反之则返回标记位置节点的引用 * @return { Object } 返回一个书签记录键值对, 其包含的key有: start => 开始标记的ID或者引用, * end => 结束标记的ID或引用, id => 当前标记的类型, 如果为true,则表示 * 返回的记录的类型为ID, 反之则为引用 */ createBookmark:function (serialize, same) { var endNode, startNode = this.document.createElement('span'); startNode.style.cssText = 'display:none;line-height:0px;'; startNode.appendChild(this.document.createTextNode('\u200D')); startNode.id = '_baidu_bookmark_start_' + (same ? '' : guid++); if (!this.collapsed) { endNode = startNode.cloneNode(true); endNode.id = '_baidu_bookmark_end_' + (same ? '' : guid++); } this.insertNode(startNode); if (endNode) { this.collapse().insertNode(endNode).setEndBefore(endNode); } this.setStartAfter(startNode); return { start:serialize ? startNode.id : startNode, end:endNode ? serialize ? endNode.id : endNode : null, id:serialize } }, /** * 调整当前range的边界到书签位置,并删除该书签对象所标记的位置内的节点 * @method moveToBookmark * @param { BookMark } bookmark createBookmark所创建的标签对象 * @return { UE.dom.Range } 当前range对象 * @see UE.dom.Range:createBookmark(Boolean) */ moveToBookmark:function (bookmark) { var start = bookmark.id ? this.document.getElementById(bookmark.start) : bookmark.start, end = bookmark.end && bookmark.id ? this.document.getElementById(bookmark.end) : bookmark.end; this.setStartBefore(start); domUtils.remove(start); if (end) { this.setEndBefore(end); domUtils.remove(end); } else { this.collapse(true); } return this; }, /** * 调整range的边界,使其"放大"到最近的父节点 * @method enlarge * @remind 会引起选区的变化 * @return { UE.dom.Range } 当前range对象 */ /** * 调整range的边界,使其"放大"到最近的父节点,根据参数 toBlock 的取值, 可以 * 要求扩大之后的父节点是block节点 * @method enlarge * @param { Boolean } toBlock 是否要求扩大之后的父节点必须是block节点 * @return { UE.dom.Range } 当前range对象 */ enlarge:function (toBlock, stopFn) { var isBody = domUtils.isBody, pre, node, tmp = this.document.createTextNode(''); if (toBlock) { node = this.startContainer; if (node.nodeType == 1) { if (node.childNodes[this.startOffset]) { pre = node = node.childNodes[this.startOffset] } else { node.appendChild(tmp); pre = node = tmp; } } else { pre = node; } while (1) { if (domUtils.isBlockElm(node)) { node = pre; while ((pre = node.previousSibling) && !domUtils.isBlockElm(pre)) { node = pre; } this.setStartBefore(node); break; } pre = node; node = node.parentNode; } node = this.endContainer; if (node.nodeType == 1) { if (pre = node.childNodes[this.endOffset]) { node.insertBefore(tmp, pre); } else { node.appendChild(tmp); } pre = node = tmp; } else { pre = node; } while (1) { if (domUtils.isBlockElm(node)) { node = pre; while ((pre = node.nextSibling) && !domUtils.isBlockElm(pre)) { node = pre; } this.setEndAfter(node); break; } pre = node; node = node.parentNode; } if (tmp.parentNode === this.endContainer) { this.endOffset--; } domUtils.remove(tmp); } // 扩展边界到最大 if (!this.collapsed) { while (this.startOffset == 0) { if (stopFn && stopFn(this.startContainer)) { break; } if (isBody(this.startContainer)) { break; } this.setStartBefore(this.startContainer); } while (this.endOffset == (this.endContainer.nodeType == 1 ? this.endContainer.childNodes.length : this.endContainer.nodeValue.length)) { if (stopFn && stopFn(this.endContainer)) { break; } if (isBody(this.endContainer)) { break; } this.setEndAfter(this.endContainer); } } return this; }, enlargeToBlockElm:function(ignoreEnd){ while(!domUtils.isBlockElm(this.startContainer)){ this.setStartBefore(this.startContainer); } if(!ignoreEnd){ while(!domUtils.isBlockElm(this.endContainer)){ this.setEndAfter(this.endContainer); } } return this; }, /** * 调整Range的边界,使其"缩小"到最合适的位置 * @method adjustmentBoundary * @return { UE.dom.Range } 当前range对象 * @see UE.dom.Range:shrinkBoundary() */ adjustmentBoundary:function () { if (!this.collapsed) { while (!domUtils.isBody(this.startContainer) && this.startOffset == this.startContainer[this.startContainer.nodeType == 3 ? 'nodeValue' : 'childNodes'].length && this.startContainer[this.startContainer.nodeType == 3 ? 'nodeValue' : 'childNodes'].length ) { this.setStartAfter(this.startContainer); } while (!domUtils.isBody(this.endContainer) && !this.endOffset && this.endContainer[this.endContainer.nodeType == 3 ? 'nodeValue' : 'childNodes'].length ) { this.setEndBefore(this.endContainer); } } return this; }, /** * 给range选区中的内容添加给定的inline标签 * @method applyInlineStyle * @param { String } tagName 需要添加的标签名 * @example * ```html *

    xxxx[xxxx]x

    ==> range.applyInlineStyle("strong") ==>

    xxxx[xxxx]x

    * ``` */ /** * 给range选区中的内容添加给定的inline标签, 并且为标签附加上一些初始化属性。 * @method applyInlineStyle * @param { String } tagName 需要添加的标签名 * @param { Object } attrs 跟随新添加的标签的属性 * @return { UE.dom.Range } 当前选区 * @example * ```html *

    xxxx[xxxx]x

    * * ==> * * * range.applyInlineStyle("strong",{"style":"font-size:12px"}) * * ==> * *

    xxxx[xxxx]x

    * ``` */ applyInlineStyle:function (tagName, attrs, list) { if (this.collapsed)return this; this.trimBoundary().enlarge(false, function (node) { return node.nodeType == 1 && domUtils.isBlockElm(node) }).adjustmentBoundary(); var bookmark = this.createBookmark(), end = bookmark.end, filterFn = function (node) { return node.nodeType == 1 ? node.tagName.toLowerCase() != 'br' : !domUtils.isWhitespace(node); }, current = domUtils.getNextDomNode(bookmark.start, false, filterFn), node, pre, range = this.cloneRange(); while (current && (domUtils.getPosition(current, end) & domUtils.POSITION_PRECEDING)) { if (current.nodeType == 3 || dtd[tagName][current.tagName]) { range.setStartBefore(current); node = current; while (node && (node.nodeType == 3 || dtd[tagName][node.tagName]) && node !== end) { pre = node; node = domUtils.getNextDomNode(node, node.nodeType == 1, null, function (parent) { return dtd[tagName][parent.tagName]; }); } var frag = range.setEndAfter(pre).extractContents(), elm; if (list && list.length > 0) { var level, top; top = level = list[0].cloneNode(false); for (var i = 1, ci; ci = list[i++];) { level.appendChild(ci.cloneNode(false)); level = level.firstChild; } elm = level; } else { elm = range.document.createElement(tagName); } if (attrs) { domUtils.setAttributes(elm, attrs); } elm.appendChild(frag); range.insertNode(list ? top : elm); //处理下滑线在a上的情况 var aNode; if (tagName == 'span' && attrs.style && /text\-decoration/.test(attrs.style) && (aNode = domUtils.findParentByTagName(elm, 'a', true))) { domUtils.setAttributes(aNode, attrs); domUtils.remove(elm, true); elm = aNode; } else { domUtils.mergeSibling(elm); domUtils.clearEmptySibling(elm); } //去除子节点相同的 domUtils.mergeChild(elm, attrs); current = domUtils.getNextDomNode(elm, false, filterFn); domUtils.mergeToParent(elm); if (node === end) { break; } } else { current = domUtils.getNextDomNode(current, true, filterFn); } } return this.moveToBookmark(bookmark); }, /** * 移除当前选区内指定的inline标签,但保留其中的内容 * @method removeInlineStyle * @param { String } tagName 需要移除的标签名 * @return { UE.dom.Range } 当前的range对象 * @example * ```html * xx[xxxxyyyzz]z => range.removeInlineStyle(["em"]) => xx[xxxxyyyzz]z * ``` */ /** * 移除当前选区内指定的一组inline标签,但保留其中的内容 * @method removeInlineStyle * @param { Array } tagNameArr 需要移除的标签名的数组 * @return { UE.dom.Range } 当前的range对象 * @see UE.dom.Range:removeInlineStyle(String) */ removeInlineStyle:function (tagNames) { if (this.collapsed)return this; tagNames = utils.isArray(tagNames) ? tagNames : [tagNames]; this.shrinkBoundary().adjustmentBoundary(); var start = this.startContainer, end = this.endContainer; while (1) { if (start.nodeType == 1) { if (utils.indexOf(tagNames, start.tagName.toLowerCase()) > -1) { break; } if (start.tagName.toLowerCase() == 'body') { start = null; break; } } start = start.parentNode; } while (1) { if (end.nodeType == 1) { if (utils.indexOf(tagNames, end.tagName.toLowerCase()) > -1) { break; } if (end.tagName.toLowerCase() == 'body') { end = null; break; } } end = end.parentNode; } var bookmark = this.createBookmark(), frag, tmpRange; if (start) { tmpRange = this.cloneRange().setEndBefore(bookmark.start).setStartBefore(start); frag = tmpRange.extractContents(); tmpRange.insertNode(frag); domUtils.clearEmptySibling(start, true); start.parentNode.insertBefore(bookmark.start, start); } if (end) { tmpRange = this.cloneRange().setStartAfter(bookmark.end).setEndAfter(end); frag = tmpRange.extractContents(); tmpRange.insertNode(frag); domUtils.clearEmptySibling(end, false, true); end.parentNode.insertBefore(bookmark.end, end.nextSibling); } var current = domUtils.getNextDomNode(bookmark.start, false, function (node) { return node.nodeType == 1; }), next; while (current && current !== bookmark.end) { next = domUtils.getNextDomNode(current, true, function (node) { return node.nodeType == 1; }); if (utils.indexOf(tagNames, current.tagName.toLowerCase()) > -1) { domUtils.remove(current, true); } current = next; } return this.moveToBookmark(bookmark); }, /** * 获取当前选中的自闭合的节点 * @method getClosedNode * @return { Node | NULL } 如果当前选中的是自闭合节点, 则返回该节点, 否则返回NULL */ getClosedNode:function () { var node; if (!this.collapsed) { var range = this.cloneRange().adjustmentBoundary().shrinkBoundary(); if (selectOneNode(range)) { var child = range.startContainer.childNodes[range.startOffset]; if (child && child.nodeType == 1 && (dtd.$empty[child.tagName] || dtd.$nonChild[child.tagName])) { node = child; } } } return node; }, /** * 在页面上高亮range所表示的选区 * @method select * @return { UE.dom.Range } 返回当前Range对象 */ //这里不区分ie9以上,trace:3824 select:browser.ie ? function (noFillData, textRange) { var nativeRange; if (!this.collapsed) this.shrinkBoundary(); var node = this.getClosedNode(); if (node && !textRange) { try { nativeRange = this.document.body.createControlRange(); nativeRange.addElement(node); nativeRange.select(); } catch (e) {} return this; } var bookmark = this.createBookmark(), start = bookmark.start, end; nativeRange = this.document.body.createTextRange(); nativeRange.moveToElementText(start); nativeRange.moveStart('character', 1); if (!this.collapsed) { var nativeRangeEnd = this.document.body.createTextRange(); end = bookmark.end; nativeRangeEnd.moveToElementText(end); nativeRange.setEndPoint('EndToEnd', nativeRangeEnd); } else { if (!noFillData && this.startContainer.nodeType != 3) { //使用|x固定住光标 var tmpText = this.document.createTextNode(fillChar), tmp = this.document.createElement('span'); tmp.appendChild(this.document.createTextNode(fillChar)); start.parentNode.insertBefore(tmp, start); start.parentNode.insertBefore(tmpText, start); //当点b,i,u时,不能清除i上边的b removeFillData(this.document, tmpText); fillData = tmpText; mergeSibling(tmp, 'previousSibling'); mergeSibling(start, 'nextSibling'); nativeRange.moveStart('character', -1); nativeRange.collapse(true); } } this.moveToBookmark(bookmark); tmp && domUtils.remove(tmp); //IE在隐藏状态下不支持range操作,catch一下 try { nativeRange.select(); } catch (e) { } return this; } : function (notInsertFillData) { function checkOffset(rng){ function check(node,offset,dir){ if(node.nodeType == 3 && node.nodeValue.length < offset){ rng[dir + 'Offset'] = node.nodeValue.length } } check(rng.startContainer,rng.startOffset,'start'); check(rng.endContainer,rng.endOffset,'end'); } var win = domUtils.getWindow(this.document), sel = win.getSelection(), txtNode; //FF下关闭自动长高时滚动条在关闭dialog时会跳 //ff下如果不body.focus将不能定位闭合光标到编辑器内 browser.gecko ? this.document.body.focus() : win.focus(); if (sel) { sel.removeAllRanges(); // trace:870 chrome/safari后边是br对于闭合得range不能定位 所以去掉了判断 // this.startContainer.nodeType != 3 &&! ((child = this.startContainer.childNodes[this.startOffset]) && child.nodeType == 1 && child.tagName == 'BR' if (this.collapsed && !notInsertFillData) { // //opear如果没有节点接着,原生的不能够定位,不能在body的第一级插入空白节点 // if (notInsertFillData && browser.opera && !domUtils.isBody(this.startContainer) && this.startContainer.nodeType == 1) { // var tmp = this.document.createTextNode(''); // this.insertNode(tmp).setStart(tmp, 0).collapse(true); // } // //处理光标落在文本节点的情况 //处理以下的情况 //|xxxx //xxxx|xxxx //xxxx| var start = this.startContainer,child = start; if(start.nodeType == 1){ child = start.childNodes[this.startOffset]; } if( !(start.nodeType == 3 && this.startOffset) && (child ? (!child.previousSibling || child.previousSibling.nodeType != 3) : (!start.lastChild || start.lastChild.nodeType != 3) ) ){ txtNode = this.document.createTextNode(fillChar); //跟着前边走 this.insertNode(txtNode); removeFillData(this.document, txtNode); mergeSibling(txtNode, 'previousSibling'); mergeSibling(txtNode, 'nextSibling'); fillData = txtNode; this.setStart(txtNode, browser.webkit ? 1 : 0).collapse(true); } } var nativeRange = this.document.createRange(); if(this.collapsed && browser.opera && this.startContainer.nodeType == 1){ var child = this.startContainer.childNodes[this.startOffset]; if(!child){ //往前靠拢 child = this.startContainer.lastChild; if( child && domUtils.isBr(child)){ this.setStartBefore(child).collapse(true); } }else{ //向后靠拢 while(child && domUtils.isBlockElm(child)){ if(child.nodeType == 1 && child.childNodes[0]){ child = child.childNodes[0] }else{ break; } } child && this.setStartBefore(child).collapse(true) } } //是createAddress最后一位算的不准,现在这里进行微调 checkOffset(this); nativeRange.setStart(this.startContainer, this.startOffset); nativeRange.setEnd(this.endContainer, this.endOffset); sel.addRange(nativeRange); } return this; }, /** * 滚动到当前range开始的位置 * @method scrollToView * @param { Window } win 当前range对象所属的window对象 * @return { UE.dom.Range } 当前Range对象 */ /** * 滚动到距离当前range开始位置 offset 的位置处 * @method scrollToView * @param { Window } win 当前range对象所属的window对象 * @param { Number } offset 距离range开始位置处的偏移量, 如果为正数, 则向下偏移, 反之, 则向上偏移 * @return { UE.dom.Range } 当前Range对象 */ scrollToView:function (win, offset) { win = win ? window : domUtils.getWindow(this.document); var me = this, span = me.document.createElement('span'); //trace:717 span.innerHTML = ' '; me.cloneRange().insertNode(span); domUtils.scrollToView(span, win, offset); domUtils.remove(span); return me; }, /** * 判断当前选区内容是否占位符 * @private * @method inFillChar * @return { Boolean } 如果是占位符返回true,否则返回false */ inFillChar : function(){ var start = this.startContainer; if(this.collapsed && start.nodeType == 3 && start.nodeValue.replace(new RegExp('^' + domUtils.fillChar),'').length + 1 == start.nodeValue.length ){ return true; } return false; }, /** * 保存 * @method createAddress * @private * @return { Boolean } 返回开始和结束的位置 * @example * ```html * *

    * aaaa * * * bbbb * * *

    * * * * ``` */ createAddress : function(ignoreEnd,ignoreTxt){ var addr = {},me = this; function getAddress(isStart){ var node = isStart ? me.startContainer : me.endContainer; var parents = domUtils.findParents(node,true,function(node){return !domUtils.isBody(node)}), addrs = []; for(var i = 0,ci;ci = parents[i++];){ addrs.push(domUtils.getNodeIndex(ci,ignoreTxt)); } var firstIndex = 0; if(ignoreTxt){ if(node.nodeType == 3){ var tmpNode = node.previousSibling; while(tmpNode && tmpNode.nodeType == 3){ firstIndex += tmpNode.nodeValue.replace(fillCharReg,'').length; tmpNode = tmpNode.previousSibling; } firstIndex += (isStart ? me.startOffset : me.endOffset)// - (fillCharReg.test(node.nodeValue) ? 1 : 0 ) }else{ node = node.childNodes[ isStart ? me.startOffset : me.endOffset]; if(node){ firstIndex = domUtils.getNodeIndex(node,ignoreTxt); }else{ node = isStart ? me.startContainer : me.endContainer; var first = node.firstChild; while(first){ if(domUtils.isFillChar(first)){ first = first.nextSibling; continue; } firstIndex++; if(first.nodeType == 3){ while( first && first.nodeType == 3){ first = first.nextSibling; } }else{ first = first.nextSibling; } } } } }else{ firstIndex = isStart ? domUtils.isFillChar(node) ? 0 : me.startOffset : me.endOffset } if(firstIndex < 0){ firstIndex = 0; } addrs.push(firstIndex); return addrs; } addr.startAddress = getAddress(true); if(!ignoreEnd){ addr.endAddress = me.collapsed ? [].concat(addr.startAddress) : getAddress(); } return addr; }, /** * 保存 * @method createAddress * @private * @return { Boolean } 返回开始和结束的位置 * @example * ```html * *

    * aaaa * * * bbbb * * *

    * * * * ``` */ moveToAddress : function(addr,ignoreEnd){ var me = this; function getNode(address,isStart){ var tmpNode = me.document.body, parentNode,offset; for(var i= 0,ci,l=address.length;i * * * * * * * * * ``` */ /** * 遍历range内的节点。 * 每当遍历一个节点时, 都会执行参数项 doFn 指定的函数, 该函数的接受当前遍历的节点 * 作为其参数。 * 可以通过参数项 filterFn 来指定一个过滤器, 只有符合该过滤器过滤规则的节点才会触 * 发doFn函数的执行 * @method traversal * @param { Function } doFn 对每个遍历的节点要执行的方法, 该方法接受当前遍历的节点作为其参数 * @param { Function } filterFn 过滤器, 该函数接受当前遍历的节点作为参数, 如果该节点满足过滤 * 规则, 请返回true, 该节点会触发doFn, 否则, 请返回false, 则该节点不 * 会触发doFn。 * @return { UE.dom.Range } 当前range对象 * @see UE.dom.Range:traversal(Function) * @example * ```html * * * * * * * * * * * ``` */ traversal:function(doFn,filterFn){ if (this.collapsed) return this; var bookmark = this.createBookmark(), end = bookmark.end, current = domUtils.getNextDomNode(bookmark.start, false, filterFn); while (current && current !== end && (domUtils.getPosition(current, end) & domUtils.POSITION_PRECEDING)) { var tmpNode = domUtils.getNextDomNode(current,false,filterFn); doFn(current); current = tmpNode; } return this.moveToBookmark(bookmark); } }; })(); // core/Selection.js /** * 选集 * @file * @module UE.dom * @class Selection * @since 1.2.6.1 */ /** * 选区集合 * @unfile * @module UE.dom * @class Selection */ (function () { function getBoundaryInformation( range, start ) { var getIndex = domUtils.getNodeIndex; range = range.duplicate(); range.collapse( start ); var parent = range.parentElement(); //如果节点里没有子节点,直接退出 if ( !parent.hasChildNodes() ) { return {container:parent, offset:0}; } var siblings = parent.children, child, testRange = range.duplicate(), startIndex = 0, endIndex = siblings.length - 1, index = -1, distance; while ( startIndex <= endIndex ) { index = Math.floor( (startIndex + endIndex) / 2 ); child = siblings[index]; testRange.moveToElementText( child ); var position = testRange.compareEndPoints( 'StartToStart', range ); if ( position > 0 ) { endIndex = index - 1; } else if ( position < 0 ) { startIndex = index + 1; } else { //trace:1043 return {container:parent, offset:getIndex( child )}; } } if ( index == -1 ) { testRange.moveToElementText( parent ); testRange.setEndPoint( 'StartToStart', range ); distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length; siblings = parent.childNodes; if ( !distance ) { child = siblings[siblings.length - 1]; return {container:child, offset:child.nodeValue.length}; } var i = siblings.length; while ( distance > 0 ){ distance -= siblings[ --i ].nodeValue.length; } return {container:siblings[i], offset:-distance}; } testRange.collapse( position > 0 ); testRange.setEndPoint( position > 0 ? 'StartToStart' : 'EndToStart', range ); distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length; if ( !distance ) { return dtd.$empty[child.tagName] || dtd.$nonChild[child.tagName] ? {container:parent, offset:getIndex( child ) + (position > 0 ? 0 : 1)} : {container:child, offset:position > 0 ? 0 : child.childNodes.length} } while ( distance > 0 ) { try { var pre = child; child = child[position > 0 ? 'previousSibling' : 'nextSibling']; distance -= child.nodeValue.length; } catch ( e ) { return {container:parent, offset:getIndex( pre )}; } } return {container:child, offset:position > 0 ? -distance : child.nodeValue.length + distance} } /** * 将ieRange转换为Range对象 * @param {Range} ieRange ieRange对象 * @param {Range} range Range对象 * @return {Range} range 返回转换后的Range对象 */ function transformIERangeToRange( ieRange, range ) { if ( ieRange.item ) { range.selectNode( ieRange.item( 0 ) ); } else { var bi = getBoundaryInformation( ieRange, true ); range.setStart( bi.container, bi.offset ); if ( ieRange.compareEndPoints( 'StartToEnd', ieRange ) != 0 ) { bi = getBoundaryInformation( ieRange, false ); range.setEnd( bi.container, bi.offset ); } } return range; } /** * 获得ieRange * @param {Selection} sel Selection对象 * @return {ieRange} 得到ieRange */ function _getIERange( sel ) { var ieRange; //ie下有可能报错 try { ieRange = sel.getNative().createRange(); } catch ( e ) { return null; } var el = ieRange.item ? ieRange.item( 0 ) : ieRange.parentElement(); if ( ( el.ownerDocument || el ) === sel.document ) { return ieRange; } return null; } var Selection = dom.Selection = function ( doc ) { var me = this, iframe; me.document = doc; if ( browser.ie9below ) { iframe = domUtils.getWindow( doc ).frameElement; domUtils.on( iframe, 'beforedeactivate', function () { me._bakIERange = me.getIERange(); } ); domUtils.on( iframe, 'activate', function () { try { if ( !_getIERange( me ) && me._bakIERange ) { me._bakIERange.select(); } } catch ( ex ) { } me._bakIERange = null; } ); } iframe = doc = null; }; Selection.prototype = { rangeInBody : function(rng,txtRange){ var node = browser.ie9below || txtRange ? rng.item ? rng.item() : rng.parentElement() : rng.startContainer; return node === this.document.body || domUtils.inDoc(node,this.document); }, /** * 获取原生seleciton对象 * @method getNative * @return { Object } 获得selection对象 * @example * ```javascript * editor.selection.getNative(); * ``` */ getNative:function () { var doc = this.document; try { return !doc ? null : browser.ie9below ? doc.selection : domUtils.getWindow( doc ).getSelection(); } catch ( e ) { return null; } }, /** * 获得ieRange * @method getIERange * @return { Object } 返回ie原生的Range * @example * ```javascript * editor.selection.getIERange(); * ``` */ getIERange:function () { var ieRange = _getIERange( this ); if ( !ieRange ) { if ( this._bakIERange ) { return this._bakIERange; } } return ieRange; }, /** * 缓存当前选区的range和选区的开始节点 * @method cache */ cache:function () { this.clear(); this._cachedRange = this.getRange(); this._cachedStartElement = this.getStart(); this._cachedStartElementPath = this.getStartElementPath(); }, /** * 获取选区开始位置的父节点到body * @method getStartElementPath * @return { Array } 返回父节点集合 * @example * ```javascript * editor.selection.getStartElementPath(); * ``` */ getStartElementPath:function () { if ( this._cachedStartElementPath ) { return this._cachedStartElementPath; } var start = this.getStart(); if ( start ) { return domUtils.findParents( start, true, null, true ) } return []; }, /** * 清空缓存 * @method clear */ clear:function () { this._cachedStartElementPath = this._cachedRange = this._cachedStartElement = null; }, /** * 编辑器是否得到了选区 * @method isFocus */ isFocus:function () { try { if(browser.ie9below){ var nativeRange = _getIERange(this); return !!(nativeRange && this.rangeInBody(nativeRange)); }else{ return !!this.getNative().rangeCount; } } catch ( e ) { return false; } }, /** * 获取选区对应的Range * @method getRange * @return { Object } 得到Range对象 * @example * ```javascript * editor.selection.getRange(); * ``` */ getRange:function () { var me = this; function optimze( range ) { var child = me.document.body.firstChild, collapsed = range.collapsed; while ( child && child.firstChild ) { range.setStart( child, 0 ); child = child.firstChild; } if ( !range.startContainer ) { range.setStart( me.document.body, 0 ) } if ( collapsed ) { range.collapse( true ); } } if ( me._cachedRange != null ) { return this._cachedRange; } var range = new baidu.editor.dom.Range( me.document ); if ( browser.ie9below ) { var nativeRange = me.getIERange(); if ( nativeRange ) { //备份的_bakIERange可能已经实效了,dom树发生了变化比如从源码模式切回来,所以try一下,实效就放到body开始位置 try{ transformIERangeToRange( nativeRange, range ); }catch(e){ optimze( range ); } } else { optimze( range ); } } else { var sel = me.getNative(); if ( sel && sel.rangeCount ) { var firstRange = sel.getRangeAt( 0 ); var lastRange = sel.getRangeAt( sel.rangeCount - 1 ); range.setStart( firstRange.startContainer, firstRange.startOffset ).setEnd( lastRange.endContainer, lastRange.endOffset ); if ( range.collapsed && domUtils.isBody( range.startContainer ) && !range.startOffset ) { optimze( range ); } } else { //trace:1734 有可能已经不在dom树上了,标识的节点 if ( this._bakRange && domUtils.inDoc( this._bakRange.startContainer, this.document ) ){ return this._bakRange; } optimze( range ); } } return this._bakRange = range; }, /** * 获取开始元素,用于状态反射 * @method getStart * @return { Element } 获得开始元素 * @example * ```javascript * editor.selection.getStart(); * ``` */ getStart:function () { if ( this._cachedStartElement ) { return this._cachedStartElement; } var range = browser.ie9below ? this.getIERange() : this.getRange(), tmpRange, start, tmp, parent; if ( browser.ie9below ) { if ( !range ) { //todo 给第一个值可能会有问题 return this.document.body.firstChild; } //control元素 if ( range.item ){ return range.item( 0 ); } tmpRange = range.duplicate(); //修正ie下x[xx] 闭合后 x|xx tmpRange.text.length > 0 && tmpRange.moveStart( 'character', 1 ); tmpRange.collapse( 1 ); start = tmpRange.parentElement(); parent = tmp = range.parentElement(); while ( tmp = tmp.parentNode ) { if ( tmp == start ) { start = parent; break; } } } else { range.shrinkBoundary(); start = range.startContainer; if ( start.nodeType == 1 && start.hasChildNodes() ){ start = start.childNodes[Math.min( start.childNodes.length - 1, range.startOffset )]; } if ( start.nodeType == 3 ){ return start.parentNode; } } return start; }, /** * 得到选区中的文本 * @method getText * @return { String } 选区中包含的文本 * @example * ```javascript * editor.selection.getText(); * ``` */ getText:function () { var nativeSel, nativeRange; if ( this.isFocus() && (nativeSel = this.getNative()) ) { nativeRange = browser.ie9below ? nativeSel.createRange() : nativeSel.getRangeAt( 0 ); return browser.ie9below ? nativeRange.text : nativeRange.toString(); } return ''; }, /** * 清除选区 * @method clearRange * @example * ```javascript * editor.selection.clearRange(); * ``` */ clearRange : function(){ this.getNative()[browser.ie9below ? 'empty' : 'removeAllRanges'](); } }; })(); // core/Editor.js /** * 编辑器主类,包含编辑器提供的大部分公用接口 * @file * @module UE * @class Editor * @since 1.2.6.1 */ /** * UEditor公用空间,UEditor所有的功能都挂载在该空间下 * @unfile * @module UE */ /** * UEditor的核心类,为用户提供与编辑器交互的接口。 * @unfile * @module UE * @class Editor */ (function () { var uid = 0, _selectionChangeTimer; /** * 获取编辑器的html内容,赋值到编辑器所在表单的textarea文本域里面 * @private * @method setValue * @param { UE.Editor } editor 编辑器事例 */ function setValue(form, editor) { var textarea; if (editor.textarea) { if (utils.isString(editor.textarea)) { for (var i = 0, ti, tis = domUtils.getElementsByTagName(form, 'textarea'); ti = tis[i++];) { if (ti.id == 'ueditor_textarea_' + editor.options.textarea) { textarea = ti; break; } } } else { textarea = editor.textarea; } } if (!textarea) { form.appendChild(textarea = domUtils.createElement(document, 'textarea', { 'name': editor.options.textarea, 'id': 'ueditor_textarea_' + editor.options.textarea, 'style': "display:none" })); //不要产生多个textarea editor.textarea = textarea; } !textarea.getAttribute('name') && textarea.setAttribute('name', editor.options.textarea ); textarea.value = editor.hasContents() ? (editor.options.allHtmlEnabled ? editor.getAllHtml() : editor.getContent(null, null, true)) : '' } function loadPlugins(me){ //初始化插件 for (var pi in UE.plugins) { UE.plugins[pi].call(me); } } function checkCurLang(I18N){ for(var lang in I18N){ return lang } } function langReadied(me){ me.langIsReady = true; me.fireEvent("langReady"); } /** * 编辑器准备就绪后会触发该事件 * @module UE * @class Editor * @event ready * @remind render方法执行完成之后,会触发该事件 * @remind * @example * ```javascript * editor.addListener( 'ready', function( editor ) { * editor.execCommand( 'focus' ); //编辑器家在完成后,让编辑器拿到焦点 * } ); * ``` */ /** * 执行destroy方法,会触发该事件 * @module UE * @class Editor * @event destroy * @see UE.Editor:destroy() */ /** * 执行reset方法,会触发该事件 * @module UE * @class Editor * @event reset * @see UE.Editor:reset() */ /** * 执行focus方法,会触发该事件 * @module UE * @class Editor * @event focus * @see UE.Editor:focus(Boolean) */ /** * 语言加载完成会触发该事件 * @module UE * @class Editor * @event langReady */ /** * 运行命令之后会触发该命令 * @module UE * @class Editor * @event beforeExecCommand */ /** * 运行命令之后会触发该命令 * @module UE * @class Editor * @event afterExecCommand */ /** * 运行命令之前会触发该命令 * @module UE * @class Editor * @event firstBeforeExecCommand */ /** * 在getContent方法执行之前会触发该事件 * @module UE * @class Editor * @event beforeGetContent * @see UE.Editor:getContent() */ /** * 在getContent方法执行之后会触发该事件 * @module UE * @class Editor * @event afterGetContent * @see UE.Editor:getContent() */ /** * 在getAllHtml方法执行时会触发该事件 * @module UE * @class Editor * @event getAllHtml * @see UE.Editor:getAllHtml() */ /** * 在setContent方法执行之前会触发该事件 * @module UE * @class Editor * @event beforeSetContent * @see UE.Editor:setContent(String) */ /** * 在setContent方法执行之后会触发该事件 * @module UE * @class Editor * @event afterSetContent * @see UE.Editor:setContent(String) */ /** * 每当编辑器内部选区发生改变时,将触发该事件 * @event selectionchange * @warning 该事件的触发非常频繁,不建议在该事件的处理过程中做重量级的处理 * @example * ```javascript * editor.addListener( 'selectionchange', function( editor ) { * console.log('选区发生改变'); * } */ /** * 在所有selectionchange的监听函数执行之前,会触发该事件 * @module UE * @class Editor * @event beforeSelectionChange * @see UE.Editor:selectionchange */ /** * 在所有selectionchange的监听函数执行完之后,会触发该事件 * @module UE * @class Editor * @event afterSelectionChange * @see UE.Editor:selectionchange */ /** * 编辑器内容发生改变时会触发该事件 * @module UE * @class Editor * @event contentChange */ /** * 以默认参数构建一个编辑器实例 * @constructor * @remind 通过 改构造方法实例化的编辑器,不带ui层.需要render到一个容器,编辑器实例才能正常渲染到页面 * @example * ```javascript * var editor = new UE.Editor(); * editor.execCommand('blod'); * ``` * @see UE.Config */ /** * 以给定的参数集合创建一个编辑器实例,对于未指定的参数,将应用默认参数。 * @constructor * @remind 通过 改构造方法实例化的编辑器,不带ui层.需要render到一个容器,编辑器实例才能正常渲染到页面 * @param { Object } setting 创建编辑器的参数 * @example * ```javascript * var editor = new UE.Editor(); * editor.execCommand('blod'); * ``` * @see UE.Config */ var Editor = UE.Editor = function (options) { var me = this; me.uid = uid++; EventBase.call(me); me.commands = {}; me.options = utils.extend(utils.clone(options || {}), UEDITOR_CONFIG, true); me.shortcutkeys = {}; me.inputRules = []; me.outputRules = []; //设置默认的常用属性 me.setOpt(Editor.defaultOptions(me)); /* 尝试异步加载后台配置 */ me.loadServerConfig(); if(!utils.isEmptyObject(UE.I18N)){ //修改默认的语言类型 me.options.lang = checkCurLang(UE.I18N); UE.plugin.load(me); langReadied(me); }else{ utils.loadFile(document, { src: me.options.langPath + me.options.lang + "/" + me.options.lang + ".js", tag: "script", type: "text/javascript", defer: "defer" }, function () { UE.plugin.load(me); langReadied(me); }); } UE.instants['ueditorInstant' + me.uid] = me; }; Editor.prototype = { registerCommand : function(name,obj){ this.commands[name] = obj; }, /** * 编辑器对外提供的监听ready事件的接口, 通过调用该方法,达到的效果与监听ready事件是一致的 * @method ready * @param { Function } fn 编辑器ready之后所执行的回调, 如果在注册事件之前编辑器已经ready,将会 * 立即触发该回调。 * @remind 需要等待编辑器加载完成后才能执行的代码,可以使用该方法传入 * @example * ```javascript * editor.ready( function( editor ) { * editor.setContent('初始化完毕'); * } ); * ``` * @see UE.Editor.event:ready */ ready: function (fn) { var me = this; if (fn) { me.isReady ? fn.apply(me) : me.addListener('ready', fn); } }, /** * 该方法是提供给插件里面使用,设置配置项默认值 * @method setOpt * @warning 三处设置配置项的优先级: 实例化时传入参数 > setOpt()设置 > config文件里设置 * @warning 该方法仅供编辑器插件内部和编辑器初始化时调用,其他地方不能调用。 * @param { String } key 编辑器的可接受的选项名称 * @param { * } val 该选项可接受的值 * @example * ```javascript * editor.setOpt( 'initContent', '欢迎使用编辑器' ); * ``` */ /** * 该方法是提供给插件里面使用,以{key:value}集合的方式设置插件内用到的配置项默认值 * @method setOpt * @warning 三处设置配置项的优先级: 实例化时传入参数 > setOpt()设置 > config文件里设置 * @warning 该方法仅供编辑器插件内部和编辑器初始化时调用,其他地方不能调用。 * @param { Object } options 将要设置的选项的键值对对象 * @example * ```javascript * editor.setOpt( { * 'initContent': '欢迎使用编辑器' * } ); * ``` */ setOpt: function (key, val) { var obj = {}; if (utils.isString(key)) { obj[key] = val } else { obj = key; } utils.extend(this.options, obj, true); }, getOpt:function(key){ return this.options[key] }, /** * 销毁编辑器实例,使用textarea代替 * @method destroy * @example * ```javascript * editor.destroy(); * ``` */ destroy: function () { var me = this; me.fireEvent('destroy'); var container = me.container.parentNode; var textarea = me.textarea; if (!textarea) { textarea = document.createElement('textarea'); container.parentNode.insertBefore(textarea, container); } else { textarea.style.display = '' } textarea.style.width = me.iframe.offsetWidth + 'px'; textarea.style.height = me.iframe.offsetHeight + 'px'; textarea.value = me.getContent(); textarea.id = me.key; container.innerHTML = ''; domUtils.remove(container); var key = me.key; //trace:2004 for (var p in me) { if (me.hasOwnProperty(p)) { delete this[p]; } } UE.delEditor(key); }, /** * 渲染编辑器的DOM到指定容器 * @method render * @param { String } containerId 指定一个容器ID * @remind 执行该方法,会触发ready事件 * @warning 必须且只能调用一次 */ /** * 渲染编辑器的DOM到指定容器 * @method render * @param { Element } containerDom 直接指定容器对象 * @remind 执行该方法,会触发ready事件 * @warning 必须且只能调用一次 */ render: function (container) { var me = this, options = me.options, getStyleValue=function(attr){ return parseInt(domUtils.getComputedStyle(container,attr)); }; if (utils.isString(container)) { container = document.getElementById(container); } if (container) { if(options.initialFrameWidth){ options.minFrameWidth = options.initialFrameWidth }else{ options.minFrameWidth = options.initialFrameWidth = container.offsetWidth; } if(options.initialFrameHeight){ options.minFrameHeight = options.initialFrameHeight }else{ options.initialFrameHeight = options.minFrameHeight = container.offsetHeight; } container.style.width = /%$/.test(options.initialFrameWidth) ? '100%' : options.initialFrameWidth- getStyleValue("padding-left")- getStyleValue("padding-right") +'px'; container.style.height = /%$/.test(options.initialFrameHeight) ? '100%' : options.initialFrameHeight - getStyleValue("padding-top")- getStyleValue("padding-bottom") +'px'; container.style.zIndex = options.zIndex; var html = ( ie && browser.version < 9 ? '' : '') + '' + '' + ( options.iframeCssUrl ? '' : '' ) + (options.initialStyle ? '' : '') + '' + ''; container.appendChild(domUtils.createElement(document, 'iframe', { id: 'ueditor_' + me.uid, width: "100%", height: "100%", frameborder: "0", //先注释掉了,加的原因忘记了,但开启会直接导致全屏模式下内容多时不会出现滚动条 // scrolling :'no', src: 'javascript:void(function(){document.open();' + (options.customDomain && document.domain != location.hostname ? 'document.domain="' + document.domain + '";' : '') + 'document.write("' + html + '");document.close();}())' })); container.style.overflow = 'hidden'; //解决如果是给定的百分比,会导致高度算不对的问题 setTimeout(function(){ if( /%$/.test(options.initialFrameWidth)){ options.minFrameWidth = options.initialFrameWidth = container.offsetWidth; //如果这里给定宽度,会导致ie在拖动窗口大小时,编辑区域不随着变化 // container.style.width = options.initialFrameWidth + 'px'; } if(/%$/.test(options.initialFrameHeight)){ options.minFrameHeight = options.initialFrameHeight = container.offsetHeight; container.style.height = options.initialFrameHeight + 'px'; } }) } }, /** * 编辑器初始化 * @method _setup * @private * @param { Element } doc 编辑器Iframe中的文档对象 */ _setup: function (doc) { var me = this, options = me.options; if (ie) { doc.body.disabled = true; doc.body.contentEditable = true; doc.body.disabled = false; } else { doc.body.contentEditable = true; } doc.body.spellcheck = false; me.document = doc; me.window = doc.defaultView || doc.parentWindow; me.iframe = me.window.frameElement; me.body = doc.body; me.selection = new dom.Selection(doc); //gecko初始化就能得到range,无法判断isFocus了 var geckoSel; if (browser.gecko && (geckoSel = this.selection.getNative())) { geckoSel.removeAllRanges(); } this._initEvents(); //为form提交提供一个隐藏的textarea for (var form = this.iframe.parentNode; !domUtils.isBody(form); form = form.parentNode) { if (form.tagName == 'FORM') { me.form = form; if(me.options.autoSyncData){ domUtils.on(me.window,'blur',function(){ setValue(form,me); }); }else{ domUtils.on(form, 'submit', function () { setValue(this, me); }); } break; } } if (options.initialContent) { if (options.autoClearinitialContent) { var oldExecCommand = me.execCommand; me.execCommand = function () { me.fireEvent('firstBeforeExecCommand'); return oldExecCommand.apply(me, arguments); }; this._setDefaultContent(options.initialContent); } else this.setContent(options.initialContent, false, true); } //编辑器不能为空内容 if (domUtils.isEmptyNode(me.body)) { me.body.innerHTML = '

    ' + (browser.ie ? '' : '
    ') + '

    '; } //如果要求focus, 就把光标定位到内容开始 if (options.focus) { setTimeout(function () { me.focus(me.options.focusInEnd); //如果自动清除开着,就不需要做selectionchange; !me.options.autoClearinitialContent && me._selectionChange(); }, 0); } if (!me.container) { me.container = this.iframe.parentNode; } if (options.fullscreen && me.ui) { me.ui.setFullScreen(true); } try { me.document.execCommand('2D-position', false, false); } catch (e) { } try { me.document.execCommand('enableInlineTableEditing', false, false); } catch (e) { } try { me.document.execCommand('enableObjectResizing', false, false); } catch (e) { } //挂接快捷键 me._bindshortcutKeys(); me.isReady = 1; me.fireEvent('ready'); options.onready && options.onready.call(me); if (!browser.ie9below) { domUtils.on(me.window, ['blur', 'focus'], function (e) { //chrome下会出现alt+tab切换时,导致选区位置不对 if (e.type == 'blur') { me._bakRange = me.selection.getRange(); try { me._bakNativeRange = me.selection.getNative().getRangeAt(0); me.selection.getNative().removeAllRanges(); } catch (e) { me._bakNativeRange = null; } } else { try { me._bakRange && me._bakRange.select(); } catch (e) { } } }); } //trace:1518 ff3.6body不够寛,会导致点击空白处无法获得焦点 if (browser.gecko && browser.version <= 10902) { //修复ff3.6初始化进来,不能点击获得焦点 me.body.contentEditable = false; setTimeout(function () { me.body.contentEditable = true; }, 100); setInterval(function () { me.body.style.height = me.iframe.offsetHeight - 20 + 'px' }, 100) } !options.isShow && me.setHide(); options.readonly && me.setDisabled(); }, /** * 同步数据到编辑器所在的form * 从编辑器的容器节点向上查找form元素,若找到,就同步编辑内容到找到的form里,为提交数据做准备,主要用于是手动提交的情况 * 后台取得数据的键值,使用你容器上的name属性,如果没有就使用参数里的textarea项 * @method sync * @example * ```javascript * editor.sync(); * form.sumbit(); //form变量已经指向了form元素 * ``` */ /** * 根据传入的formId,在页面上查找要同步数据的表单,若找到,就同步编辑内容到找到的form里,为提交数据做准备 * 后台取得数据的键值,该键值默认使用给定的编辑器容器的name属性,如果没有name属性则使用参数项里给定的“textarea”项 * @method sync * @param { String } formID 指定一个要同步数据的form的id,编辑器的数据会同步到你指定form下 */ sync: function (formId) { var me = this, form = formId ? document.getElementById(formId) : domUtils.findParent(me.iframe.parentNode, function (node) { return node.tagName == 'FORM' }, true); form && setValue(form, me); }, /** * 设置编辑器高度 * @method setHeight * @remind 当配置项autoHeightEnabled为真时,该方法无效 * @param { Number } number 设置的高度值,纯数值,不带单位 * @example * ```javascript * editor.setHeight(number); * ``` */ setHeight: function (height,notSetHeight) { if (height !== parseInt(this.iframe.parentNode.style.height)) { this.iframe.parentNode.style.height = height + 'px'; } !notSetHeight && (this.options.minFrameHeight = this.options.initialFrameHeight = height); this.body.style.height = height + 'px'; !notSetHeight && this.trigger('setHeight') }, /** * 为编辑器的编辑命令提供快捷键 * 这个接口是为插件扩展提供的接口,主要是为新添加的插件,如果需要添加快捷键,所提供的接口 * @method addshortcutkey * @param { Object } keyset 命令名和快捷键键值对对象,多个按钮的快捷键用“+”分隔 * @example * ```javascript * editor.addshortcutkey({ * "Bold" : "ctrl+66",//^B * "Italic" : "ctrl+73", //^I * }); * ``` */ /** * 这个接口是为插件扩展提供的接口,主要是为新添加的插件,如果需要添加快捷键,所提供的接口 * @method addshortcutkey * @param { String } cmd 触发快捷键时,响应的命令 * @param { String } keys 快捷键的字符串,多个按钮用“+”分隔 * @example * ```javascript * editor.addshortcutkey("Underline", "ctrl+85"); //^U * ``` */ addshortcutkey: function (cmd, keys) { var obj = {}; if (keys) { obj[cmd] = keys } else { obj = cmd; } utils.extend(this.shortcutkeys, obj) }, /** * 对编辑器设置keydown事件监听,绑定快捷键和命令,当快捷键组合触发成功,会响应对应的命令 * @method _bindshortcutKeys * @private */ _bindshortcutKeys: function () { var me = this, shortcutkeys = this.shortcutkeys; me.addListener('keydown', function (type, e) { var keyCode = e.keyCode || e.which; for (var i in shortcutkeys) { var tmp = shortcutkeys[i].split(','); for (var t = 0, ti; ti = tmp[t++];) { ti = ti.split(':'); var key = ti[0], param = ti[1]; if (/^(ctrl)(\+shift)?\+(\d+)$/.test(key.toLowerCase()) || /^(\d+)$/.test(key)) { if (( (RegExp.$1 == 'ctrl' ? (e.ctrlKey || e.metaKey) : 0) && (RegExp.$2 != "" ? e[RegExp.$2.slice(1) + "Key"] : 1) && keyCode == RegExp.$3 ) || keyCode == RegExp.$1 ) { if (me.queryCommandState(i,param) != -1) me.execCommand(i, param); domUtils.preventDefault(e); } } } } }); }, /** * 获取编辑器的内容 * @method getContent * @warning 该方法获取到的是经过编辑器内置的过滤规则进行过滤后得到的内容 * @return { String } 编辑器的内容字符串, 如果编辑器的内容为空,或者是空的标签内容(如:”<p><br/></p>“), 则返回空字符串 * @example * ```javascript * //编辑器html内容:

    123456

    * var content = editor.getContent(); //返回值:

    123456

    * ``` */ /** * 获取编辑器的内容。 可以通过参数定义编辑器内置的判空规则 * @method getContent * @param { Function } fn 自定的判空规则, 要求该方法返回一个boolean类型的值, * 代表当前编辑器的内容是否空, * 如果返回true, 则该方法将直接返回空字符串;如果返回false,则编辑器将返回 * 经过内置过滤规则处理后的内容。 * @remind 该方法在处理包含有初始化内容的时候能起到很好的作用。 * @warning 该方法获取到的是经过编辑器内置的过滤规则进行过滤后得到的内容 * @return { String } 编辑器的内容字符串 * @example * ```javascript * // editor 是一个编辑器的实例 * var content = editor.getContent( function ( editor ) { * return editor.body.innerHTML === '欢迎使用UEditor'; //返回空字符串 * } ); * ``` */ getContent: function (cmd, fn,notSetCursor,ignoreBlank,formatter) { var me = this; if (cmd && utils.isFunction(cmd)) { fn = cmd; cmd = ''; } if (fn ? !fn() : !this.hasContents()) { return ''; } me.fireEvent('beforegetcontent'); var root = UE.htmlparser(me.body.innerHTML,ignoreBlank); me.filterOutputRule(root); me.fireEvent('aftergetcontent', cmd,root); return root.toHtml(formatter); }, /** * 取得完整的html代码,可以直接显示成完整的html文档 * @method getAllHtml * @return { String } 编辑器的内容html文档字符串 * @eaxmple * ```javascript * editor.getAllHtml(); //返回格式大致是: ...... * ``` */ getAllHtml: function () { var me = this, headHtml = [], html = ''; me.fireEvent('getAllHtml', headHtml); if (browser.ie && browser.version > 8) { var headHtmlForIE9 = ''; utils.each(me.document.styleSheets, function (si) { headHtmlForIE9 += ( si.href ? '' : ''); }); utils.each(me.document.getElementsByTagName('script'), function (si) { headHtmlForIE9 += si.outerHTML; }); } return '' + (me.options.charset ? '' : '') + (headHtmlForIE9 || me.document.getElementsByTagName('head')[0].innerHTML) + headHtml.join('\n') + '' + '' + me.getContent(null, null, true) + ''; }, /** * 得到编辑器的纯文本内容,但会保留段落格式 * @method getPlainTxt * @return { String } 编辑器带段落格式的纯文本内容字符串 * @example * ```javascript * //编辑器html内容:

    1

    2

    * console.log(editor.getPlainTxt()); //输出:"1\n2\n * ``` */ getPlainTxt: function () { var reg = new RegExp(domUtils.fillChar, 'g'), html = this.body.innerHTML.replace(/[\n\r]/g, '');//ie要先去了\n在处理 html = html.replace(/<(p|div)[^>]*>(| )<\/\1>/gi, '\n') .replace(//gi, '\n') .replace(/<[^>/]+>/g, '') .replace(/(\n)?<\/([^>]+)>/g, function (a, b, c) { return dtd.$block[c] ? '\n' : b ? b : ''; }); //取出来的空格会有c2a0会变成乱码,处理这种情况\u00a0 return html.replace(reg, '').replace(/\u00a0/g, ' ').replace(/ /g, ' '); }, /** * 获取编辑器中的纯文本内容,没有段落格式 * @method getContentTxt * @return { String } 编辑器不带段落格式的纯文本内容字符串 * @example * ```javascript * //编辑器html内容:

    1

    2

    * console.log(editor.getPlainTxt()); //输出:"12 * ``` */ getContentTxt: function () { var reg = new RegExp(domUtils.fillChar, 'g'); //取出来的空格会有c2a0会变成乱码,处理这种情况\u00a0 return this.body[browser.ie ? 'innerText' : 'textContent'].replace(reg, '').replace(/\u00a0/g, ' '); }, /** * 设置编辑器的内容,可修改编辑器当前的html内容 * @method setContent * @warning 通过该方法插入的内容,是经过编辑器内置的过滤规则进行过滤后得到的内容 * @warning 该方法会触发selectionchange事件 * @param { String } html 要插入的html内容 * @example * ```javascript * editor.getContent('

    test

    '); * ``` */ /** * 设置编辑器的内容,可修改编辑器当前的html内容 * @method setContent * @warning 通过该方法插入的内容,是经过编辑器内置的过滤规则进行过滤后得到的内容 * @warning 该方法会触发selectionchange事件 * @param { String } html 要插入的html内容 * @param { Boolean } isAppendTo 若传入true,不清空原来的内容,在最后插入内容,否则,清空内容再插入 * @example * ```javascript * //假设设置前的编辑器内容是

    old text

    * editor.setContent('

    new text

    ', true); //插入的结果是

    old text

    new text

    * ``` */ setContent: function (html, isAppendTo, notFireSelectionchange) { var me = this; me.fireEvent('beforesetcontent', html); var root = UE.htmlparser(html); me.filterInputRule(root); html = root.toHtml(); me.body.innerHTML = (isAppendTo ? me.body.innerHTML : '') + html; function isCdataDiv(node){ return node.tagName == 'DIV' && node.getAttribute('cdata_tag'); } //给文本或者inline节点套p标签 if (me.options.enterTag == 'p') { var child = this.body.firstChild, tmpNode; if (!child || child.nodeType == 1 && (dtd.$cdata[child.tagName] || isCdataDiv(child) || domUtils.isCustomeNode(child) ) && child === this.body.lastChild) { this.body.innerHTML = '

    ' + (browser.ie ? ' ' : '
    ') + '

    ' + this.body.innerHTML; } else { var p = me.document.createElement('p'); while (child) { while (child && (child.nodeType == 3 || child.nodeType == 1 && dtd.p[child.tagName] && !dtd.$cdata[child.tagName])) { tmpNode = child.nextSibling; p.appendChild(child); child = tmpNode; } if (p.firstChild) { if (!child) { me.body.appendChild(p); break; } else { child.parentNode.insertBefore(p, child); p = me.document.createElement('p'); } } child = child.nextSibling; } } } me.fireEvent('aftersetcontent'); me.fireEvent('contentchange'); !notFireSelectionchange && me._selectionChange(); //清除保存的选区 me._bakRange = me._bakIERange = me._bakNativeRange = null; //trace:1742 setContent后gecko能得到焦点问题 var geckoSel; if (browser.gecko && (geckoSel = this.selection.getNative())) { geckoSel.removeAllRanges(); } if(me.options.autoSyncData){ me.form && setValue(me.form,me); } }, /** * 让编辑器获得焦点,默认focus到编辑器头部 * @method focus * @example * ```javascript * editor.focus() * ``` */ /** * 让编辑器获得焦点,toEnd确定focus位置 * @method focus * @param { Boolean } toEnd 默认focus到编辑器头部,toEnd为true时focus到内容尾部 * @example * ```javascript * editor.focus(true) * ``` */ focus: function (toEnd) { try { var me = this, rng = me.selection.getRange(); if (toEnd) { var node = me.body.lastChild; if(node && node.nodeType == 1 && !dtd.$empty[node.tagName]){ if(domUtils.isEmptyBlock(node)){ rng.setStartAtFirst(node) }else{ rng.setStartAtLast(node) } rng.collapse(true); } rng.setCursor(true); } else { if(!rng.collapsed && domUtils.isBody(rng.startContainer) && rng.startOffset == 0){ var node = me.body.firstChild; if(node && node.nodeType == 1 && !dtd.$empty[node.tagName]){ rng.setStartAtFirst(node).collapse(true); } } rng.select(true); } this.fireEvent('focus selectionchange'); } catch (e) { } }, isFocus:function(){ return this.selection.isFocus(); }, blur:function(){ var sel = this.selection.getNative(); if(sel.empty && browser.ie){ var nativeRng = document.body.createTextRange(); nativeRng.moveToElementText(document.body); nativeRng.collapse(true); nativeRng.select(); sel.empty() }else{ sel.removeAllRanges() } //this.fireEvent('blur selectionchange'); }, /** * 初始化UE事件及部分事件代理 * @method _initEvents * @private */ _initEvents: function () { var me = this, doc = me.document, win = me.window; me._proxyDomEvent = utils.bind(me._proxyDomEvent, me); domUtils.on(doc, ['click', 'contextmenu', 'mousedown', 'keydown', 'keyup', 'keypress', 'mouseup', 'mouseover', 'mouseout', 'selectstart'], me._proxyDomEvent); domUtils.on(win, ['focus', 'blur'], me._proxyDomEvent); domUtils.on(me.body,'drop',function(e){ //阻止ff下默认的弹出新页面打开图片 if(browser.gecko && e.stopPropagation) { e.stopPropagation(); } me.fireEvent('contentchange') }); domUtils.on(doc, ['mouseup', 'keydown'], function (evt) { //特殊键不触发selectionchange if (evt.type == 'keydown' && (evt.ctrlKey || evt.metaKey || evt.shiftKey || evt.altKey)) { return; } if (evt.button == 2)return; me._selectionChange(250, evt); }); }, /** * 触发事件代理 * @method _proxyDomEvent * @private * @return { * } fireEvent的返回值 * @see UE.EventBase:fireEvent(String) */ _proxyDomEvent: function (evt) { if(this.fireEvent('before' + evt.type.replace(/^on/, '').toLowerCase()) === false){ return false; } if(this.fireEvent(evt.type.replace(/^on/, ''), evt) === false){ return false; } return this.fireEvent('after' + evt.type.replace(/^on/, '').toLowerCase()) }, /** * 变化选区 * @method _selectionChange * @private */ _selectionChange: function (delay, evt) { var me = this; //有光标才做selectionchange 为了解决未focus时点击source不能触发更改工具栏状态的问题(source命令notNeedUndo=1) // if ( !me.selection.isFocus() ){ // return; // } var hackForMouseUp = false; var mouseX, mouseY; if (browser.ie && browser.version < 9 && evt && evt.type == 'mouseup') { var range = this.selection.getRange(); if (!range.collapsed) { hackForMouseUp = true; mouseX = evt.clientX; mouseY = evt.clientY; } } clearTimeout(_selectionChangeTimer); _selectionChangeTimer = setTimeout(function () { if (!me.selection || !me.selection.getNative()) { return; } //修复一个IE下的bug: 鼠标点击一段已选择的文本中间时,可能在mouseup后的一段时间内取到的range是在selection的type为None下的错误值. //IE下如果用户是拖拽一段已选择文本,则不会触发mouseup事件,所以这里的特殊处理不会对其有影响 var ieRange; if (hackForMouseUp && me.selection.getNative().type == 'None') { ieRange = me.document.body.createTextRange(); try { ieRange.moveToPoint(mouseX, mouseY); } catch (ex) { ieRange = null; } } var bakGetIERange; if (ieRange) { bakGetIERange = me.selection.getIERange; me.selection.getIERange = function () { return ieRange; }; } me.selection.cache(); if (bakGetIERange) { me.selection.getIERange = bakGetIERange; } if (me.selection._cachedRange && me.selection._cachedStartElement) { me.fireEvent('beforeselectionchange'); // 第二个参数causeByUi为true代表由用户交互造成的selectionchange. me.fireEvent('selectionchange', !!evt); me.fireEvent('afterselectionchange'); me.selection.clear(); } }, delay || 50); }, /** * 执行编辑命令 * @method _callCmdFn * @private * @param { String } fnName 函数名称 * @param { * } args 传给命令函数的参数 * @return { * } 返回命令函数运行的返回值 */ _callCmdFn: function (fnName, args) { var cmdName = args[0].toLowerCase(), cmd, cmdFn; cmd = this.commands[cmdName] || UE.commands[cmdName]; cmdFn = cmd && cmd[fnName]; //没有querycommandstate或者没有command的都默认返回0 if ((!cmd || !cmdFn) && fnName == 'queryCommandState') { return 0; } else if (cmdFn) { return cmdFn.apply(this, args); } }, /** * 执行编辑命令cmdName,完成富文本编辑效果 * @method execCommand * @param { String } cmdName 需要执行的命令 * @remind 具体命令的使用请参考命令列表 * @return { * } 返回命令函数运行的返回值 * @example * ```javascript * editor.execCommand(cmdName); * ``` */ execCommand: function (cmdName) { cmdName = cmdName.toLowerCase(); var me = this, result, cmd = me.commands[cmdName] || UE.commands[cmdName]; if (!cmd || !cmd.execCommand) { return null; } if (!cmd.notNeedUndo && !me.__hasEnterExecCommand) { me.__hasEnterExecCommand = true; if (me.queryCommandState.apply(me,arguments) != -1) { me.fireEvent('saveScene'); me.fireEvent.apply(me, ['beforeexeccommand', cmdName].concat(arguments)); result = this._callCmdFn('execCommand', arguments); //保存场景时,做了内容对比,再看是否进行contentchange触发,这里多触发了一次,去掉 // (!cmd.ignoreContentChange && !me._ignoreContentChange) && me.fireEvent('contentchange'); me.fireEvent.apply(me, ['afterexeccommand', cmdName].concat(arguments)); me.fireEvent('saveScene'); } me.__hasEnterExecCommand = false; } else { result = this._callCmdFn('execCommand', arguments); (!me.__hasEnterExecCommand && !cmd.ignoreContentChange && !me._ignoreContentChange) && me.fireEvent('contentchange') } (!me.__hasEnterExecCommand && !cmd.ignoreContentChange && !me._ignoreContentChange) && me._selectionChange(); return result; }, /** * 根据传入的command命令,查选编辑器当前的选区,返回命令的状态 * @method queryCommandState * @param { String } cmdName 需要查询的命令名称 * @remind 具体命令的使用请参考命令列表 * @return { Number } number 返回放前命令的状态,返回值三种情况:(-1|0|1) * @example * ```javascript * editor.queryCommandState(cmdName) => (-1|0|1) * ``` * @see COMMAND.LIST */ queryCommandState: function (cmdName) { return this._callCmdFn('queryCommandState', arguments); }, /** * 根据传入的command命令,查选编辑器当前的选区,根据命令返回相关的值 * @method queryCommandValue * @param { String } cmdName 需要查询的命令名称 * @remind 具体命令的使用请参考命令列表 * @remind 只有部分插件有此方法 * @return { * } 返回每个命令特定的当前状态值 * @grammar editor.queryCommandValue(cmdName) => {*} * @see COMMAND.LIST */ queryCommandValue: function (cmdName) { return this._callCmdFn('queryCommandValue', arguments); }, /** * 检查编辑区域中是否有内容 * @method hasContents * @remind 默认有文本内容,或者有以下节点都不认为是空 * table,ul,ol,dl,iframe,area,base,col,hr,img,embed,input,link,meta,param * @return { Boolean } 检查有内容返回true,否则返回false * @example * ```javascript * editor.hasContents() * ``` */ /** * 检查编辑区域中是否有内容,若包含参数tags中的节点类型,直接返回true * @method hasContents * @param { Array } tags 传入数组判断时用到的节点类型 * @return { Boolean } 若文档中包含tags数组里对应的tag,返回true,否则返回false * @example * ```javascript * editor.hasContents(['span']); * ``` */ hasContents: function (tags) { if (tags) { for (var i = 0, ci; ci = tags[i++];) { if (this.document.getElementsByTagName(ci).length > 0) { return true; } } } if (!domUtils.isEmptyBlock(this.body)) { return true } //随时添加,定义的特殊标签如果存在,不能认为是空 tags = ['div']; for (i = 0; ci = tags[i++];) { var nodes = domUtils.getElementsByTagName(this.document, ci); for (var n = 0, cn; cn = nodes[n++];) { if (domUtils.isCustomeNode(cn)) { return true; } } } return false; }, /** * 重置编辑器,可用来做多个tab使用同一个编辑器实例 * @method reset * @remind 此方法会清空编辑器内容,清空回退列表,会触发reset事件 * @example * ```javascript * editor.reset() * ``` */ reset: function () { this.fireEvent('reset'); }, /** * 设置当前编辑区域可以编辑 * @method setEnabled * @example * ```javascript * editor.setEnabled() * ``` */ setEnabled: function () { var me = this, range; if (me.body.contentEditable == 'false') { me.body.contentEditable = true; range = me.selection.getRange(); //有可能内容丢失了 try { range.moveToBookmark(me.lastBk); delete me.lastBk } catch (e) { range.setStartAtFirst(me.body).collapse(true) } range.select(true); if (me.bkqueryCommandState) { me.queryCommandState = me.bkqueryCommandState; delete me.bkqueryCommandState; } if (me.bkqueryCommandValue) { me.queryCommandValue = me.bkqueryCommandValue; delete me.bkqueryCommandValue; } me.fireEvent('selectionchange'); } }, enable: function () { return this.setEnabled(); }, /** 设置当前编辑区域不可编辑 * @method setDisabled */ /** 设置当前编辑区域不可编辑,except中的命令除外 * @method setDisabled * @param { String } except 例外命令的字符串 * @remind 即使设置了disable,此处配置的例外命令仍然可以执行 * @example * ```javascript * editor.setDisabled('bold'); //禁用工具栏中除加粗之外的所有功能 * ``` */ /** 设置当前编辑区域不可编辑,except中的命令除外 * @method setDisabled * @param { Array } except 例外命令的字符串数组,数组中的命令仍然可以执行 * @remind 即使设置了disable,此处配置的例外命令仍然可以执行 * @example * ```javascript * editor.setDisabled(['bold','insertimage']); //禁用工具栏中除加粗和插入图片之外的所有功能 * ``` */ setDisabled: function (except) { var me = this; except = except ? utils.isArray(except) ? except : [except] : []; if (me.body.contentEditable == 'true') { if (!me.lastBk) { me.lastBk = me.selection.getRange().createBookmark(true); } me.body.contentEditable = false; me.bkqueryCommandState = me.queryCommandState; me.bkqueryCommandValue = me.queryCommandValue; me.queryCommandState = function (type) { if (utils.indexOf(except, type) != -1) { return me.bkqueryCommandState.apply(me, arguments); } return -1; }; me.queryCommandValue = function (type) { if (utils.indexOf(except, type) != -1) { return me.bkqueryCommandValue.apply(me, arguments); } return null; }; me.fireEvent('selectionchange'); } }, disable: function (except) { return this.setDisabled(except); }, /** * 设置默认内容 * @method _setDefaultContent * @private * @param { String } cont 要存入的内容 */ _setDefaultContent: function () { function clear() { var me = this; if (me.document.getElementById('initContent')) { me.body.innerHTML = '

    ' + (ie ? '' : '
    ') + '

    '; me.removeListener('firstBeforeExecCommand focus', clear); setTimeout(function () { me.focus(); me._selectionChange(); }, 0) } } return function (cont) { var me = this; me.body.innerHTML = '

    ' + cont + '

    '; me.addListener('firstBeforeExecCommand focus', clear); } }(), /** * 显示编辑器 * @method setShow * @example * ```javascript * editor.setShow() * ``` */ setShow: function () { var me = this, range = me.selection.getRange(); if (me.container.style.display == 'none') { //有可能内容丢失了 try { range.moveToBookmark(me.lastBk); delete me.lastBk } catch (e) { range.setStartAtFirst(me.body).collapse(true) } //ie下focus实效,所以做了个延迟 setTimeout(function () { range.select(true); }, 100); me.container.style.display = ''; } }, show: function () { return this.setShow(); }, /** * 隐藏编辑器 * @method setHide * @example * ```javascript * editor.setHide() * ``` */ setHide: function () { var me = this; if (!me.lastBk) { me.lastBk = me.selection.getRange().createBookmark(true); } me.container.style.display = 'none' }, hide: function () { return this.setHide(); }, /** * 根据指定的路径,获取对应的语言资源 * @method getLang * @param { String } path 路径根据的是lang目录下的语言文件的路径结构 * @return { Object | String } 根据路径返回语言资源的Json格式对象或者语言字符串 * @example * ```javascript * editor.getLang('contextMenu.delete'); //如果当前是中文,那返回是的是'删除' * ``` */ getLang: function (path) { // HaoChuan9421 if(!this.options){ return ''; } var lang = UE.I18N[this.options.lang]; if (!lang) { throw Error("not import language file"); } path = (path || "").split("."); for (var i = 0, ci; ci = path[i++];) { lang = lang[ci]; if (!lang)break; } return lang; }, /** * 计算编辑器html内容字符串的长度 * @method getContentLength * @return { Number } 返回计算的长度 * @example * ```javascript * //编辑器html内容

    132

    * editor.getContentLength() //返回27 * ``` */ /** * 计算编辑器当前纯文本内容的长度 * @method getContentLength * @param { Boolean } ingoneHtml 传入true时,只按照纯文本来计算 * @return { Number } 返回计算的长度,内容中有hr/img/iframe标签,长度加1 * @example * ```javascript * //编辑器html内容

    132

    * editor.getContentLength() //返回3 * ``` */ getContentLength: function (ingoneHtml, tagNames) { var count = this.getContent(false,false,true).length; if (ingoneHtml) { tagNames = (tagNames || []).concat([ 'hr', 'img', 'iframe']); count = this.getContentTxt().replace(/[\t\r\n]+/g, '').length; for (var i = 0, ci; ci = tagNames[i++];) { count += this.document.getElementsByTagName(ci).length; } } return count; }, /** * 注册输入过滤规则 * @method addInputRule * @param { Function } rule 要添加的过滤规则 * @example * ```javascript * editor.addInputRule(function(root){ * $.each(root.getNodesByTagName('div'),function(i,node){ * node.tagName="p"; * }); * }); * ``` */ addInputRule: function (rule) { this.inputRules.push(rule); }, /** * 执行注册的过滤规则 * @method filterInputRule * @param { UE.uNode } root 要过滤的uNode节点 * @remind 执行editor.setContent方法和执行'inserthtml'命令后,会运行该过滤函数 * @example * ```javascript * editor.filterInputRule(editor.body); * ``` * @see UE.Editor:addInputRule */ filterInputRule: function (root) { for (var i = 0, ci; ci = this.inputRules[i++];) { ci.call(this, root) } }, /** * 注册输出过滤规则 * @method addOutputRule * @param { Function } rule 要添加的过滤规则 * @example * ```javascript * editor.addOutputRule(function(root){ * $.each(root.getNodesByTagName('p'),function(i,node){ * node.tagName="div"; * }); * }); * ``` */ addOutputRule: function (rule) { this.outputRules.push(rule) }, /** * 根据输出过滤规则,过滤编辑器内容 * @method filterOutputRule * @remind 执行editor.getContent方法的时候,会先运行该过滤函数 * @param { UE.uNode } root 要过滤的uNode节点 * @example * ```javascript * editor.filterOutputRule(editor.body); * ``` * @see UE.Editor:addOutputRule */ filterOutputRule: function (root) { for (var i = 0, ci; ci = this.outputRules[i++];) { ci.call(this, root) } }, /** * 根据action名称获取请求的路径 * @method getActionUrl * @remind 假如没有设置serverUrl,会根据imageUrl设置默认的controller路径 * @param { String } action action名称 * @example * ```javascript * editor.getActionUrl('config'); //返回 "/ueditor/php/controller.php?action=config" * editor.getActionUrl('image'); //返回 "/ueditor/php/controller.php?action=uplaodimage" * editor.getActionUrl('scrawl'); //返回 "/ueditor/php/controller.php?action=uplaodscrawl" * editor.getActionUrl('imageManager'); //返回 "/ueditor/php/controller.php?action=listimage" * ``` */ getActionUrl: function(action){ var actionName = this.getOpt(action) || action, imageUrl = this.getOpt('imageUrl'), serverUrl = this.getOpt('serverUrl'); if(!serverUrl && imageUrl) { serverUrl = imageUrl.replace(/^(.*[\/]).+([\.].+)$/, '$1controller$2'); } if(serverUrl) { serverUrl = serverUrl + (serverUrl.indexOf('?') == -1 ? '?':'&') + 'action=' + (actionName || ''); return utils.formatUrl(serverUrl); } else { return ''; } } }; utils.inherits(Editor, EventBase); })(); // core/Editor.defaultoptions.js //维护编辑器一下默认的不在插件中的配置项 UE.Editor.defaultOptions = function(editor){ var _url = editor.options.UEDITOR_HOME_URL; return { isShow: true, initialContent: '', initialStyle:'', autoClearinitialContent: false, iframeCssUrl: _url + 'themes/iframe.css', textarea: 'editorValue', focus: false, focusInEnd: true, autoClearEmptyNode: true, fullscreen: false, readonly: false, zIndex: 99999, imagePopup: true, enterTag: 'p', customDomain: false, lang: 'zh-cn', langPath: _url + 'lang/', theme: 'default', themePath: _url + 'themes/', allHtmlEnabled: false, scaleEnabled: false, tableNativeEditInFF: false, autoSyncData : true, fileNameFormat: '{time}{rand:6}' } }; // core/loadconfig.js (function(){ UE.Editor.prototype.loadServerConfig = function(){ var me = this; setTimeout(function(){ try{ me.options.imageUrl && me.setOpt('serverUrl', me.options.imageUrl.replace(/^(.*[\/]).+([\.].+)$/, '$1controller$2')); var configUrl = me.getActionUrl('config'), isJsonp = utils.isCrossDomainUrl(configUrl); /* 发出ajax请求 */ me._serverConfigLoaded = false; configUrl && UE.ajax.request(configUrl,{ 'method': 'GET', 'dataType': isJsonp ? 'jsonp':'', 'onsuccess':function(r){ try { var config = isJsonp ? r:eval("("+r.responseText+")"); utils.extend(me.options, config); me.fireEvent('serverConfigLoaded'); me._serverConfigLoaded = true; } catch (e) { showErrorMsg(me.getLang('loadconfigFormatError')); } }, 'onerror':function(){ showErrorMsg(me.getLang('loadconfigHttpError')); } }); } catch(e){ showErrorMsg(me.getLang('loadconfigError')); } }); function showErrorMsg(msg) { console && console.error(msg); //me.fireEvent('showMessage', { // 'title': msg, // 'type': 'error' //}); } }; UE.Editor.prototype.isServerConfigLoaded = function(){ var me = this; return me._serverConfigLoaded || false; }; UE.Editor.prototype.afterConfigReady = function(handler){ if (!handler || !utils.isFunction(handler)) return; var me = this; var readyHandler = function(){ handler.apply(me, arguments); me.removeListener('serverConfigLoaded', readyHandler); }; if (me.isServerConfigLoaded()) { handler.call(me, 'serverConfigLoaded'); } else { me.addListener('serverConfigLoaded', readyHandler); } }; })(); // core/ajax.js /** * @file * @module UE.ajax * @since 1.2.6.1 */ /** * 提供对ajax请求的支持 * @module UE.ajax */ UE.ajax = function() { //创建一个ajaxRequest对象 var fnStr = 'XMLHttpRequest()'; try { new ActiveXObject("Msxml2.XMLHTTP"); fnStr = 'ActiveXObject(\'Msxml2.XMLHTTP\')'; } catch (e) { try { new ActiveXObject("Microsoft.XMLHTTP"); fnStr = 'ActiveXObject(\'Microsoft.XMLHTTP\')' } catch (e) { } } var creatAjaxRequest = new Function('return new ' + fnStr); /** * 将json参数转化成适合ajax提交的参数列表 * @param json */ function json2str(json) { var strArr = []; for (var i in json) { //忽略默认的几个参数 if(i=="method" || i=="timeout" || i=="async" || i=="dataType" || i=="callback") continue; //忽略控制 if(json[i] == undefined || json[i] == null) continue; //传递过来的对象和函数不在提交之列 if (!((typeof json[i]).toLowerCase() == "function" || (typeof json[i]).toLowerCase() == "object")) { strArr.push( encodeURIComponent(i) + "="+encodeURIComponent(json[i]) ); } else if (utils.isArray(json[i])) { //支持传数组内容 for(var j = 0; j < json[i].length; j++) { strArr.push( encodeURIComponent(i) + "[]="+encodeURIComponent(json[i][j]) ); } } } return strArr.join("&"); } function doAjax(url, ajaxOptions) { var xhr = creatAjaxRequest(), //是否超时 timeIsOut = false, //默认参数 defaultAjaxOptions = { method:"POST", timeout:5000, async:true, data:{},//需要传递对象的话只能覆盖 onsuccess:function() { }, onerror:function() { } }; if (typeof url === "object") { ajaxOptions = url; url = ajaxOptions.url; } if (!xhr || !url) return; var ajaxOpts = ajaxOptions ? utils.extend(defaultAjaxOptions,ajaxOptions) : defaultAjaxOptions; var submitStr = json2str(ajaxOpts); // { name:"Jim",city:"Beijing" } --> "name=Jim&city=Beijing" //如果用户直接通过data参数传递json对象过来,则也要将此json对象转化为字符串 if (!utils.isEmptyObject(ajaxOpts.data)){ submitStr += (submitStr? "&":"") + json2str(ajaxOpts.data); } //超时检测 var timerID = setTimeout(function() { if (xhr.readyState != 4) { timeIsOut = true; xhr.abort(); clearTimeout(timerID); } }, ajaxOpts.timeout); var method = ajaxOpts.method.toUpperCase(); var str = url + (url.indexOf("?")==-1?"?":"&") + (method=="POST"?"":submitStr+ "&noCache=" + +new Date); xhr.open(method, str, ajaxOpts.async); xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if (!timeIsOut && xhr.status == 200) { ajaxOpts.onsuccess(xhr); } else { ajaxOpts.onerror(xhr); } } }; if (method == "POST") { xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send(submitStr); } else { xhr.send(null); } } function doJsonp(url, opts) { var successhandler = opts.onsuccess || function(){}, scr = document.createElement('SCRIPT'), options = opts || {}, charset = options['charset'], callbackField = options['jsonp'] || 'callback', callbackFnName, timeOut = options['timeOut'] || 0, timer, reg = new RegExp('(\\?|&)' + callbackField + '=([^&]*)'), matches; if (utils.isFunction(successhandler)) { callbackFnName = 'bd__editor__' + Math.floor(Math.random() * 2147483648).toString(36); window[callbackFnName] = getCallBack(0); } else if(utils.isString(successhandler)){ callbackFnName = successhandler; } else { if (matches = reg.exec(url)) { callbackFnName = matches[2]; } } url = url.replace(reg, '\x241' + callbackField + '=' + callbackFnName); if (url.search(reg) < 0) { url += (url.indexOf('?') < 0 ? '?' : '&') + callbackField + '=' + callbackFnName; } var queryStr = json2str(opts); // { name:"Jim",city:"Beijing" } --> "name=Jim&city=Beijing" //如果用户直接通过data参数传递json对象过来,则也要将此json对象转化为字符串 if (!utils.isEmptyObject(opts.data)){ queryStr += (queryStr? "&":"") + json2str(opts.data); } if (queryStr) { url = url.replace(/\?/, '?' + queryStr + '&'); } scr.onerror = getCallBack(1); if( timeOut ){ timer = setTimeout(getCallBack(1), timeOut); } createScriptTag(scr, url, charset); function createScriptTag(scr, url, charset) { scr.setAttribute('type', 'text/javascript'); scr.setAttribute('defer', 'defer'); charset && scr.setAttribute('charset', charset); scr.setAttribute('src', url); document.getElementsByTagName('head')[0].appendChild(scr); } function getCallBack(onTimeOut){ return function(){ try { if(onTimeOut){ options.onerror && options.onerror(); }else{ try{ clearTimeout(timer); successhandler.apply(window, arguments); } catch (e){} } } catch (exception) { options.onerror && options.onerror.call(window, exception); } finally { options.oncomplete && options.oncomplete.apply(window, arguments); scr.parentNode && scr.parentNode.removeChild(scr); window[callbackFnName] = null; try { delete window[callbackFnName]; }catch(e){} } } } } return { /** * 根据给定的参数项,向指定的url发起一个ajax请求。 ajax请求完成后,会根据请求结果调用相应回调: 如果请求 * 成功, 则调用onsuccess回调, 失败则调用 onerror 回调 * @method request * @param { URLString } url ajax请求的url地址 * @param { Object } ajaxOptions ajax请求选项的键值对,支持的选项如下: * @example * ```javascript * //向sayhello.php发起一个异步的Ajax GET请求, 请求超时时间为10s, 请求完成后执行相应的回调。 * UE.ajax.requeset( 'sayhello.php', { * * //请求方法。可选值: 'GET', 'POST',默认值是'POST' * method: 'GET', * * //超时时间。 默认为5000, 单位是ms * timeout: 10000, * * //是否是异步请求。 true为异步请求, false为同步请求 * async: true, * * //请求携带的数据。如果请求为GET请求, data会经过stringify后附加到请求url之后。 * data: { * name: 'ueditor' * }, * * //请求成功后的回调, 该回调接受当前的XMLHttpRequest对象作为参数。 * onsuccess: function ( xhr ) { * console.log( xhr.responseText ); * }, * * //请求失败或者超时后的回调。 * onerror: function ( xhr ) { * alert( 'Ajax请求失败' ); * } * * } ); * ``` */ /** * 根据给定的参数项发起一个ajax请求, 参数项里必须包含一个url地址。 ajax请求完成后,会根据请求结果调用相应回调: 如果请求 * 成功, 则调用onsuccess回调, 失败则调用 onerror 回调。 * @method request * @warning 如果在参数项里未提供一个key为“url”的地址值,则该请求将直接退出。 * @param { Object } ajaxOptions ajax请求选项的键值对,支持的选项如下: * @example * ```javascript * * //向sayhello.php发起一个异步的Ajax POST请求, 请求超时时间为5s, 请求完成后不执行任何回调。 * UE.ajax.requeset( 'sayhello.php', { * * //请求的地址, 该项是必须的。 * url: 'sayhello.php' * * } ); * ``` */ request:function(url, opts) { if (opts && opts.dataType == 'jsonp') { doJsonp(url, opts); } else { doAjax(url, opts); } }, getJSONP:function(url, data, fn) { var opts = { 'data': data, 'oncomplete': fn }; doJsonp(url, opts); } }; }(); // core/filterword.js /** * UE过滤word的静态方法 * @file */ /** * UEditor公用空间,UEditor所有的功能都挂载在该空间下 * @module UE */ /** * 根据传入html字符串过滤word * @module UE * @since 1.2.6.1 * @method filterWord * @param { String } html html字符串 * @return { String } 已过滤后的结果字符串 * @example * ```javascript * UE.filterWord(html); * ``` */ var filterWord = UE.filterWord = function () { //是否是word过来的内容 function isWordDocument( str ) { return /(class="?Mso|style="[^"]*\bmso\-|w:WordDocument|<(v|o):|lang=)/ig.test( str ); } //去掉小数 function transUnit( v ) { v = v.replace( /[\d.]+\w+/g, function ( m ) { return utils.transUnitToPx(m); } ); return v; } function filterPasteWord( str ) { return str.replace(/[\t\r\n]+/g,' ') .replace( //ig, "" ) //转换图片 .replace(/]*>[\s\S]*?.<\/v:shape>/gi,function(str){ //opera能自己解析出image所这里直接返回空 if(browser.opera){ return ''; } try{ //有可能是bitmap占为图,无用,直接过滤掉,主要体现在粘贴excel表格中 if(/Bitmap/i.test(str)){ return ''; } var width = str.match(/width:([ \d.]*p[tx])/i)[1], height = str.match(/height:([ \d.]*p[tx])/i)[1], src = str.match(/src=\s*"([^"]*)"/i)[1]; return ''; } catch(e){ return ''; } }) //针对wps添加的多余标签处理 .replace(/<\/?div[^>]*>/g,'') //去掉多余的属性 .replace( /v:\w+=(["']?)[^'"]+\1/g, '' ) .replace( /<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|xml|meta|link|style|\w+:\w+)(?=[\s\/>]))[^>]*>/gi, "" ) .replace( /

    ]*class="?MsoHeading"?[^>]*>(.*?)<\/p>/gi, "

    $1

    " ) //去掉多余的属性 .replace( /\s+(class|lang|align)\s*=\s*(['"]?)([\w-]+)\2/ig, function(str,name,marks,val){ //保留list的标示 return name == 'class' && val == 'MsoListParagraph' ? str : '' }) //清除多余的font/span不能匹配 有可能是空格 .replace( /<(font|span)[^>]*>(\s*)<\/\1>/gi, function(a,b,c){ return c.replace(/[\t\r\n ]+/g,' ') }) //处理style的问题 .replace( /(<[a-z][^>]*)\sstyle=(["'])([^\2]*?)\2/gi, function( str, tag, tmp, style ) { var n = [], s = style.replace( /^\s+|\s+$/, '' ) .replace(/'/g,'\'') .replace( /"/gi, "'" ) .replace(/[\d.]+(cm|pt)/g,function(str){ return utils.transUnitToPx(str) }) .split( /;\s*/g ); for ( var i = 0,v; v = s[i];i++ ) { var name, value, parts = v.split( ":" ); if ( parts.length == 2 ) { name = parts[0].toLowerCase(); value = parts[1].toLowerCase(); if(/^(background)\w*/.test(name) && value.replace(/(initial|\s)/g,'').length == 0 || /^(margin)\w*/.test(name) && /^0\w+$/.test(value) ){ continue; } switch ( name ) { case "mso-padding-alt": case "mso-padding-top-alt": case "mso-padding-right-alt": case "mso-padding-bottom-alt": case "mso-padding-left-alt": case "mso-margin-alt": case "mso-margin-top-alt": case "mso-margin-right-alt": case "mso-margin-bottom-alt": case "mso-margin-left-alt": //ie下会出现挤到一起的情况 //case "mso-table-layout-alt": case "mso-height": case "mso-width": case "mso-vertical-align-alt": //trace:1819 ff下会解析出padding在table上 if(!/]/.test(html)) { return UE.htmlparser(html).children[0] } else { return new uNode({ type:'element', children:[], tagName:html }) } }; uNode.createText = function (data,noTrans) { return new UE.uNode({ type:'text', 'data':noTrans ? data : utils.unhtml(data || '') }) }; function nodeToHtml(node, arr, formatter, current) { switch (node.type) { case 'root': for (var i = 0, ci; ci = node.children[i++];) { //插入新行 if (formatter && ci.type == 'element' && !dtd.$inlineWithA[ci.tagName] && i > 1) { insertLine(arr, current, true); insertIndent(arr, current) } nodeToHtml(ci, arr, formatter, current) } break; case 'text': isText(node, arr); break; case 'element': isElement(node, arr, formatter, current); break; case 'comment': isComment(node, arr, formatter); } return arr; } function isText(node, arr) { if(node.parentNode.tagName == 'pre'){ //源码模式下输入html标签,不能做转换处理,直接输出 arr.push(node.data) }else{ arr.push(notTransTagName[node.parentNode.tagName] ? utils.html(node.data) : node.data.replace(/[ ]{2}/g,'  ')) } } function isElement(node, arr, formatter, current) { var attrhtml = ''; if (node.attrs) { attrhtml = []; var attrs = node.attrs; for (var a in attrs) { //这里就针对 //

    '

    //这里边的\"做转换,要不用innerHTML直接被截断了,属性src //有可能做的不够 attrhtml.push(a + (attrs[a] !== undefined ? '="' + (notTransAttrs[a] ? utils.html(attrs[a]).replace(/["]/g, function (a) { return '"' }) : utils.unhtml(attrs[a])) + '"' : '')) } attrhtml = attrhtml.join(' '); } arr.push('<' + node.tagName + (attrhtml ? ' ' + attrhtml : '') + (dtd.$empty[node.tagName] ? '\/' : '' ) + '>' ); //插入新行 if (formatter && !dtd.$inlineWithA[node.tagName] && node.tagName != 'pre') { if(node.children && node.children.length){ current = insertLine(arr, current, true); insertIndent(arr, current) } } if (node.children && node.children.length) { for (var i = 0, ci; ci = node.children[i++];) { if (formatter && ci.type == 'element' && !dtd.$inlineWithA[ci.tagName] && i > 1) { insertLine(arr, current); insertIndent(arr, current) } nodeToHtml(ci, arr, formatter, current) } } if (!dtd.$empty[node.tagName]) { if (formatter && !dtd.$inlineWithA[node.tagName] && node.tagName != 'pre') { if(node.children && node.children.length){ current = insertLine(arr, current); insertIndent(arr, current) } } arr.push('<\/' + node.tagName + '>'); } } function isComment(node, arr) { arr.push(''); } function getNodeById(root, id) { var node; if (root.type == 'element' && root.getAttr('id') == id) { return root; } if (root.children && root.children.length) { for (var i = 0, ci; ci = root.children[i++];) { if (node = getNodeById(ci, id)) { return node; } } } } function getNodesByTagName(node, tagName, arr) { if (node.type == 'element' && node.tagName == tagName) { arr.push(node); } if (node.children && node.children.length) { for (var i = 0, ci; ci = node.children[i++];) { getNodesByTagName(ci, tagName, arr) } } } function nodeTraversal(root,fn){ if(root.children && root.children.length){ for(var i= 0,ci;ci=root.children[i];){ nodeTraversal(ci,fn); //ci被替换的情况,这里就不再走 fn了 if(ci.parentNode ){ if(ci.children && ci.children.length){ fn(ci) } if(ci.parentNode) i++ } } }else{ fn(root) } } uNode.prototype = { /** * 当前节点对象,转换成html文本 * @method toHtml * @return { String } 返回转换后的html字符串 * @example * ```javascript * node.toHtml(); * ``` */ /** * 当前节点对象,转换成html文本 * @method toHtml * @param { Boolean } formatter 是否格式化返回值 * @return { String } 返回转换后的html字符串 * @example * ```javascript * node.toHtml( true ); * ``` */ toHtml:function (formatter) { var arr = []; nodeToHtml(this, arr, formatter, 0); return arr.join('') }, /** * 获取节点的html内容 * @method innerHTML * @warning 假如节点的type不是'element',或节点的标签名称不在dtd列表里,直接返回当前节点 * @return { String } 返回节点的html内容 * @example * ```javascript * var htmlstr = node.innerHTML(); * ``` */ /** * 设置节点的html内容 * @method innerHTML * @warning 假如节点的type不是'element',或节点的标签名称不在dtd列表里,直接返回当前节点 * @param { String } htmlstr 传入要设置的html内容 * @return { UE.uNode } 返回节点本身 * @example * ```javascript * node.innerHTML('text'); * ``` */ innerHTML:function (htmlstr) { if (this.type != 'element' || dtd.$empty[this.tagName]) { return this; } if (utils.isString(htmlstr)) { if(this.children){ for (var i = 0, ci; ci = this.children[i++];) { ci.parentNode = null; } } this.children = []; var tmpRoot = UE.htmlparser(htmlstr); for (var i = 0, ci; ci = tmpRoot.children[i++];) { this.children.push(ci); ci.parentNode = this; } return this; } else { var tmpRoot = new UE.uNode({ type:'root', children:this.children }); return tmpRoot.toHtml(); } }, /** * 获取节点的纯文本内容 * @method innerText * @warning 假如节点的type不是'element',或节点的标签名称不在dtd列表里,直接返回当前节点 * @return { String } 返回节点的存文本内容 * @example * ```javascript * var textStr = node.innerText(); * ``` */ /** * 设置节点的纯文本内容 * @method innerText * @warning 假如节点的type不是'element',或节点的标签名称不在dtd列表里,直接返回当前节点 * @param { String } textStr 传入要设置的文本内容 * @return { UE.uNode } 返回节点本身 * @example * ```javascript * node.innerText('text'); * ``` */ innerText:function (textStr,noTrans) { if (this.type != 'element' || dtd.$empty[this.tagName]) { return this; } if (textStr) { if(this.children){ for (var i = 0, ci; ci = this.children[i++];) { ci.parentNode = null; } } this.children = []; this.appendChild(uNode.createText(textStr,noTrans)); return this; } else { return this.toHtml().replace(/<[^>]+>/g, ''); } }, /** * 获取当前对象的data属性 * @method getData * @return { Object } 若节点的type值是elemenet,返回空字符串,否则返回节点的data属性 * @example * ```javascript * node.getData(); * ``` */ getData:function () { if (this.type == 'element') return ''; return this.data }, /** * 获取当前节点下的第一个子节点 * @method firstChild * @return { UE.uNode } 返回第一个子节点 * @example * ```javascript * node.firstChild(); //返回第一个子节点 * ``` */ firstChild:function () { // if (this.type != 'element' || dtd.$empty[this.tagName]) { // return this; // } return this.children ? this.children[0] : null; }, /** * 获取当前节点下的最后一个子节点 * @method lastChild * @return { UE.uNode } 返回最后一个子节点 * @example * ```javascript * node.lastChild(); //返回最后一个子节点 * ``` */ lastChild:function () { // if (this.type != 'element' || dtd.$empty[this.tagName] ) { // return this; // } return this.children ? this.children[this.children.length - 1] : null; }, /** * 获取和当前节点有相同父亲节点的前一个节点 * @method previousSibling * @return { UE.uNode } 返回前一个节点 * @example * ```javascript * node.children[2].previousSibling(); //返回子节点node.children[1] * ``` */ previousSibling : function(){ var parent = this.parentNode; for (var i = 0, ci; ci = parent.children[i]; i++) { if (ci === this) { return i == 0 ? null : parent.children[i-1]; } } }, /** * 获取和当前节点有相同父亲节点的后一个节点 * @method nextSibling * @return { UE.uNode } 返回后一个节点,找不到返回null * @example * ```javascript * node.children[2].nextSibling(); //如果有,返回子节点node.children[3] * ``` */ nextSibling : function(){ var parent = this.parentNode; for (var i = 0, ci; ci = parent.children[i++];) { if (ci === this) { return parent.children[i]; } } }, /** * 用新的节点替换当前节点 * @method replaceChild * @param { UE.uNode } target 要替换成该节点参数 * @param { UE.uNode } source 要被替换掉的节点 * @return { UE.uNode } 返回替换之后的节点对象 * @example * ```javascript * node.replaceChild(newNode, childNode); //用newNode替换childNode,childNode是node的子节点 * ``` */ replaceChild:function (target, source) { if (this.children) { if(target.parentNode){ target.parentNode.removeChild(target); } for (var i = 0, ci; ci = this.children[i]; i++) { if (ci === source) { this.children.splice(i, 1, target); source.parentNode = null; target.parentNode = this; return target; } } } }, /** * 在节点的子节点列表最后位置插入一个节点 * @method appendChild * @param { UE.uNode } node 要插入的节点 * @return { UE.uNode } 返回刚插入的子节点 * @example * ```javascript * node.appendChild( newNode ); //在node内插入子节点newNode * ``` */ appendChild:function (node) { if (this.type == 'root' || (this.type == 'element' && !dtd.$empty[this.tagName])) { if (!this.children) { this.children = [] } if(node.parentNode){ node.parentNode.removeChild(node); } for (var i = 0, ci; ci = this.children[i]; i++) { if (ci === node) { this.children.splice(i, 1); break; } } this.children.push(node); node.parentNode = this; return node; } }, /** * 在传入节点的前面插入一个节点 * @method insertBefore * @param { UE.uNode } target 要插入的节点 * @param { UE.uNode } source 在该参数节点前面插入 * @return { UE.uNode } 返回刚插入的子节点 * @example * ```javascript * node.parentNode.insertBefore(newNode, node); //在node节点后面插入newNode * ``` */ insertBefore:function (target, source) { if (this.children) { if(target.parentNode){ target.parentNode.removeChild(target); } for (var i = 0, ci; ci = this.children[i]; i++) { if (ci === source) { this.children.splice(i, 0, target); target.parentNode = this; return target; } } } }, /** * 在传入节点的后面插入一个节点 * @method insertAfter * @param { UE.uNode } target 要插入的节点 * @param { UE.uNode } source 在该参数节点后面插入 * @return { UE.uNode } 返回刚插入的子节点 * @example * ```javascript * node.parentNode.insertAfter(newNode, node); //在node节点后面插入newNode * ``` */ insertAfter:function (target, source) { if (this.children) { if(target.parentNode){ target.parentNode.removeChild(target); } for (var i = 0, ci; ci = this.children[i]; i++) { if (ci === source) { this.children.splice(i + 1, 0, target); target.parentNode = this; return target; } } } }, /** * 从当前节点的子节点列表中,移除节点 * @method removeChild * @param { UE.uNode } node 要移除的节点引用 * @param { Boolean } keepChildren 是否保留移除节点的子节点,若传入true,自动把移除节点的子节点插入到移除的位置 * @return { * } 返回刚移除的子节点 * @example * ```javascript * node.removeChild(childNode,true); //在node的子节点列表中移除child节点,并且吧child的子节点插入到移除的位置 * ``` */ removeChild:function (node,keepChildren) { if (this.children) { for (var i = 0, ci; ci = this.children[i]; i++) { if (ci === node) { this.children.splice(i, 1); ci.parentNode = null; if(keepChildren && ci.children && ci.children.length){ for(var j= 0,cj;cj=ci.children[j];j++){ this.children.splice(i+j,0,cj); cj.parentNode = this; } } return ci; } } } }, /** * 获取当前节点所代表的元素属性,即获取attrs对象下的属性值 * @method getAttr * @param { String } attrName 要获取的属性名称 * @return { * } 返回attrs对象下的属性值 * @example * ```javascript * node.getAttr('title'); * ``` */ getAttr:function (attrName) { return this.attrs && this.attrs[attrName.toLowerCase()] }, /** * 设置当前节点所代表的元素属性,即设置attrs对象下的属性值 * @method setAttr * @param { String } attrName 要设置的属性名称 * @param { * } attrVal 要设置的属性值,类型视设置的属性而定 * @return { * } 返回attrs对象下的属性值 * @example * ```javascript * node.setAttr('title','标题'); * ``` */ setAttr:function (attrName, attrVal) { if (!attrName) { delete this.attrs; return; } if(!this.attrs){ this.attrs = {}; } if (utils.isObject(attrName)) { for (var a in attrName) { if (!attrName[a]) { delete this.attrs[a] } else { this.attrs[a.toLowerCase()] = attrName[a]; } } } else { if (!attrVal) { delete this.attrs[attrName] } else { this.attrs[attrName.toLowerCase()] = attrVal; } } }, /** * 获取当前节点在父节点下的位置索引 * @method getIndex * @return { Number } 返回索引数值,如果没有父节点,返回-1 * @example * ```javascript * node.getIndex(); * ``` */ getIndex:function(){ var parent = this.parentNode; for(var i= 0,ci;ci=parent.children[i];i++){ if(ci === this){ return i; } } return -1; }, /** * 在当前节点下,根据id查找节点 * @method getNodeById * @param { String } id 要查找的id * @return { UE.uNode } 返回找到的节点 * @example * ```javascript * node.getNodeById('textId'); * ``` */ getNodeById:function (id) { var node; if (this.children && this.children.length) { for (var i = 0, ci; ci = this.children[i++];) { if (node = getNodeById(ci, id)) { return node; } } } }, /** * 在当前节点下,根据元素名称查找节点列表 * @method getNodesByTagName * @param { String } tagNames 要查找的元素名称 * @return { Array } 返回找到的节点列表 * @example * ```javascript * node.getNodesByTagName('span'); * ``` */ getNodesByTagName:function (tagNames) { tagNames = utils.trim(tagNames).replace(/[ ]{2,}/g, ' ').split(' '); var arr = [], me = this; utils.each(tagNames, function (tagName) { if (me.children && me.children.length) { for (var i = 0, ci; ci = me.children[i++];) { getNodesByTagName(ci, tagName, arr) } } }); return arr; }, /** * 根据样式名称,获取节点的样式值 * @method getStyle * @param { String } name 要获取的样式名称 * @return { String } 返回样式值 * @example * ```javascript * node.getStyle('font-size'); * ``` */ getStyle:function (name) { var cssStyle = this.getAttr('style'); if (!cssStyle) { return '' } var reg = new RegExp('(^|;)\\s*' + name + ':([^;]+)','i'); var match = cssStyle.match(reg); if (match && match[0]) { return match[2] } return ''; }, /** * 给节点设置样式 * @method setStyle * @param { String } name 要设置的的样式名称 * @param { String } val 要设置的的样值 * @example * ```javascript * node.setStyle('font-size', '12px'); * ``` */ setStyle:function (name, val) { function exec(name, val) { var reg = new RegExp('(^|;)\\s*' + name + ':([^;]+;?)', 'gi'); cssStyle = cssStyle.replace(reg, '$1'); if (val) { cssStyle = name + ':' + utils.unhtml(val) + ';' + cssStyle } } var cssStyle = this.getAttr('style'); if (!cssStyle) { cssStyle = ''; } if (utils.isObject(name)) { for (var a in name) { exec(a, name[a]) } } else { exec(name, val) } this.setAttr('style', utils.trim(cssStyle)) }, /** * 传入一个函数,递归遍历当前节点下的所有节点 * @method traversal * @param { Function } fn 遍历到节点的时,传入节点作为参数,运行此函数 * @example * ```javascript * traversal(node, function(){ * console.log(node.type); * }); * ``` */ traversal:function(fn){ if(this.children && this.children.length){ nodeTraversal(this,fn); } return this; } } })(); // core/htmlparser.js /** * html字符串转换成uNode节点 * @file * @module UE * @since 1.2.6.1 */ /** * UEditor公用空间,UEditor所有的功能都挂载在该空间下 * @unfile * @module UE */ /** * html字符串转换成uNode节点的静态方法 * @method htmlparser * @param { String } htmlstr 要转换的html代码 * @param { Boolean } ignoreBlank 若设置为true,转换的时候忽略\n\r\t等空白字符 * @return { uNode } 给定的html片段转换形成的uNode对象 * @example * ```javascript * var root = UE.htmlparser('

    htmlparser

    ', true); * ``` */ var htmlparser = UE.htmlparser = function (htmlstr,ignoreBlank) { //todo 原来的方式 [^"'<>\/] 有\/就不能配对上 ') } html.push('') } //禁止指定table-width return '
    这样的标签了 //先去掉了,加上的原因忘了,这里先记录 var re_tag = /<(?:(?:\/([^>]+)>)|(?:!--([\S|\s]*?)-->)|(?:([^\s\/<>]+)\s*((?:(?:"[^"]*")|(?:'[^']*')|[^"'<>])*)\/?>))/g, re_attr = /([\w\-:.]+)(?:(?:\s*=\s*(?:(?:"([^"]*)")|(?:'([^']*)')|([^\s>]+)))|(?=\s|$))/g; //ie下取得的html可能会有\n存在,要去掉,在处理replace(/[\t\r\n]*/g,'');代码高量的\n不能去除 var allowEmptyTags = { b:1,code:1,i:1,u:1,strike:1,s:1,tt:1,strong:1,q:1,samp:1,em:1,span:1, sub:1,img:1,sup:1,font:1,big:1,small:1,iframe:1,a:1,br:1,pre:1 }; htmlstr = htmlstr.replace(new RegExp(domUtils.fillChar, 'g'), ''); if(!ignoreBlank){ htmlstr = htmlstr.replace(new RegExp('[\\r\\t\\n'+(ignoreBlank?'':' ')+']*<\/?(\\w+)\\s*(?:[^>]*)>[\\r\\t\\n'+(ignoreBlank?'':' ')+']*','g'), function(a,b){ //br暂时单独处理 if(b && allowEmptyTags[b.toLowerCase()]){ return a.replace(/(^[\n\r]+)|([\n\r]+$)/g,''); } return a.replace(new RegExp('^[\\r\\n'+(ignoreBlank?'':' ')+']+'),'').replace(new RegExp('[\\r\\n'+(ignoreBlank?'':' ')+']+$'),''); }); } var notTransAttrs = { 'href':1, 'src':1 }; var uNode = UE.uNode, needParentNode = { 'td':'tr', 'tr':['tbody','thead','tfoot'], 'tbody':'table', 'th':'tr', 'thead':'table', 'tfoot':'table', 'caption':'table', 'li':['ul', 'ol'], 'dt':'dl', 'dd':'dl', 'option':'select' }, needChild = { 'ol':'li', 'ul':'li' }; function text(parent, data) { if(needChild[parent.tagName]){ var tmpNode = uNode.createElement(needChild[parent.tagName]); parent.appendChild(tmpNode); tmpNode.appendChild(uNode.createText(data)); parent = tmpNode; }else{ parent.appendChild(uNode.createText(data)); } } function element(parent, tagName, htmlattr) { var needParentTag; if (needParentTag = needParentNode[tagName]) { var tmpParent = parent,hasParent; while(tmpParent.type != 'root'){ if(utils.isArray(needParentTag) ? utils.indexOf(needParentTag, tmpParent.tagName) != -1 : needParentTag == tmpParent.tagName){ parent = tmpParent; hasParent = true; break; } tmpParent = tmpParent.parentNode; } if(!hasParent){ parent = element(parent, utils.isArray(needParentTag) ? needParentTag[0] : needParentTag) } } //按dtd处理嵌套 // if(parent.type != 'root' && !dtd[parent.tagName][tagName]) // parent = parent.parentNode; var elm = new uNode({ parentNode:parent, type:'element', tagName:tagName.toLowerCase(), //是自闭合的处理一下 children:dtd.$empty[tagName] ? null : [] }); //如果属性存在,处理属性 if (htmlattr) { var attrs = {}, match; while (match = re_attr.exec(htmlattr)) { attrs[match[1].toLowerCase()] = notTransAttrs[match[1].toLowerCase()] ? (match[2] || match[3] || match[4]) : utils.unhtml(match[2] || match[3] || match[4]) } elm.attrs = attrs; } //trace:3970 // //如果parent下不能放elm // if(dtd.$inline[parent.tagName] && dtd.$block[elm.tagName] && !dtd[parent.tagName][elm.tagName]){ // parent = parent.parentNode; // elm.parentNode = parent; // } parent.children.push(elm); //如果是自闭合节点返回父亲节点 return dtd.$empty[tagName] ? parent : elm } function comment(parent, data) { parent.children.push(new uNode({ type:'comment', data:data, parentNode:parent })); } var match, currentIndex = 0, nextIndex = 0; //设置根节点 var root = new uNode({ type:'root', children:[] }); var currentParent = root; while (match = re_tag.exec(htmlstr)) { currentIndex = match.index; try{ if (currentIndex > nextIndex) { //text node text(currentParent, htmlstr.slice(nextIndex, currentIndex)); } if (match[3]) { if(dtd.$cdata[currentParent.tagName]){ text(currentParent, match[0]); }else{ //start tag currentParent = element(currentParent, match[3].toLowerCase(), match[4]); } } else if (match[1]) { if(currentParent.type != 'root'){ if(dtd.$cdata[currentParent.tagName] && !dtd.$cdata[match[1]]){ text(currentParent, match[0]); }else{ var tmpParent = currentParent; while(currentParent.type == 'element' && currentParent.tagName != match[1].toLowerCase()){ currentParent = currentParent.parentNode; if(currentParent.type == 'root'){ currentParent = tmpParent; throw 'break' } } //end tag currentParent = currentParent.parentNode; } } } else if (match[2]) { //comment comment(currentParent, match[2]) } }catch(e){} nextIndex = re_tag.lastIndex; } //如果结束是文本,就有可能丢掉,所以这里手动判断一下 //例如
  • sdfsdfsdf
  • sdfsdfsdfsdf if (nextIndex < htmlstr.length) { text(currentParent, htmlstr.slice(nextIndex)); } return root; }; // core/filternode.js /** * UE过滤节点的静态方法 * @file */ /** * UEditor公用空间,UEditor所有的功能都挂载在该空间下 * @module UE */ /** * 根据传入节点和过滤规则过滤相应节点 * @module UE * @since 1.2.6.1 * @method filterNode * @param { Object } root 指定root节点 * @param { Object } rules 过滤规则json对象 * @example * ```javascript * UE.filterNode(root,editor.options.filterRules); * ``` */ var filterNode = UE.filterNode = function () { function filterNode(node,rules){ switch (node.type) { case 'text': break; case 'element': var val; if(val = rules[node.tagName]){ if(val === '-'){ node.parentNode.removeChild(node) }else if(utils.isFunction(val)){ var parentNode = node.parentNode, index = node.getIndex(); val(node); if(node.parentNode){ if(node.children){ for(var i = 0,ci;ci=node.children[i];){ filterNode(ci,rules); if(ci.parentNode){ i++; } } } }else{ for(var i = index,ci;ci=parentNode.children[i];){ filterNode(ci,rules); if(ci.parentNode){ i++; } } } }else{ var attrs = val['$']; if(attrs && node.attrs){ var tmpAttrs = {},tmpVal; for(var a in attrs){ tmpVal = node.getAttr(a); //todo 只先对style单独处理 if(a == 'style' && utils.isArray(attrs[a])){ var tmpCssStyle = []; utils.each(attrs[a],function(v){ var tmp; if(tmp = node.getStyle(v)){ tmpCssStyle.push(v + ':' + tmp); } }); tmpVal = tmpCssStyle.join(';') } if(tmpVal){ tmpAttrs[a] = tmpVal; } } node.attrs = tmpAttrs; } if(node.children){ for(var i = 0,ci;ci=node.children[i];){ filterNode(ci,rules); if(ci.parentNode){ i++; } } } } }else{ //如果不在名单里扣出子节点并删除该节点,cdata除外 if(dtd.$cdata[node.tagName]){ node.parentNode.removeChild(node) }else{ var parentNode = node.parentNode, index = node.getIndex(); node.parentNode.removeChild(node,true); for(var i = index,ci;ci=parentNode.children[i];){ filterNode(ci,rules); if(ci.parentNode){ i++; } } } } break; case 'comment': node.parentNode.removeChild(node) } } return function(root,rules){ if(utils.isEmptyObject(rules)){ return root; } var val; if(val = rules['-']){ utils.each(val.split(' '),function(k){ rules[k] = '-' }) } for(var i= 0,ci;ci=root.children[i];){ filterNode(ci,rules); if(ci.parentNode){ i++; } } return root; } }(); // core/plugin.js /** * Created with JetBrains PhpStorm. * User: campaign * Date: 10/8/13 * Time: 6:15 PM * To change this template use File | Settings | File Templates. */ UE.plugin = function(){ var _plugins = {}; return { register : function(pluginName,fn,oldOptionName,afterDisabled){ if(oldOptionName && utils.isFunction(oldOptionName)){ afterDisabled = oldOptionName; oldOptionName = null } _plugins[pluginName] = { optionName : oldOptionName || pluginName, execFn : fn, //当插件被禁用时执行 afterDisabled : afterDisabled } }, load : function(editor){ utils.each(_plugins,function(plugin){ var _export = plugin.execFn.call(editor); if(editor.options[plugin.optionName] !== false){ if(_export){ //后边需要再做扩展 utils.each(_export,function(v,k){ switch(k.toLowerCase()){ case 'shortcutkey': editor.addshortcutkey(v); break; case 'bindevents': utils.each(v,function(fn,eventName){ editor.addListener(eventName,fn); }); break; case 'bindmultievents': utils.each(utils.isArray(v) ? v:[v],function(event){ var types = utils.trim(event.type).split(/\s+/); utils.each(types,function(eventName){ editor.addListener(eventName, event.handler); }); }); break; case 'commands': utils.each(v,function(execFn,execName){ editor.commands[execName] = execFn }); break; case 'outputrule': editor.addOutputRule(v); break; case 'inputrule': editor.addInputRule(v); break; case 'defaultoptions': editor.setOpt(v) } }) } }else if(plugin.afterDisabled){ plugin.afterDisabled.call(editor) } }); //向下兼容 utils.each(UE.plugins,function(plugin){ plugin.call(editor); }); }, run : function(pluginName,editor){ var plugin = _plugins[pluginName]; if(plugin){ plugin.exeFn.call(editor) } } } }(); // core/keymap.js var keymap = UE.keymap = { 'Backspace' : 8, 'Tab' : 9, 'Enter' : 13, 'Shift':16, 'Control':17, 'Alt':18, 'CapsLock':20, 'Esc':27, 'Spacebar':32, 'PageUp':33, 'PageDown':34, 'End':35, 'Home':36, 'Left':37, 'Up':38, 'Right':39, 'Down':40, 'Insert':45, 'Del':46, 'NumLock':144, 'Cmd':91, '=':187, '-':189, "b":66, 'i':73, //回退 'z':90, 'y':89, //粘贴 'v' : 86, 'x' : 88, 's' : 83, 'n' : 78 }; // core/localstorage.js //存储媒介封装 var LocalStorage = UE.LocalStorage = (function () { var storage = window.localStorage || getUserData() || null, LOCAL_FILE = 'localStorage'; return { saveLocalData: function (key, data) { if (storage && data) { storage.setItem(key, data); return true; } return false; }, getLocalData: function (key) { if (storage) { return storage.getItem(key); } return null; }, removeItem: function (key) { storage && storage.removeItem(key); } }; function getUserData() { var container = document.createElement("div"); container.style.display = "none"; if (!container.addBehavior) { return null; } container.addBehavior("#default#userdata"); return { getItem: function (key) { var result = null; try { document.body.appendChild(container); container.load(LOCAL_FILE); result = container.getAttribute(key); document.body.removeChild(container); } catch (e) { } return result; }, setItem: function (key, value) { document.body.appendChild(container); container.setAttribute(key, value); container.save(LOCAL_FILE); document.body.removeChild(container); }, //// 暂时没有用到 //clear: function () { // // var expiresTime = new Date(); // expiresTime.setFullYear(expiresTime.getFullYear() - 1); // document.body.appendChild(container); // container.expires = expiresTime.toUTCString(); // container.save(LOCAL_FILE); // document.body.removeChild(container); // //}, removeItem: function (key) { document.body.appendChild(container); container.removeAttribute(key); container.save(LOCAL_FILE); document.body.removeChild(container); } }; } })(); (function () { var ROOTKEY = 'ueditor_preference'; UE.Editor.prototype.setPreferences = function(key,value){ var obj = {}; if (utils.isString(key)) { obj[ key ] = value; } else { obj = key; } var data = LocalStorage.getLocalData(ROOTKEY); if (data && (data = utils.str2json(data))) { utils.extend(data, obj); } else { data = obj; } data && LocalStorage.saveLocalData(ROOTKEY, utils.json2str(data)); }; UE.Editor.prototype.getPreferences = function(key){ var data = LocalStorage.getLocalData(ROOTKEY); if (data && (data = utils.str2json(data))) { return key ? data[key] : data } return null; }; UE.Editor.prototype.removePreferences = function (key) { var data = LocalStorage.getLocalData(ROOTKEY); if (data && (data = utils.str2json(data))) { data[key] = undefined; delete data[key] } data && LocalStorage.saveLocalData(ROOTKEY, utils.json2str(data)); }; })(); // plugins/defaultfilter.js ///import core ///plugin 编辑器默认的过滤转换机制 UE.plugins['defaultfilter'] = function () { var me = this; me.setOpt({ 'allowDivTransToP':true, 'disabledTableInTable':true }); //默认的过滤处理 //进入编辑器的内容处理 me.addInputRule(function (root) { var allowDivTransToP = this.options.allowDivTransToP; var val; function tdParent(node){ while(node && node.type == 'element'){ if(node.tagName == 'td'){ return true; } node = node.parentNode; } return false; } //进行默认的处理 root.traversal(function (node) { if (node.type == 'element') { if (!dtd.$cdata[node.tagName] && me.options.autoClearEmptyNode && dtd.$inline[node.tagName] && !dtd.$empty[node.tagName] && (!node.attrs || utils.isEmptyObject(node.attrs))) { if (!node.firstChild()) node.parentNode.removeChild(node); else if (node.tagName == 'span' && (!node.attrs || utils.isEmptyObject(node.attrs))) { node.parentNode.removeChild(node, true) } return; } switch (node.tagName) { case 'style': case 'script': node.setAttr({ cdata_tag: node.tagName, cdata_data: (node.innerHTML() || ''), '_ue_custom_node_':'true' }); node.tagName = 'div'; node.innerHTML(''); break; case 'a': if (val = node.getAttr('href')) { node.setAttr('_href', val) } break; case 'img': //todo base64暂时去掉,后边做远程图片上传后,干掉这个 if (val = node.getAttr('src')) { if (/^data:/.test(val)) { node.parentNode.removeChild(node); break; } } node.setAttr('_src', node.getAttr('src')); break; case 'span': if (browser.webkit && (val = node.getStyle('white-space'))) { if (/nowrap|normal/.test(val)) { node.setStyle('white-space', ''); if (me.options.autoClearEmptyNode && utils.isEmptyObject(node.attrs)) { node.parentNode.removeChild(node, true) } } } val = node.getAttr('id'); if(val && /^_baidu_bookmark_/i.test(val)){ node.parentNode.removeChild(node) } break; case 'p': if (val = node.getAttr('align')) { node.setAttr('align'); node.setStyle('text-align', val) } //trace:3431 // var cssStyle = node.getAttr('style'); // if (cssStyle) { // cssStyle = cssStyle.replace(/(margin|padding)[^;]+/g, ''); // node.setAttr('style', cssStyle) // // } //p标签不允许嵌套 utils.each(node.children,function(n){ if(n.type == 'element' && n.tagName == 'p'){ var next = n.nextSibling(); node.parentNode.insertAfter(n,node); var last = n; while(next){ var tmp = next.nextSibling(); node.parentNode.insertAfter(next,last); last = next; next = tmp; } return false; } }); if (!node.firstChild()) { node.innerHTML(browser.ie ? ' ' : '
    ') } break; case 'div': if(node.getAttr('cdata_tag')){ break; } //针对代码这里不处理插入代码的div val = node.getAttr('class'); if(val && /^line number\d+/.test(val)){ break; } if(!allowDivTransToP){ break; } var tmpNode, p = UE.uNode.createElement('p'); while (tmpNode = node.firstChild()) { if (tmpNode.type == 'text' || !UE.dom.dtd.$block[tmpNode.tagName]) { p.appendChild(tmpNode); } else { if (p.firstChild()) { node.parentNode.insertBefore(p, node); p = UE.uNode.createElement('p'); } else { node.parentNode.insertBefore(tmpNode, node); } } } if (p.firstChild()) { node.parentNode.insertBefore(p, node); } node.parentNode.removeChild(node); break; case 'dl': node.tagName = 'ul'; break; case 'dt': case 'dd': node.tagName = 'li'; break; case 'li': var className = node.getAttr('class'); if (!className || !/list\-/.test(className)) { node.setAttr() } var tmpNodes = node.getNodesByTagName('ol ul'); UE.utils.each(tmpNodes, function (n) { node.parentNode.insertAfter(n, node); }); break; case 'td': case 'th': case 'caption': if(!node.children || !node.children.length){ node.appendChild(browser.ie11below ? UE.uNode.createText(' ') : UE.uNode.createElement('br')) } break; case 'table': if(me.options.disabledTableInTable && tdParent(node)){ node.parentNode.insertBefore(UE.uNode.createText(node.innerText()),node); node.parentNode.removeChild(node) } } } // if(node.type == 'comment'){ // node.parentNode.removeChild(node); // } }) }); //从编辑器出去的内容处理 me.addOutputRule(function (root) { var val; root.traversal(function (node) { if (node.type == 'element') { if (me.options.autoClearEmptyNode && dtd.$inline[node.tagName] && !dtd.$empty[node.tagName] && (!node.attrs || utils.isEmptyObject(node.attrs))) { if (!node.firstChild()) node.parentNode.removeChild(node); else if (node.tagName == 'span' && (!node.attrs || utils.isEmptyObject(node.attrs))) { node.parentNode.removeChild(node, true) } return; } switch (node.tagName) { case 'div': if (val = node.getAttr('cdata_tag')) { node.tagName = val; node.appendChild(UE.uNode.createText(node.getAttr('cdata_data'))); node.setAttr({cdata_tag: '', cdata_data: '','_ue_custom_node_':''}); } break; case 'a': if (val = node.getAttr('_href')) { node.setAttr({ 'href': utils.html(val), '_href': '' }) } break; break; case 'span': val = node.getAttr('id'); if(val && /^_baidu_bookmark_/i.test(val)){ node.parentNode.removeChild(node) } break; case 'img': if (val = node.getAttr('_src')) { node.setAttr({ 'src': node.getAttr('_src'), '_src': '' }) } } } }) }); }; // plugins/inserthtml.js /** * 插入html字符串插件 * @file * @since 1.2.6.1 */ /** * 插入html代码 * @command inserthtml * @method execCommand * @param { String } cmd 命令字符串 * @param { String } html 插入的html字符串 * @remaind 插入的标签内容是在当前的选区位置上插入,如果当前是闭合状态,那直接插入内容, 如果当前是选中状态,将先清除当前选中内容后,再做插入 * @warning 注意:该命令会对当前选区的位置,对插入的内容进行过滤转换处理。 过滤的规则遵循html语意化的原则。 * @example * ```javascript * //xxx[BB]xxx 当前选区为非闭合选区,选中BB这两个文本 * //执行命令,插入CC * //插入后的效果 xxxCCxxx * //

    xx|xxx

    当前选区为闭合状态 * //插入

    CC

    * //结果

    xx

    CC

    xxx

    * //

    xxxx

    |

    xxx

    当前选区在两个p标签之间 * //插入 xxxx * //结果

    xxxx

    xxxx

    xxx

    * ``` */ UE.commands['inserthtml'] = { execCommand: function (command,html,notNeedFilter){ var me = this, range, div; if(!html){ return; } if(me.fireEvent('beforeinserthtml',html) === true){ return; } range = me.selection.getRange(); div = range.document.createElement( 'div' ); div.style.display = 'inline'; if (!notNeedFilter) { var root = UE.htmlparser(html); //如果给了过滤规则就先进行过滤 if(me.options.filterRules){ UE.filterNode(root,me.options.filterRules); } //执行默认的处理 me.filterInputRule(root); html = root.toHtml() } div.innerHTML = utils.trim( html ); if ( !range.collapsed ) { var tmpNode = range.startContainer; if(domUtils.isFillChar(tmpNode)){ range.setStartBefore(tmpNode) } tmpNode = range.endContainer; if(domUtils.isFillChar(tmpNode)){ range.setEndAfter(tmpNode) } range.txtToElmBoundary(); //结束边界可能放到了br的前边,要把br包含进来 // x[xxx]
    if(range.endContainer && range.endContainer.nodeType == 1){ tmpNode = range.endContainer.childNodes[range.endOffset]; if(tmpNode && domUtils.isBr(tmpNode)){ range.setEndAfter(tmpNode); } } if(range.startOffset == 0){ tmpNode = range.startContainer; if(domUtils.isBoundaryNode(tmpNode,'firstChild') ){ tmpNode = range.endContainer; if(range.endOffset == (tmpNode.nodeType == 3 ? tmpNode.nodeValue.length : tmpNode.childNodes.length) && domUtils.isBoundaryNode(tmpNode,'lastChild')){ me.body.innerHTML = '

    '+(browser.ie ? '' : '
    ')+'

    '; range.setStart(me.body.firstChild,0).collapse(true) } } } !range.collapsed && range.deleteContents(); if(range.startContainer.nodeType == 1){ var child = range.startContainer.childNodes[range.startOffset],pre; if(child && domUtils.isBlockElm(child) && (pre = child.previousSibling) && domUtils.isBlockElm(pre)){ range.setEnd(pre,pre.childNodes.length).collapse(); while(child.firstChild){ pre.appendChild(child.firstChild); } domUtils.remove(child); } } } var child,parent,pre,tmp,hadBreak = 0, nextNode; //如果当前位置选中了fillchar要干掉,要不会产生空行 if(range.inFillChar()){ child = range.startContainer; if(domUtils.isFillChar(child)){ range.setStartBefore(child).collapse(true); domUtils.remove(child); }else if(domUtils.isFillChar(child,true)){ child.nodeValue = child.nodeValue.replace(fillCharReg,''); range.startOffset--; range.collapsed && range.collapse(true) } } //列表单独处理 var li = domUtils.findParentByTagName(range.startContainer,'li',true); if(li){ var next,last; while(child = div.firstChild){ //针对hr单独处理一下先 while(child && (child.nodeType == 3 || !domUtils.isBlockElm(child) || child.tagName=='HR' )){ next = child.nextSibling; range.insertNode( child).collapse(); last = child; child = next; } if(child){ if(/^(ol|ul)$/i.test(child.tagName)){ while(child.firstChild){ last = child.firstChild; domUtils.insertAfter(li,child.firstChild); li = li.nextSibling; } domUtils.remove(child) }else{ var tmpLi; next = child.nextSibling; tmpLi = me.document.createElement('li'); domUtils.insertAfter(li,tmpLi); tmpLi.appendChild(child); last = child; child = next; li = tmpLi; } } } li = domUtils.findParentByTagName(range.startContainer,'li',true); if(domUtils.isEmptyBlock(li)){ domUtils.remove(li) } if(last){ range.setStartAfter(last).collapse(true).select(true) } }else{ while ( child = div.firstChild ) { if(hadBreak){ var p = me.document.createElement('p'); while(child && (child.nodeType == 3 || !dtd.$block[child.tagName])){ nextNode = child.nextSibling; p.appendChild(child); child = nextNode; } if(p.firstChild){ child = p } } range.insertNode( child ); nextNode = child.nextSibling; if ( !hadBreak && child.nodeType == domUtils.NODE_ELEMENT && domUtils.isBlockElm( child ) ){ parent = domUtils.findParent( child,function ( node ){ return domUtils.isBlockElm( node ); } ); if ( parent && parent.tagName.toLowerCase() != 'body' && !(dtd[parent.tagName][child.nodeName] && child.parentNode === parent)){ if(!dtd[parent.tagName][child.nodeName]){ pre = parent; }else{ tmp = child.parentNode; while (tmp !== parent){ pre = tmp; tmp = tmp.parentNode; } } domUtils.breakParent( child, pre || tmp ); //去掉break后前一个多余的节点

    |<[p> ==>

    |

    var pre = child.previousSibling; domUtils.trimWhiteTextNode(pre); if(!pre.childNodes.length){ domUtils.remove(pre); } //trace:2012,在非ie的情况,切开后剩下的节点有可能不能点入光标添加br占位 if(!browser.ie && (next = child.nextSibling) && domUtils.isBlockElm(next) && next.lastChild && !domUtils.isBr(next.lastChild)){ next.appendChild(me.document.createElement('br')); } hadBreak = 1; } } var next = child.nextSibling; if(!div.firstChild && next && domUtils.isBlockElm(next)){ range.setStart(next,0).collapse(true); break; } range.setEndAfter( child ).collapse(); } child = range.startContainer; if(nextNode && domUtils.isBr(nextNode)){ domUtils.remove(nextNode) } //用chrome可能有空白展位符 if(domUtils.isBlockElm(child) && domUtils.isEmptyNode(child)){ if(nextNode = child.nextSibling){ domUtils.remove(child); if(nextNode.nodeType == 1 && dtd.$block[nextNode.tagName]){ range.setStart(nextNode,0).collapse(true).shrinkBoundary() } }else{ try{ child.innerHTML = browser.ie ? domUtils.fillChar : '
    '; }catch(e){ range.setStartBefore(child); domUtils.remove(child) } } } //加上true因为在删除表情等时会删两次,第一次是删的fillData try{ range.select(true); }catch(e){} } setTimeout(function(){ range = me.selection.getRange(); range.scrollToView(me.autoHeightEnabled,me.autoHeightEnabled ? domUtils.getXY(me.iframe).y:0); me.fireEvent('afterinserthtml', html); },200); } }; // plugins/autotypeset.js /** * 自动排版 * @file * @since 1.2.6.1 */ /** * 对当前编辑器的内容执行自动排版, 排版的行为根据config配置文件里的“autotypeset”选项进行控制。 * @command autotypeset * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'autotypeset' ); * ``` */ UE.plugins['autotypeset'] = function(){ this.setOpt({'autotypeset': { mergeEmptyline: true, //合并空行 removeClass: true, //去掉冗余的class removeEmptyline: false, //去掉空行 textAlign:"left", //段落的排版方式,可以是 left,right,center,justify 去掉这个属性表示不执行排版 imageBlockLine: 'center', //图片的浮动方式,独占一行剧中,左右浮动,默认: center,left,right,none 去掉这个属性表示不执行排版 pasteFilter: false, //根据规则过滤没事粘贴进来的内容 clearFontSize: false, //去掉所有的内嵌字号,使用编辑器默认的字号 clearFontFamily: false, //去掉所有的内嵌字体,使用编辑器默认的字体 removeEmptyNode: false, // 去掉空节点 //可以去掉的标签 removeTagNames: utils.extend({div:1},dtd.$removeEmpty), indent: false, // 行首缩进 indentValue : '2em', //行首缩进的大小 bdc2sb: false, tobdc: false }}); var me = this, opt = me.options.autotypeset, remainClass = { 'selectTdClass':1, 'pagebreak':1, 'anchorclass':1 }, remainTag = { 'li':1 }, tags = { div:1, p:1, //trace:2183 这些也认为是行 blockquote:1,center:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1, span:1 }, highlightCont; //升级了版本,但配置项目里没有autotypeset if(!opt){ return; } readLocalOpts(); function isLine(node,notEmpty){ if(!node || node.nodeType == 3) return 0; if(domUtils.isBr(node)) return 1; if(node && node.parentNode && tags[node.tagName.toLowerCase()]){ if(highlightCont && highlightCont.contains(node) || node.getAttribute('pagebreak') ){ return 0; } return notEmpty ? !domUtils.isEmptyBlock(node) : domUtils.isEmptyBlock(node,new RegExp('[\\s'+domUtils.fillChar +']','g')); } } function removeNotAttributeSpan(node){ if(!node.style.cssText){ domUtils.removeAttributes(node,['style']); if(node.tagName.toLowerCase() == 'span' && domUtils.hasNoAttributes(node)){ domUtils.remove(node,true); } } } function autotype(type,html){ var me = this,cont; if(html){ if(!opt.pasteFilter){ return; } cont = me.document.createElement('div'); cont.innerHTML = html.html; }else{ cont = me.document.body; } var nodes = domUtils.getElementsByTagName(cont,'*'); // 行首缩进,段落方向,段间距,段内间距 for(var i=0,ci;ci=nodes[i++];){ if(me.fireEvent('excludeNodeinautotype',ci) === true){ continue; } //font-size if(opt.clearFontSize && ci.style.fontSize){ domUtils.removeStyle(ci,'font-size'); removeNotAttributeSpan(ci); } //font-family if(opt.clearFontFamily && ci.style.fontFamily){ domUtils.removeStyle(ci,'font-family'); removeNotAttributeSpan(ci); } if(isLine(ci)){ //合并空行 if(opt.mergeEmptyline ){ var next = ci.nextSibling,tmpNode,isBr = domUtils.isBr(ci); while(isLine(next)){ tmpNode = next; next = tmpNode.nextSibling; if(isBr && (!next || next && !domUtils.isBr(next))){ break; } domUtils.remove(tmpNode); } } //去掉空行,保留占位的空行 if(opt.removeEmptyline && domUtils.inDoc(ci,cont) && !remainTag[ci.parentNode.tagName.toLowerCase()] ){ if(domUtils.isBr(ci)){ next = ci.nextSibling; if(next && !domUtils.isBr(next)){ continue; } } domUtils.remove(ci); continue; } } if(isLine(ci,true) && ci.tagName != 'SPAN'){ if(opt.indent){ ci.style.textIndent = opt.indentValue; } if(opt.textAlign){ ci.style.textAlign = opt.textAlign; } // if(opt.lineHeight) // ci.style.lineHeight = opt.lineHeight + 'cm'; } //去掉class,保留的class不去掉 if(opt.removeClass && ci.className && !remainClass[ci.className.toLowerCase()]){ if(highlightCont && highlightCont.contains(ci)){ continue; } domUtils.removeAttributes(ci,['class']); } //表情不处理 if(opt.imageBlockLine && ci.tagName.toLowerCase() == 'img' && !ci.getAttribute('emotion')){ if(html){ var img = ci; switch (opt.imageBlockLine){ case 'left': case 'right': case 'none': var pN = img.parentNode,tmpNode,pre,next; while(dtd.$inline[pN.tagName] || pN.tagName == 'A'){ pN = pN.parentNode; } tmpNode = pN; if(tmpNode.tagName == 'P' && domUtils.getStyle(tmpNode,'text-align') == 'center'){ if(!domUtils.isBody(tmpNode) && domUtils.getChildCount(tmpNode,function(node){return !domUtils.isBr(node) && !domUtils.isWhitespace(node)}) == 1){ pre = tmpNode.previousSibling; next = tmpNode.nextSibling; if(pre && next && pre.nodeType == 1 && next.nodeType == 1 && pre.tagName == next.tagName && domUtils.isBlockElm(pre)){ pre.appendChild(tmpNode.firstChild); while(next.firstChild){ pre.appendChild(next.firstChild); } domUtils.remove(tmpNode); domUtils.remove(next); }else{ domUtils.setStyle(tmpNode,'text-align',''); } } } domUtils.setStyle(img,'float', opt.imageBlockLine); break; case 'center': if(me.queryCommandValue('imagefloat') != 'center'){ pN = img.parentNode; domUtils.setStyle(img,'float','none'); tmpNode = img; while(pN && domUtils.getChildCount(pN,function(node){return !domUtils.isBr(node) && !domUtils.isWhitespace(node)}) == 1 && (dtd.$inline[pN.tagName] || pN.tagName == 'A')){ tmpNode = pN; pN = pN.parentNode; } var pNode = me.document.createElement('p'); domUtils.setAttributes(pNode,{ style:'text-align:center' }); tmpNode.parentNode.insertBefore(pNode,tmpNode); pNode.appendChild(tmpNode); domUtils.setStyle(tmpNode,'float',''); } } } else { var range = me.selection.getRange(); range.selectNode(ci).select(); me.execCommand('imagefloat', opt.imageBlockLine); } } //去掉冗余的标签 if(opt.removeEmptyNode){ if(opt.removeTagNames[ci.tagName.toLowerCase()] && domUtils.hasNoAttributes(ci) && domUtils.isEmptyBlock(ci)){ domUtils.remove(ci); } } } if(opt.tobdc){ var root = UE.htmlparser(cont.innerHTML); root.traversal(function(node){ if(node.type == 'text'){ node.data = ToDBC(node.data) } }); cont.innerHTML = root.toHtml() } if(opt.bdc2sb){ var root = UE.htmlparser(cont.innerHTML); root.traversal(function(node){ if(node.type == 'text'){ node.data = DBC2SB(node.data) } }); cont.innerHTML = root.toHtml() } if(html){ html.html = cont.innerHTML; } } if(opt.pasteFilter){ me.addListener('beforepaste',autotype); } function DBC2SB(str) { var result = ''; for (var i = 0; i < str.length; i++) { var code = str.charCodeAt(i); //获取当前字符的unicode编码 if (code >= 65281 && code <= 65373)//在这个unicode编码范围中的是所有的英文字母已经各种字符 { result += String.fromCharCode(str.charCodeAt(i) - 65248); //把全角字符的unicode编码转换为对应半角字符的unicode码 } else if (code == 12288)//空格 { result += String.fromCharCode(str.charCodeAt(i) - 12288 + 32); } else { result += str.charAt(i); } } return result; } function ToDBC(txtstring) { txtstring = utils.html(txtstring); var tmp = ""; var mark = "";/*用于判断,如果是html尖括里的标记,则不进行全角的转换*/ for (var i = 0; i < txtstring.length; i++) { if (txtstring.charCodeAt(i) == 32) { tmp = tmp + String.fromCharCode(12288); } else if (txtstring.charCodeAt(i) < 127) { tmp = tmp + String.fromCharCode(txtstring.charCodeAt(i) + 65248); } else { tmp += txtstring.charAt(i); } } return tmp; } function readLocalOpts() { var cookieOpt = me.getPreferences('autotypeset'); utils.extend(me.options.autotypeset, cookieOpt); } me.commands['autotypeset'] = { execCommand:function () { me.removeListener('beforepaste',autotype); if(opt.pasteFilter){ me.addListener('beforepaste',autotype); } autotype.call(me) } }; }; // plugins/autosubmit.js /** * 快捷键提交 * @file * @since 1.2.6.1 */ /** * 提交表单 * @command autosubmit * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'autosubmit' ); * ``` */ UE.plugin.register('autosubmit',function(){ return { shortcutkey:{ "autosubmit":"ctrl+13" //手动提交 }, commands:{ 'autosubmit':{ execCommand:function () { var me=this, form = domUtils.findParentByTagName(me.iframe,"form", false); if (form){ if(me.fireEvent("beforesubmit")===false){ return; } me.sync(); form.submit(); } } } } } }); // plugins/background.js /** * 背景插件,为UEditor提供设置背景功能 * @file * @since 1.2.6.1 */ UE.plugin.register('background', function () { var me = this, cssRuleId = 'editor_background', isSetColored, reg = new RegExp('body[\\s]*\\{(.+)\\}', 'i'); function stringToObj(str) { var obj = {}, styles = str.split(';'); utils.each(styles, function (v) { var index = v.indexOf(':'), key = utils.trim(v.substr(0, index)).toLowerCase(); key && (obj[key] = utils.trim(v.substr(index + 1) || '')); }); return obj; } function setBackground(obj) { if (obj) { var styles = []; for (var name in obj) { if (obj.hasOwnProperty(name)) { styles.push(name + ":" + obj[name] + '; '); } } utils.cssRule(cssRuleId, styles.length ? ('body{' + styles.join("") + '}') : '', me.document); } else { utils.cssRule(cssRuleId, '', me.document) } } //重写editor.hasContent方法 var orgFn = me.hasContents; me.hasContents = function(){ if(me.queryCommandValue('background')){ return true } return orgFn.apply(me,arguments); }; return { bindEvents: { 'getAllHtml': function (type, headHtml) { var body = this.body, su = domUtils.getComputedStyle(body, "background-image"), url = ""; if (su.indexOf(me.options.imagePath) > 0) { url = su.substring(su.indexOf(me.options.imagePath), su.length - 1).replace(/"|\(|\)/ig, ""); } else { url = su != "none" ? su.replace(/url\("?|"?\)/ig, "") : ""; } var html = ' '; headHtml.push(html); }, 'aftersetcontent': function () { if(isSetColored == false) setBackground(); } }, inputRule: function (root) { isSetColored = false; utils.each(root.getNodesByTagName('p'), function (p) { var styles = p.getAttr('data-background'); if (styles) { isSetColored = true; setBackground(stringToObj(styles)); p.parentNode.removeChild(p); } }) }, outputRule: function (root) { var me = this, styles = (utils.cssRule(cssRuleId, me.document) || '').replace(/[\n\r]+/g, '').match(reg); if (styles) { root.appendChild(UE.uNode.createElement('


    ')); } }, commands: { 'background': { execCommand: function (cmd, obj) { setBackground(obj); }, queryCommandValue: function () { var me = this, styles = (utils.cssRule(cssRuleId, me.document) || '').replace(/[\n\r]+/g, '').match(reg); return styles ? stringToObj(styles[1]) : null; }, notNeedUndo: true } } } }); // plugins/image.js /** * 图片插入、排版插件 * @file * @since 1.2.6.1 */ /** * 图片对齐方式 * @command imagefloat * @method execCommand * @remind 值center为独占一行居中 * @param { String } cmd 命令字符串 * @param { String } align 对齐方式,可传left、right、none、center * @remaind center表示图片独占一行 * @example * ```javascript * editor.execCommand( 'imagefloat', 'center' ); * ``` */ /** * 如果选区所在位置是图片区域 * @command imagefloat * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { String } 返回图片对齐方式 * @example * ```javascript * editor.queryCommandValue( 'imagefloat' ); * ``` */ UE.commands['imagefloat'] = { execCommand:function (cmd, align) { var me = this, range = me.selection.getRange(); if (!range.collapsed) { var img = range.getClosedNode(); if (img && img.tagName == 'IMG') { switch (align) { case 'left': case 'right': case 'none': var pN = img.parentNode, tmpNode, pre, next; while (dtd.$inline[pN.tagName] || pN.tagName == 'A') { pN = pN.parentNode; } tmpNode = pN; if (tmpNode.tagName == 'P' && domUtils.getStyle(tmpNode, 'text-align') == 'center') { if (!domUtils.isBody(tmpNode) && domUtils.getChildCount(tmpNode, function (node) { return !domUtils.isBr(node) && !domUtils.isWhitespace(node); }) == 1) { pre = tmpNode.previousSibling; next = tmpNode.nextSibling; if (pre && next && pre.nodeType == 1 && next.nodeType == 1 && pre.tagName == next.tagName && domUtils.isBlockElm(pre)) { pre.appendChild(tmpNode.firstChild); while (next.firstChild) { pre.appendChild(next.firstChild); } domUtils.remove(tmpNode); domUtils.remove(next); } else { domUtils.setStyle(tmpNode, 'text-align', ''); } } range.selectNode(img).select(); } domUtils.setStyle(img, 'float', align == 'none' ? '' : align); if(align == 'none'){ domUtils.removeAttributes(img,'align'); } break; case 'center': if (me.queryCommandValue('imagefloat') != 'center') { pN = img.parentNode; domUtils.setStyle(img, 'float', ''); domUtils.removeAttributes(img,'align'); tmpNode = img; while (pN && domUtils.getChildCount(pN, function (node) { return !domUtils.isBr(node) && !domUtils.isWhitespace(node); }) == 1 && (dtd.$inline[pN.tagName] || pN.tagName == 'A')) { tmpNode = pN; pN = pN.parentNode; } range.setStartBefore(tmpNode).setCursor(false); pN = me.document.createElement('div'); pN.appendChild(tmpNode); domUtils.setStyle(tmpNode, 'float', ''); me.execCommand('insertHtml', '

    ' + pN.innerHTML + '

    '); tmpNode = me.document.getElementById('_img_parent_tmp'); tmpNode.removeAttribute('id'); tmpNode = tmpNode.firstChild; range.selectNode(tmpNode).select(); //去掉后边多余的元素 next = tmpNode.parentNode.nextSibling; if (next && domUtils.isEmptyNode(next)) { domUtils.remove(next); } } break; } } } }, queryCommandValue:function () { var range = this.selection.getRange(), startNode, floatStyle; if (range.collapsed) { return 'none'; } startNode = range.getClosedNode(); if (startNode && startNode.nodeType == 1 && startNode.tagName == 'IMG') { floatStyle = domUtils.getComputedStyle(startNode, 'float') || startNode.getAttribute('align'); if (floatStyle == 'none') { floatStyle = domUtils.getComputedStyle(startNode.parentNode, 'text-align') == 'center' ? 'center' : floatStyle; } return { left:1, right:1, center:1 }[floatStyle] ? floatStyle : 'none'; } return 'none'; }, queryCommandState:function () { var range = this.selection.getRange(), startNode; if (range.collapsed) return -1; startNode = range.getClosedNode(); if (startNode && startNode.nodeType == 1 && startNode.tagName == 'IMG') { return 0; } return -1; } }; /** * 插入图片 * @command insertimage * @method execCommand * @param { String } cmd 命令字符串 * @param { Object } opt 属性键值对,这些属性都将被复制到当前插入图片 * @remind 该命令第二个参数可接受一个图片配置项对象的数组,可以插入多张图片, * 此时数组的每一个元素都是一个Object类型的图片属性集合。 * @example * ```javascript * editor.execCommand( 'insertimage', { * src:'a/b/c.jpg', * width:'100', * height:'100' * } ); * ``` * @example * ```javascript * editor.execCommand( 'insertimage', [{ * src:'a/b/c.jpg', * width:'100', * height:'100' * },{ * src:'a/b/d.jpg', * width:'100', * height:'100' * }] ); * ``` */ UE.commands['insertimage'] = { execCommand:function (cmd, opt) { opt = utils.isArray(opt) ? opt : [opt]; if (!opt.length) { return; } var me = this, range = me.selection.getRange(), img = range.getClosedNode(); if(me.fireEvent('beforeinsertimage', opt) === true){ return; } function unhtmlData(imgCi) { utils.each('width,height,border,hspace,vspace'.split(','), function (item) { if (imgCi[item]) { imgCi[item] = parseInt(imgCi[item], 10) || 0; } }); utils.each('src,_src'.split(','), function (item) { if (imgCi[item]) { imgCi[item] = utils.unhtmlForUrl(imgCi[item]); } }); utils.each('title,alt'.split(','), function (item) { if (imgCi[item]) { imgCi[item] = utils.unhtml(imgCi[item]); } }); } if (img && /img/i.test(img.tagName) && (img.className != "edui-faked-video" || img.className.indexOf("edui-upload-video")!=-1) && !img.getAttribute("word_img")) { var first = opt.shift(); var floatStyle = first['floatStyle']; delete first['floatStyle']; //// img.style.border = (first.border||0) +"px solid #000"; //// img.style.margin = (first.margin||0) +"px"; // img.style.cssText += ';margin:' + (first.margin||0) +"px;" + 'border:' + (first.border||0) +"px solid #000"; domUtils.setAttributes(img, first); me.execCommand('imagefloat', floatStyle); if (opt.length > 0) { range.setStartAfter(img).setCursor(false, true); me.execCommand('insertimage', opt); } } else { var html = [], str = '', ci; ci = opt[0]; if (opt.length == 1) { unhtmlData(ci); str = '' + ci.alt + ''; if (ci['floatStyle'] == 'center') { str = '

    ' + str + '

    '; } html.push(str); } else { for (var i = 0; ci = opt[i++];) { unhtmlData(ci); str = '

    '; html.push(str); } } me.execCommand('insertHtml', html.join('')); } me.fireEvent('afterinsertimage', opt) } }; // plugins/justify.js /** * 段落格式 * @file * @since 1.2.6.1 */ /** * 段落对齐方式 * @command justify * @method execCommand * @param { String } cmd 命令字符串 * @param { String } align 对齐方式:left => 居左,right => 居右,center => 居中,justify => 两端对齐 * @example * ```javascript * editor.execCommand( 'justify', 'center' ); * ``` */ /** * 如果选区所在位置是段落区域,返回当前段落对齐方式 * @command justify * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { String } 返回段落对齐方式 * @example * ```javascript * editor.queryCommandValue( 'justify' ); * ``` */ UE.plugins['justify']=function(){ var me=this, block = domUtils.isBlockElm, defaultValue = { left:1, right:1, center:1, justify:1 }, doJustify = function (range, style) { var bookmark = range.createBookmark(), filterFn = function (node) { return node.nodeType == 1 ? node.tagName.toLowerCase() != 'br' && !domUtils.isBookmarkNode(node) : !domUtils.isWhitespace(node); }; range.enlarge(true); var bookmark2 = range.createBookmark(), current = domUtils.getNextDomNode(bookmark2.start, false, filterFn), tmpRange = range.cloneRange(), tmpNode; while (current && !(domUtils.getPosition(current, bookmark2.end) & domUtils.POSITION_FOLLOWING)) { if (current.nodeType == 3 || !block(current)) { tmpRange.setStartBefore(current); while (current && current !== bookmark2.end && !block(current)) { tmpNode = current; current = domUtils.getNextDomNode(current, false, null, function (node) { return !block(node); }); } tmpRange.setEndAfter(tmpNode); var common = tmpRange.getCommonAncestor(); if (!domUtils.isBody(common) && block(common)) { domUtils.setStyles(common, utils.isString(style) ? {'text-align':style} : style); current = common; } else { var p = range.document.createElement('p'); domUtils.setStyles(p, utils.isString(style) ? {'text-align':style} : style); var frag = tmpRange.extractContents(); p.appendChild(frag); tmpRange.insertNode(p); current = p; } current = domUtils.getNextDomNode(current, false, filterFn); } else { current = domUtils.getNextDomNode(current, true, filterFn); } } return range.moveToBookmark(bookmark2).moveToBookmark(bookmark); }; UE.commands['justify'] = { execCommand:function (cmdName, align) { var range = this.selection.getRange(), txt; //闭合时单独处理 if (range.collapsed) { txt = this.document.createTextNode('p'); range.insertNode(txt); } doJustify(range, align); if (txt) { range.setStartBefore(txt).collapse(true); domUtils.remove(txt); } range.select(); return true; }, queryCommandValue:function () { var startNode = this.selection.getStart(), value = domUtils.getComputedStyle(startNode, 'text-align'); return defaultValue[value] ? value : 'left'; }, queryCommandState:function () { var start = this.selection.getStart(), cell = start && domUtils.findParentByTagName(start, ["td", "th","caption"], true); return cell? -1:0; } }; }; // plugins/font.js /** * 字体颜色,背景色,字号,字体,下划线,删除线 * @file * @since 1.2.6.1 */ /** * 字体颜色 * @command forecolor * @method execCommand * @param { String } cmd 命令字符串 * @param { String } value 色值(必须十六进制) * @example * ```javascript * editor.execCommand( 'forecolor', '#000' ); * ``` */ /** * 返回选区字体颜色 * @command forecolor * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { String } 返回字体颜色 * @example * ```javascript * editor.queryCommandValue( 'forecolor' ); * ``` */ /** * 字体背景颜色 * @command backcolor * @method execCommand * @param { String } cmd 命令字符串 * @param { String } value 色值(必须十六进制) * @example * ```javascript * editor.execCommand( 'backcolor', '#000' ); * ``` */ /** * 返回选区字体颜色 * @command backcolor * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { String } 返回字体背景颜色 * @example * ```javascript * editor.queryCommandValue( 'backcolor' ); * ``` */ /** * 字体大小 * @command fontsize * @method execCommand * @param { String } cmd 命令字符串 * @param { String } value 字体大小 * @example * ```javascript * editor.execCommand( 'fontsize', '14px' ); * ``` */ /** * 返回选区字体大小 * @command fontsize * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { String } 返回字体大小 * @example * ```javascript * editor.queryCommandValue( 'fontsize' ); * ``` */ /** * 字体样式 * @command fontfamily * @method execCommand * @param { String } cmd 命令字符串 * @param { String } value 字体样式 * @example * ```javascript * editor.execCommand( 'fontfamily', '微软雅黑' ); * ``` */ /** * 返回选区字体样式 * @command fontfamily * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { String } 返回字体样式 * @example * ```javascript * editor.queryCommandValue( 'fontfamily' ); * ``` */ /** * 字体下划线,与删除线互斥 * @command underline * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'underline' ); * ``` */ /** * 字体删除线,与下划线互斥 * @command strikethrough * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'strikethrough' ); * ``` */ /** * 字体边框 * @command fontborder * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'fontborder' ); * ``` */ UE.plugins['font'] = function () { var me = this, fonts = { 'forecolor': 'color', 'backcolor': 'background-color', 'fontsize': 'font-size', 'fontfamily': 'font-family', 'underline': 'text-decoration', 'strikethrough': 'text-decoration', 'fontborder': 'border' }, needCmd = {'underline': 1, 'strikethrough': 1, 'fontborder': 1}, needSetChild = { 'forecolor': 'color', 'backcolor': 'background-color', 'fontsize': 'font-size', 'fontfamily': 'font-family' }; me.setOpt({ 'fontfamily': [ { name: 'songti', val: '宋体,SimSun'}, { name: 'yahei', val: '微软雅黑,Microsoft YaHei'}, { name: 'kaiti', val: '楷体,楷体_GB2312, SimKai'}, { name: 'heiti', val: '黑体, SimHei'}, { name: 'lishu', val: '隶书, SimLi'}, { name: 'andaleMono', val: 'andale mono'}, { name: 'arial', val: 'arial, helvetica,sans-serif'}, { name: 'arialBlack', val: 'arial black,avant garde'}, { name: 'comicSansMs', val: 'comic sans ms'}, { name: 'impact', val: 'impact,chicago'}, { name: 'timesNewRoman', val: 'times new roman'} ], 'fontsize': [10, 11, 12, 14, 16, 18, 20, 24, 36] }); function mergeWithParent(node){ var parent; while(parent = node.parentNode){ if(parent.tagName == 'SPAN' && domUtils.getChildCount(parent,function(child){ return !domUtils.isBookmarkNode(child) && !domUtils.isBr(child) }) == 1) { parent.style.cssText += node.style.cssText; domUtils.remove(node,true); node = parent; }else{ break; } } } function mergeChild(rng,cmdName,value){ if(needSetChild[cmdName]){ rng.adjustmentBoundary(); if(!rng.collapsed && rng.startContainer.nodeType == 1){ var start = rng.startContainer.childNodes[rng.startOffset]; if(start && domUtils.isTagNode(start,'span')){ var bk = rng.createBookmark(); utils.each(domUtils.getElementsByTagName(start, 'span'), function (span) { if (!span.parentNode || domUtils.isBookmarkNode(span))return; if(cmdName == 'backcolor' && domUtils.getComputedStyle(span,'background-color').toLowerCase() === value){ return; } domUtils.removeStyle(span,needSetChild[cmdName]); if(span.style.cssText.replace(/^\s+$/,'').length == 0){ domUtils.remove(span,true) } }); rng.moveToBookmark(bk) } } } } function mergesibling(rng,cmdName,value) { var collapsed = rng.collapsed, bk = rng.createBookmark(), common; if (collapsed) { common = bk.start.parentNode; while (dtd.$inline[common.tagName]) { common = common.parentNode; } } else { common = domUtils.getCommonAncestor(bk.start, bk.end); } utils.each(domUtils.getElementsByTagName(common, 'span'), function (span) { if (!span.parentNode || domUtils.isBookmarkNode(span))return; if (/\s*border\s*:\s*none;?\s*/i.test(span.style.cssText)) { if(/^\s*border\s*:\s*none;?\s*$/.test(span.style.cssText)){ domUtils.remove(span, true); }else{ domUtils.removeStyle(span,'border'); } return } if (/border/i.test(span.style.cssText) && span.parentNode.tagName == 'SPAN' && /border/i.test(span.parentNode.style.cssText)) { span.style.cssText = span.style.cssText.replace(/border[^:]*:[^;]+;?/gi, ''); } if(!(cmdName=='fontborder' && value=='none')){ var next = span.nextSibling; while (next && next.nodeType == 1 && next.tagName == 'SPAN' ) { if(domUtils.isBookmarkNode(next) && cmdName == 'fontborder') { span.appendChild(next); next = span.nextSibling; continue; } if (next.style.cssText == span.style.cssText) { domUtils.moveChild(next, span); domUtils.remove(next); } if (span.nextSibling === next) break; next = span.nextSibling; } } mergeWithParent(span); if(browser.ie && browser.version > 8 ){ //拷贝父亲们的特别的属性,这里只做背景颜色的处理 var parent = domUtils.findParent(span,function(n){return n.tagName == 'SPAN' && /background-color/.test(n.style.cssText)}); if(parent && !/background-color/.test(span.style.cssText)){ span.style.backgroundColor = parent.style.backgroundColor; } } }); rng.moveToBookmark(bk); mergeChild(rng,cmdName,value) } me.addInputRule(function (root) { utils.each(root.getNodesByTagName('u s del font strike'), function (node) { if (node.tagName == 'font') { var cssStyle = []; for (var p in node.attrs) { switch (p) { case 'size': cssStyle.push('font-size:' + ({ '1':'10', '2':'12', '3':'16', '4':'18', '5':'24', '6':'32', '7':'48' }[node.attrs[p]] || node.attrs[p]) + 'px'); break; case 'color': cssStyle.push('color:' + node.attrs[p]); break; case 'face': cssStyle.push('font-family:' + node.attrs[p]); break; case 'style': cssStyle.push(node.attrs[p]); } } node.attrs = { 'style': cssStyle.join(';') }; } else { var val = node.tagName == 'u' ? 'underline' : 'line-through'; node.attrs = { 'style': (node.getAttr('style') || '') + 'text-decoration:' + val + ';' } } node.tagName = 'span'; }); // utils.each(root.getNodesByTagName('span'), function (node) { // var val; // if(val = node.getAttr('class')){ // if(/fontstrikethrough/.test(val)){ // node.setStyle('text-decoration','line-through'); // if(node.attrs['class']){ // node.attrs['class'] = node.attrs['class'].replace(/fontstrikethrough/,''); // }else{ // node.setAttr('class') // } // } // if(/fontborder/.test(val)){ // node.setStyle('border','1px solid #000'); // if(node.attrs['class']){ // node.attrs['class'] = node.attrs['class'].replace(/fontborder/,''); // }else{ // node.setAttr('class') // } // } // } // }); }); // me.addOutputRule(function(root){ // utils.each(root.getNodesByTagName('span'), function (node) { // var val; // if(val = node.getStyle('text-decoration')){ // if(/line-through/.test(val)){ // if(node.attrs['class']){ // node.attrs['class'] += ' fontstrikethrough'; // }else{ // node.setAttr('class','fontstrikethrough') // } // } // // node.setStyle('text-decoration') // } // if(val = node.getStyle('border')){ // if(/1px/.test(val) && /solid/.test(val)){ // if(node.attrs['class']){ // node.attrs['class'] += ' fontborder'; // // }else{ // node.setAttr('class','fontborder') // } // } // node.setStyle('border') // // } // }); // }); for (var p in fonts) { (function (cmd, style) { UE.commands[cmd] = { execCommand: function (cmdName, value) { value = value || (this.queryCommandState(cmdName) ? 'none' : cmdName == 'underline' ? 'underline' : cmdName == 'fontborder' ? '1px solid #000' : 'line-through'); var me = this, range = this.selection.getRange(), text; if (value == 'default') { if (range.collapsed) { text = me.document.createTextNode('font'); range.insertNode(text).select(); } me.execCommand('removeFormat', 'span,a', style); if (text) { range.setStartBefore(text).collapse(true); domUtils.remove(text); } mergesibling(range,cmdName,value); range.select() } else { if (!range.collapsed) { if (needCmd[cmd] && me.queryCommandValue(cmd)) { me.execCommand('removeFormat', 'span,a', style); } range = me.selection.getRange(); range.applyInlineStyle('span', {'style': style + ':' + value}); mergesibling(range, cmdName,value); range.select(); } else { var span = domUtils.findParentByTagName(range.startContainer, 'span', true); text = me.document.createTextNode('font'); if (span && !span.children.length && !span[browser.ie ? 'innerText' : 'textContent'].replace(fillCharReg, '').length) { //for ie hack when enter range.insertNode(text); if (needCmd[cmd]) { range.selectNode(text).select(); me.execCommand('removeFormat', 'span,a', style, null); span = domUtils.findParentByTagName(text, 'span', true); range.setStartBefore(text); } span && (span.style.cssText += ';' + style + ':' + value); range.collapse(true).select(); } else { range.insertNode(text); range.selectNode(text).select(); span = range.document.createElement('span'); if (needCmd[cmd]) { //a标签内的不处理跳过 if (domUtils.findParentByTagName(text, 'a', true)) { range.setStartBefore(text).setCursor(); domUtils.remove(text); return; } me.execCommand('removeFormat', 'span,a', style); } span.style.cssText = style + ':' + value; text.parentNode.insertBefore(span, text); //修复,span套span 但样式不继承的问题 if (!browser.ie || browser.ie && browser.version == 9) { var spanParent = span.parentNode; while (!domUtils.isBlockElm(spanParent)) { if (spanParent.tagName == 'SPAN') { //opera合并style不会加入";" span.style.cssText = spanParent.style.cssText + ";" + span.style.cssText; } spanParent = spanParent.parentNode; } } if (opera) { setTimeout(function () { range.setStart(span, 0).collapse(true); mergesibling(range, cmdName,value); range.select(); }); } else { range.setStart(span, 0).collapse(true); mergesibling(range,cmdName,value); range.select(); } //trace:981 //domUtils.mergeToParent(span) } domUtils.remove(text); } } return true; }, queryCommandValue: function (cmdName) { var startNode = this.selection.getStart(); //trace:946 if (cmdName == 'underline' || cmdName == 'strikethrough') { var tmpNode = startNode, value; while (tmpNode && !domUtils.isBlockElm(tmpNode) && !domUtils.isBody(tmpNode)) { if (tmpNode.nodeType == 1) { value = domUtils.getComputedStyle(tmpNode, style); if (value != 'none') { return value; } } tmpNode = tmpNode.parentNode; } return 'none'; } if (cmdName == 'fontborder') { var tmp = startNode, val; while (tmp && dtd.$inline[tmp.tagName]) { if (val = domUtils.getComputedStyle(tmp, 'border')) { if (/1px/.test(val) && /solid/.test(val)) { return val; } } tmp = tmp.parentNode; } return '' } if( cmdName == 'FontSize' ) { var styleVal = domUtils.getComputedStyle(startNode, style), tmp = /^([\d\.]+)(\w+)$/.exec( styleVal ); if( tmp ) { return Math.floor( tmp[1] ) + tmp[2]; } return styleVal; } return domUtils.getComputedStyle(startNode, style); }, queryCommandState: function (cmdName) { if (!needCmd[cmdName]) return 0; var val = this.queryCommandValue(cmdName); if (cmdName == 'fontborder') { return /1px/.test(val) && /solid/.test(val) } else { return cmdName == 'underline' ? /underline/.test(val) : /line\-through/.test(val); } } }; })(p, fonts[p]); } }; // plugins/link.js /** * 超链接 * @file * @since 1.2.6.1 */ /** * 插入超链接 * @command link * @method execCommand * @param { String } cmd 命令字符串 * @param { Object } options 设置自定义属性,例如:url、title、target * @example * ```javascript * editor.execCommand( 'link', '{ * url:'ueditor.baidu.com', * title:'ueditor', * target:'_blank' * }' ); * ``` */ /** * 返回当前选中的第一个超链接节点 * @command link * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { Element } 超链接节点 * @example * ```javascript * editor.queryCommandValue( 'link' ); * ``` */ /** * 取消超链接 * @command unlink * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'unlink'); * ``` */ UE.plugins['link'] = function(){ function optimize( range ) { var start = range.startContainer,end = range.endContainer; if ( start = domUtils.findParentByTagName( start, 'a', true ) ) { range.setStartBefore( start ); } if ( end = domUtils.findParentByTagName( end, 'a', true ) ) { range.setEndAfter( end ); } } UE.commands['unlink'] = { execCommand : function() { var range = this.selection.getRange(), bookmark; if(range.collapsed && !domUtils.findParentByTagName( range.startContainer, 'a', true )){ return; } bookmark = range.createBookmark(); optimize( range ); range.removeInlineStyle( 'a' ).moveToBookmark( bookmark ).select(); }, queryCommandState : function(){ return !this.highlight && this.queryCommandValue('link') ? 0 : -1; } }; function doLink(range,opt,me){ var rngClone = range.cloneRange(), link = me.queryCommandValue('link'); optimize( range = range.adjustmentBoundary() ); var start = range.startContainer; if(start.nodeType == 1 && link){ start = start.childNodes[range.startOffset]; if(start && start.nodeType == 1 && start.tagName == 'A' && /^(?:https?|ftp|file)\s*:\s*\/\//.test(start[browser.ie?'innerText':'textContent'])){ start[browser.ie ? 'innerText' : 'textContent'] = utils.html(opt.textValue||opt.href); } } if( !rngClone.collapsed || link){ range.removeInlineStyle( 'a' ); rngClone = range.cloneRange(); } if ( rngClone.collapsed ) { var a = range.document.createElement( 'a'), text = ''; if(opt.textValue){ text = utils.html(opt.textValue); delete opt.textValue; }else{ text = utils.html(opt.href); } domUtils.setAttributes( a, opt ); start = domUtils.findParentByTagName( rngClone.startContainer, 'a', true ); if(start && domUtils.isInNodeEndBoundary(rngClone,start)){ range.setStartAfter(start).collapse(true); } a[browser.ie ? 'innerText' : 'textContent'] = text; range.insertNode(a).selectNode( a ); } else { range.applyInlineStyle( 'a', opt ); } } UE.commands['link'] = { execCommand : function( cmdName, opt ) { var range; opt._href && (opt._href = utils.unhtml(opt._href,/[<">]/g)); opt.href && (opt.href = utils.unhtml(opt.href,/[<">]/g)); opt.textValue && (opt.textValue = utils.unhtml(opt.textValue,/[<">]/g)); doLink(range=this.selection.getRange(),opt,this); //闭合都不加占位符,如果加了会在a后边多个占位符节点,导致a是图片背景组成的列表,出现空白问题 range.collapse().select(true); }, queryCommandValue : function() { var range = this.selection.getRange(), node; if ( range.collapsed ) { // node = this.selection.getStart(); //在ie下getstart()取值偏上了 node = range.startContainer; node = node.nodeType == 1 ? node : node.parentNode; if ( node && (node = domUtils.findParentByTagName( node, 'a', true )) && ! domUtils.isInNodeEndBoundary(range,node)) { return node; } } else { //trace:1111 如果是

    xx

    startContainer是p就会找不到a range.shrinkBoundary(); var start = range.startContainer.nodeType == 3 || !range.startContainer.childNodes[range.startOffset] ? range.startContainer : range.startContainer.childNodes[range.startOffset], end = range.endContainer.nodeType == 3 || range.endOffset == 0 ? range.endContainer : range.endContainer.childNodes[range.endOffset-1], common = range.getCommonAncestor(); node = domUtils.findParentByTagName( common, 'a', true ); if ( !node && common.nodeType == 1){ var as = common.getElementsByTagName( 'a' ), ps,pe; for ( var i = 0,ci; ci = as[i++]; ) { ps = domUtils.getPosition( ci, start ),pe = domUtils.getPosition( ci,end); if ( (ps & domUtils.POSITION_FOLLOWING || ps & domUtils.POSITION_CONTAINS) && (pe & domUtils.POSITION_PRECEDING || pe & domUtils.POSITION_CONTAINS) ) { node = ci; break; } } } return node; } }, queryCommandState : function() { //判断如果是视频的话连接不可用 //fix 853 var img = this.selection.getRange().getClosedNode(), flag = img && (img.className == "edui-faked-video" || img.className.indexOf("edui-upload-video")!=-1); return flag ? -1 : 0; } }; }; // plugins/iframe.js ///import core ///import plugins\inserthtml.js ///commands 插入框架 ///commandsName InsertFrame ///commandsTitle 插入Iframe ///commandsDialog dialogs\insertframe UE.plugins['insertframe'] = function() { var me =this; function deleteIframe(){ me._iframe && delete me._iframe; } me.addListener("selectionchange",function(){ deleteIframe(); }); }; // plugins/scrawl.js ///import core ///commands 涂鸦 ///commandsName Scrawl ///commandsTitle 涂鸦 ///commandsDialog dialogs\scrawl UE.commands['scrawl'] = { queryCommandState : function(){ return ( browser.ie && browser.version <= 8 ) ? -1 :0; } }; // plugins/removeformat.js /** * 清除格式 * @file * @since 1.2.6.1 */ /** * 清除文字样式 * @command removeformat * @method execCommand * @param { String } cmd 命令字符串 * @param {String} tags 以逗号隔开的标签。如:strong * @param {String} style 样式如:color * @param {String} attrs 属性如:width * @example * ```javascript * editor.execCommand( 'removeformat', 'strong','color','width' ); * ``` */ UE.plugins['removeformat'] = function(){ var me = this; me.setOpt({ 'removeFormatTags': 'b,big,code,del,dfn,em,font,i,ins,kbd,q,samp,small,span,strike,strong,sub,sup,tt,u,var', 'removeFormatAttributes':'class,style,lang,width,height,align,hspace,valign' }); me.commands['removeformat'] = { execCommand : function( cmdName, tags, style, attrs,notIncludeA ) { var tagReg = new RegExp( '^(?:' + (tags || this.options.removeFormatTags).replace( /,/g, '|' ) + ')$', 'i' ) , removeFormatAttributes = style ? [] : (attrs || this.options.removeFormatAttributes).split( ',' ), range = new dom.Range( this.document ), bookmark,node,parent, filter = function( node ) { return node.nodeType == 1; }; function isRedundantSpan (node) { if (node.nodeType == 3 || node.tagName.toLowerCase() != 'span'){ return 0; } if (browser.ie) { //ie 下判断实效,所以只能简单用style来判断 //return node.style.cssText == '' ? 1 : 0; var attrs = node.attributes; if ( attrs.length ) { for ( var i = 0,l = attrs.length; i var node = range.startContainer, tmp, collapsed = range.collapsed; while(node.nodeType == 1 && domUtils.isEmptyNode(node) && dtd.$removeEmpty[node.tagName]){ tmp = node.parentNode; range.setStartBefore(node); //trace:937 //更新结束边界 if(range.startContainer === range.endContainer){ range.endOffset--; } domUtils.remove(node); node = tmp; } if(!collapsed){ node = range.endContainer; while(node.nodeType == 1 && domUtils.isEmptyNode(node) && dtd.$removeEmpty[node.tagName]){ tmp = node.parentNode; range.setEndBefore(node); domUtils.remove(node); node = tmp; } } } range = this.selection.getRange(); doRemove( range ); range.select(); } }; }; // plugins/blockquote.js /** * 添加引用 * @file * @since 1.2.6.1 */ /** * 添加引用 * @command blockquote * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'blockquote' ); * ``` */ /** * 添加引用 * @command blockquote * @method execCommand * @param { String } cmd 命令字符串 * @param { Object } attrs 节点属性 * @example * ```javascript * editor.execCommand( 'blockquote',{ * style: "color: red;" * } ); * ``` */ UE.plugins['blockquote'] = function(){ var me = this; function getObj(editor){ return domUtils.filterNodeList(editor.selection.getStartElementPath(),'blockquote'); } me.commands['blockquote'] = { execCommand : function( cmdName, attrs ) { var range = this.selection.getRange(), obj = getObj(this), blockquote = dtd.blockquote, bookmark = range.createBookmark(); if ( obj ) { var start = range.startContainer, startBlock = domUtils.isBlockElm(start) ? start : domUtils.findParent(start,function(node){return domUtils.isBlockElm(node)}), end = range.endContainer, endBlock = domUtils.isBlockElm(end) ? end : domUtils.findParent(end,function(node){return domUtils.isBlockElm(node)}); //处理一下li startBlock = domUtils.findParentByTagName(startBlock,'li',true) || startBlock; endBlock = domUtils.findParentByTagName(endBlock,'li',true) || endBlock; if(startBlock.tagName == 'LI' || startBlock.tagName == 'TD' || startBlock === obj || domUtils.isBody(startBlock)){ domUtils.remove(obj,true); }else{ domUtils.breakParent(startBlock,obj); } if(startBlock !== endBlock){ obj = domUtils.findParentByTagName(endBlock,'blockquote'); if(obj){ if(endBlock.tagName == 'LI' || endBlock.tagName == 'TD'|| domUtils.isBody(endBlock)){ obj.parentNode && domUtils.remove(obj,true); }else{ domUtils.breakParent(endBlock,obj); } } } var blockquotes = domUtils.getElementsByTagName(this.document,'blockquote'); for(var i=0,bi;bi=blockquotes[i++];){ if(!bi.childNodes.length){ domUtils.remove(bi); }else if(domUtils.getPosition(bi,startBlock)&domUtils.POSITION_FOLLOWING && domUtils.getPosition(bi,endBlock)&domUtils.POSITION_PRECEDING){ domUtils.remove(bi,true); } } } else { var tmpRange = range.cloneRange(), node = tmpRange.startContainer.nodeType == 1 ? tmpRange.startContainer : tmpRange.startContainer.parentNode, preNode = node, doEnd = 1; //调整开始 while ( 1 ) { if ( domUtils.isBody(node) ) { if ( preNode !== node ) { if ( range.collapsed ) { tmpRange.selectNode( preNode ); doEnd = 0; } else { tmpRange.setStartBefore( preNode ); } }else{ tmpRange.setStart(node,0); } break; } if ( !blockquote[node.tagName] ) { if ( range.collapsed ) { tmpRange.selectNode( preNode ); } else{ tmpRange.setStartBefore( preNode); } break; } preNode = node; node = node.parentNode; } //调整结束 if ( doEnd ) { preNode = node = node = tmpRange.endContainer.nodeType == 1 ? tmpRange.endContainer : tmpRange.endContainer.parentNode; while ( 1 ) { if ( domUtils.isBody( node ) ) { if ( preNode !== node ) { tmpRange.setEndAfter( preNode ); } else { tmpRange.setEnd( node, node.childNodes.length ); } break; } if ( !blockquote[node.tagName] ) { tmpRange.setEndAfter( preNode ); break; } preNode = node; node = node.parentNode; } } node = range.document.createElement( 'blockquote' ); domUtils.setAttributes( node, attrs ); node.appendChild( tmpRange.extractContents() ); tmpRange.insertNode( node ); //去除重复的 var childs = domUtils.getElementsByTagName(node,'blockquote'); for(var i=0,ci;ci=childs[i++];){ if(ci.parentNode){ domUtils.remove(ci,true); } } } range.moveToBookmark( bookmark ).select(); }, queryCommandState : function() { return getObj(this) ? 1 : 0; } }; }; // plugins/convertcase.js /** * 大小写转换 * @file * @since 1.2.6.1 */ /** * 把选区内文本变大写,与“tolowercase”命令互斥 * @command touppercase * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'touppercase' ); * ``` */ /** * 把选区内文本变小写,与“touppercase”命令互斥 * @command tolowercase * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'tolowercase' ); * ``` */ UE.commands['touppercase'] = UE.commands['tolowercase'] = { execCommand:function (cmd) { var me = this; var rng = me.selection.getRange(); if(rng.collapsed){ return rng; } var bk = rng.createBookmark(), bkEnd = bk.end, filterFn = function( node ) { return !domUtils.isBr(node) && !domUtils.isWhitespace( node ); }, curNode = domUtils.getNextDomNode( bk.start, false, filterFn ); while ( curNode && (domUtils.getPosition( curNode, bkEnd ) & domUtils.POSITION_PRECEDING) ) { if ( curNode.nodeType == 3 ) { curNode.nodeValue = curNode.nodeValue[cmd == 'touppercase' ? 'toUpperCase' : 'toLowerCase'](); } curNode = domUtils.getNextDomNode( curNode, true, filterFn ); if(curNode === bkEnd){ break; } } rng.moveToBookmark(bk).select(); } }; // plugins/indent.js /** * 首行缩进 * @file * @since 1.2.6.1 */ /** * 缩进 * @command indent * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'indent' ); * ``` */ UE.commands['indent'] = { execCommand : function() { var me = this,value = me.queryCommandState("indent") ? "0em" : (me.options.indentValue || '2em'); me.execCommand('Paragraph','p',{style:'text-indent:'+ value}); }, queryCommandState : function() { var pN = domUtils.filterNodeList(this.selection.getStartElementPath(),'p h1 h2 h3 h4 h5 h6'); return pN && pN.style.textIndent && parseInt(pN.style.textIndent) ? 1 : 0; } }; // plugins/print.js /** * 打印 * @file * @since 1.2.6.1 */ /** * 打印 * @command print * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'print' ); * ``` */ UE.commands['print'] = { execCommand : function(){ this.window.print(); }, notNeedUndo : 1 }; // plugins/preview.js /** * 预览 * @file * @since 1.2.6.1 */ /** * 预览 * @command preview * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'preview' ); * ``` */ UE.commands['preview'] = { execCommand : function(){ var w = window.open('', '_blank', ''), d = w.document; d.open(); d.write('
    '+this.getContent(null,null,true)+'
    '); d.close(); }, notNeedUndo : 1 }; // plugins/selectall.js /** * 全选 * @file * @since 1.2.6.1 */ /** * 选中所有内容 * @command selectall * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'selectall' ); * ``` */ UE.plugins['selectall'] = function(){ var me = this; me.commands['selectall'] = { execCommand : function(){ //去掉了原生的selectAll,因为会出现报错和当内容为空时,不能出现闭合状态的光标 var me = this,body = me.body, range = me.selection.getRange(); range.selectNodeContents(body); if(domUtils.isEmptyBlock(body)){ //opera不能自动合并到元素的里边,要手动处理一下 if(browser.opera && body.firstChild && body.firstChild.nodeType == 1){ range.setStartAtFirst(body.firstChild); } range.collapse(true); } range.select(true); }, notNeedUndo : 1 }; //快捷键 me.addshortcutkey({ "selectAll" : "ctrl+65" }); }; // plugins/paragraph.js /** * 段落样式 * @file * @since 1.2.6.1 */ /** * 段落格式 * @command paragraph * @method execCommand * @param { String } cmd 命令字符串 * @param {String} style 标签值为:'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' * @param {Object} attrs 标签的属性 * @example * ```javascript * editor.execCommand( 'Paragraph','h1','{ * class:'test' * }' ); * ``` */ /** * 返回选区内节点标签名 * @command paragraph * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { String } 节点标签名 * @example * ```javascript * editor.queryCommandValue( 'Paragraph' ); * ``` */ UE.plugins['paragraph'] = function() { var me = this, block = domUtils.isBlockElm, notExchange = ['TD','LI','PRE'], doParagraph = function(range,style,attrs,sourceCmdName){ var bookmark = range.createBookmark(), filterFn = function( node ) { return node.nodeType == 1 ? node.tagName.toLowerCase() != 'br' && !domUtils.isBookmarkNode(node) : !domUtils.isWhitespace( node ); }, para; range.enlarge( true ); var bookmark2 = range.createBookmark(), current = domUtils.getNextDomNode( bookmark2.start, false, filterFn ), tmpRange = range.cloneRange(), tmpNode; while ( current && !(domUtils.getPosition( current, bookmark2.end ) & domUtils.POSITION_FOLLOWING) ) { if ( current.nodeType == 3 || !block( current ) ) { tmpRange.setStartBefore( current ); while ( current && current !== bookmark2.end && !block( current ) ) { tmpNode = current; current = domUtils.getNextDomNode( current, false, null, function( node ) { return !block( node ); } ); } tmpRange.setEndAfter( tmpNode ); para = range.document.createElement( style ); if(attrs){ domUtils.setAttributes(para,attrs); if(sourceCmdName && sourceCmdName == 'customstyle' && attrs.style){ para.style.cssText = attrs.style; } } para.appendChild( tmpRange.extractContents() ); //需要内容占位 if(domUtils.isEmptyNode(para)){ domUtils.fillChar(range.document,para); } tmpRange.insertNode( para ); var parent = para.parentNode; //如果para上一级是一个block元素且不是body,td就删除它 if ( block( parent ) && !domUtils.isBody( para.parentNode ) && utils.indexOf(notExchange,parent.tagName)==-1) { //存储dir,style if(!(sourceCmdName && sourceCmdName == 'customstyle')){ parent.getAttribute('dir') && para.setAttribute('dir',parent.getAttribute('dir')); //trace:1070 parent.style.cssText && (para.style.cssText = parent.style.cssText + ';' + para.style.cssText); //trace:1030 parent.style.textAlign && !para.style.textAlign && (para.style.textAlign = parent.style.textAlign); parent.style.textIndent && !para.style.textIndent && (para.style.textIndent = parent.style.textIndent); parent.style.padding && !para.style.padding && (para.style.padding = parent.style.padding); } //trace:1706 选择的就是h1-6要删除 if(attrs && /h\d/i.test(parent.tagName) && !/h\d/i.test(para.tagName) ){ domUtils.setAttributes(parent,attrs); if(sourceCmdName && sourceCmdName == 'customstyle' && attrs.style){ parent.style.cssText = attrs.style; } domUtils.remove(para,true); para = parent; }else{ domUtils.remove( para.parentNode, true ); } } if( utils.indexOf(notExchange,parent.tagName)!=-1){ current = parent; }else{ current = para; } current = domUtils.getNextDomNode( current, false, filterFn ); } else { current = domUtils.getNextDomNode( current, true, filterFn ); } } return range.moveToBookmark( bookmark2 ).moveToBookmark( bookmark ); }; me.setOpt('paragraph',{'p':'', 'h1':'', 'h2':'', 'h3':'', 'h4':'', 'h5':'', 'h6':''}); me.commands['paragraph'] = { execCommand : function( cmdName, style,attrs,sourceCmdName ) { var range = this.selection.getRange(); //闭合时单独处理 if(range.collapsed){ var txt = this.document.createTextNode('p'); range.insertNode(txt); //去掉冗余的fillchar if(browser.ie){ var node = txt.previousSibling; if(node && domUtils.isWhitespace(node)){ domUtils.remove(node); } node = txt.nextSibling; if(node && domUtils.isWhitespace(node)){ domUtils.remove(node); } } } range = doParagraph(range,style,attrs,sourceCmdName); if(txt){ range.setStartBefore(txt).collapse(true); pN = txt.parentNode; domUtils.remove(txt); if(domUtils.isBlockElm(pN)&&domUtils.isEmptyNode(pN)){ domUtils.fillNode(this.document,pN); } } if(browser.gecko && range.collapsed && range.startContainer.nodeType == 1){ var child = range.startContainer.childNodes[range.startOffset]; if(child && child.nodeType == 1 && child.tagName.toLowerCase() == style){ range.setStart(child,0).collapse(true); } } //trace:1097 原来有true,原因忘了,但去了就不能清除多余的占位符了 range.select(); return true; }, queryCommandValue : function() { var node = domUtils.filterNodeList(this.selection.getStartElementPath(),'p h1 h2 h3 h4 h5 h6'); return node ? node.tagName.toLowerCase() : ''; } }; }; // plugins/directionality.js /** * 设置文字输入的方向的插件 * @file * @since 1.2.6.1 */ (function() { var block = domUtils.isBlockElm , getObj = function(editor){ // var startNode = editor.selection.getStart(), // parents; // if ( startNode ) { // //查找所有的是block的父亲节点 // parents = domUtils.findParents( startNode, true, block, true ); // for ( var i = 0,ci; ci = parents[i++]; ) { // if ( ci.getAttribute( 'dir' ) ) { // return ci; // } // } // } return domUtils.filterNodeList(editor.selection.getStartElementPath(),function(n){return n && n.nodeType == 1 && n.getAttribute('dir')}); }, doDirectionality = function(range,editor,forward){ var bookmark, filterFn = function( node ) { return node.nodeType == 1 ? !domUtils.isBookmarkNode(node) : !domUtils.isWhitespace(node); }, obj = getObj( editor ); if ( obj && range.collapsed ) { obj.setAttribute( 'dir', forward ); return range; } bookmark = range.createBookmark(); range.enlarge( true ); var bookmark2 = range.createBookmark(), current = domUtils.getNextDomNode( bookmark2.start, false, filterFn ), tmpRange = range.cloneRange(), tmpNode; while ( current && !(domUtils.getPosition( current, bookmark2.end ) & domUtils.POSITION_FOLLOWING) ) { if ( current.nodeType == 3 || !block( current ) ) { tmpRange.setStartBefore( current ); while ( current && current !== bookmark2.end && !block( current ) ) { tmpNode = current; current = domUtils.getNextDomNode( current, false, null, function( node ) { return !block( node ); } ); } tmpRange.setEndAfter( tmpNode ); var common = tmpRange.getCommonAncestor(); if ( !domUtils.isBody( common ) && block( common ) ) { //遍历到了block节点 common.setAttribute( 'dir', forward ); current = common; } else { //没有遍历到,添加一个block节点 var p = range.document.createElement( 'p' ); p.setAttribute( 'dir', forward ); var frag = tmpRange.extractContents(); p.appendChild( frag ); tmpRange.insertNode( p ); current = p; } current = domUtils.getNextDomNode( current, false, filterFn ); } else { current = domUtils.getNextDomNode( current, true, filterFn ); } } return range.moveToBookmark( bookmark2 ).moveToBookmark( bookmark ); }; /** * 文字输入方向 * @command directionality * @method execCommand * @param { String } cmdName 命令字符串 * @param { String } forward 传入'ltr'表示从左向右输入,传入'rtl'表示从右向左输入 * @example * ```javascript * editor.execCommand( 'directionality', 'ltr'); * ``` */ /** * 查询当前选区的文字输入方向 * @command directionality * @method queryCommandValue * @param { String } cmdName 命令字符串 * @return { String } 返回'ltr'表示从左向右输入,返回'rtl'表示从右向左输入 * @example * ```javascript * editor.queryCommandValue( 'directionality'); * ``` */ UE.commands['directionality'] = { execCommand : function( cmdName,forward ) { var range = this.selection.getRange(); //闭合时单独处理 if(range.collapsed){ var txt = this.document.createTextNode('d'); range.insertNode(txt); } doDirectionality(range,this,forward); if(txt){ range.setStartBefore(txt).collapse(true); domUtils.remove(txt); } range.select(); return true; }, queryCommandValue : function() { var node = getObj(this); return node ? node.getAttribute('dir') : 'ltr'; } }; })(); // plugins/horizontal.js /** * 插入分割线插件 * @file * @since 1.2.6.1 */ /** * 插入分割线 * @command horizontal * @method execCommand * @param { String } cmdName 命令字符串 * @example * ```javascript * editor.execCommand( 'horizontal' ); * ``` */ UE.plugins['horizontal'] = function(){ var me = this; me.commands['horizontal'] = { execCommand : function( cmdName ) { var me = this; if(me.queryCommandState(cmdName)!==-1){ me.execCommand('insertHtml','
    '); var range = me.selection.getRange(), start = range.startContainer; if(start.nodeType == 1 && !start.childNodes[range.startOffset] ){ var tmp; if(tmp = start.childNodes[range.startOffset - 1]){ if(tmp.nodeType == 1 && tmp.tagName == 'HR'){ if(me.options.enterTag == 'p'){ tmp = me.document.createElement('p'); range.insertNode(tmp); range.setStart(tmp,0).setCursor(); }else{ tmp = me.document.createElement('br'); range.insertNode(tmp); range.setStartBefore(tmp).setCursor(); } } } } return true; } }, //边界在table里不能加分隔线 queryCommandState : function() { return domUtils.filterNodeList(this.selection.getStartElementPath(),'table') ? -1 : 0; } }; // me.addListener('delkeyup',function(){ // var rng = this.selection.getRange(); // if(browser.ie && browser.version > 8){ // rng.txtToElmBoundary(true); // if(domUtils.isStartInblock(rng)){ // var tmpNode = rng.startContainer; // var pre = tmpNode.previousSibling; // if(pre && domUtils.isTagNode(pre,'hr')){ // domUtils.remove(pre); // rng.select(); // return; // } // } // } // if(domUtils.isBody(rng.startContainer)){ // var hr = rng.startContainer.childNodes[rng.startOffset -1]; // if(hr && hr.nodeName == 'HR'){ // var next = hr.nextSibling; // if(next){ // rng.setStart(next,0) // }else if(hr.previousSibling){ // rng.setStartAtLast(hr.previousSibling) // }else{ // var p = this.document.createElement('p'); // hr.parentNode.insertBefore(p,hr); // domUtils.fillNode(this.document,p); // rng.setStart(p,0); // } // domUtils.remove(hr); // rng.setCursor(false,true); // } // } // }) me.addListener('delkeydown',function(name,evt){ var rng = this.selection.getRange(); rng.txtToElmBoundary(true); if(domUtils.isStartInblock(rng)){ var tmpNode = rng.startContainer; var pre = tmpNode.previousSibling; if(pre && domUtils.isTagNode(pre,'hr')){ domUtils.remove(pre); rng.select(); domUtils.preventDefault(evt); return true; } } }) }; // plugins/time.js /** * 插入时间和日期 * @file * @since 1.2.6.1 */ /** * 插入时间,默认格式:12:59:59 * @command time * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'time'); * ``` */ /** * 插入日期,默认格式:2013-08-30 * @command date * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'date'); * ``` */ UE.commands['time'] = UE.commands["date"] = { execCommand : function(cmd, format){ var date = new Date; function formatTime(date, format) { var hh = ('0' + date.getHours()).slice(-2), ii = ('0' + date.getMinutes()).slice(-2), ss = ('0' + date.getSeconds()).slice(-2); format = format || 'hh:ii:ss'; return format.replace(/hh/ig, hh).replace(/ii/ig, ii).replace(/ss/ig, ss); } function formatDate(date, format) { var yyyy = ('000' + date.getFullYear()).slice(-4), yy = yyyy.slice(-2), mm = ('0' + (date.getMonth()+1)).slice(-2), dd = ('0' + date.getDate()).slice(-2); format = format || 'yyyy-mm-dd'; return format.replace(/yyyy/ig, yyyy).replace(/yy/ig, yy).replace(/mm/ig, mm).replace(/dd/ig, dd); } this.execCommand('insertHtml',cmd == "time" ? formatTime(date, format):formatDate(date, format) ); } }; // plugins/rowspacing.js /** * 段前段后间距插件 * @file * @since 1.2.6.1 */ /** * 设置段间距 * @command rowspacing * @method execCommand * @param { String } cmd 命令字符串 * @param { String } value 段间距的值,以px为单位 * @param { String } dir 间距位置,top或bottom,分别表示段前和段后 * @example * ```javascript * editor.execCommand( 'rowspacing', '10', 'top' ); * ``` */ UE.plugins['rowspacing'] = function(){ var me = this; me.setOpt({ 'rowspacingtop':['5', '10', '15', '20', '25'], 'rowspacingbottom':['5', '10', '15', '20', '25'] }); me.commands['rowspacing'] = { execCommand : function( cmdName,value,dir ) { this.execCommand('paragraph','p',{style:'margin-'+dir+':'+value + 'px'}); return true; }, queryCommandValue : function(cmdName,dir) { var pN = domUtils.filterNodeList(this.selection.getStartElementPath(),function(node){return domUtils.isBlockElm(node) }), value; //trace:1026 if(pN){ value = domUtils.getComputedStyle(pN,'margin-'+dir).replace(/[^\d]/g,''); return !value ? 0 : value; } return 0; } }; }; // plugins/lineheight.js /** * 设置行内间距 * @file * @since 1.2.6.1 */ UE.plugins['lineheight'] = function(){ var me = this; me.setOpt({'lineheight':['1', '1.5','1.75','2', '3', '4', '5']}); /** * 行距 * @command lineheight * @method execCommand * @param { String } cmdName 命令字符串 * @param { String } value 传入的行高值, 该值是当前字体的倍数, 例如: 1.5, 1.75 * @example * ```javascript * editor.execCommand( 'lineheight', 1.5); * ``` */ /** * 查询当前选区内容的行高大小 * @command lineheight * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { String } 返回当前行高大小 * @example * ```javascript * editor.queryCommandValue( 'lineheight' ); * ``` */ me.commands['lineheight'] = { execCommand : function( cmdName,value ) { this.execCommand('paragraph','p',{style:'line-height:'+ (value == "1" ? "normal" : value + 'em') }); return true; }, queryCommandValue : function() { var pN = domUtils.filterNodeList(this.selection.getStartElementPath(),function(node){return domUtils.isBlockElm(node)}); if(pN){ var value = domUtils.getComputedStyle(pN,'line-height'); return value == 'normal' ? 1 : value.replace(/[^\d.]*/ig,""); } } }; }; // plugins/insertcode.js /** * 插入代码插件 * @file * @since 1.2.6.1 */ UE.plugins['insertcode'] = function() { var me = this; me.ready(function(){ utils.cssRule('pre','pre{margin:.5em 0;padding:.4em .6em;border-radius:8px;background:#f8f8f8;}', me.document) }); me.setOpt('insertcode',{ 'as3':'ActionScript3', 'bash':'Bash/Shell', 'cpp':'C/C++', 'css':'Css', 'cf':'CodeFunction', 'c#':'C#', 'delphi':'Delphi', 'diff':'Diff', 'erlang':'Erlang', 'groovy':'Groovy', 'html':'Html', 'java':'Java', 'jfx':'JavaFx', 'js':'Javascript', 'pl':'Perl', 'php':'Php', 'plain':'Plain Text', 'ps':'PowerShell', 'python':'Python', 'ruby':'Ruby', 'scala':'Scala', 'sql':'Sql', 'vb':'Vb', 'xml':'Xml' }); /** * 插入代码 * @command insertcode * @method execCommand * @param { String } cmd 命令字符串 * @param { String } lang 插入代码的语言 * @example * ```javascript * editor.execCommand( 'insertcode', 'javascript' ); * ``` */ /** * 如果选区所在位置是插入插入代码区域,返回代码的语言 * @command insertcode * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { String } 返回代码的语言 * @example * ```javascript * editor.queryCommandValue( 'insertcode' ); * ``` */ me.commands['insertcode'] = { execCommand : function(cmd,lang){ var me = this, rng = me.selection.getRange(), pre = domUtils.findParentByTagName(rng.startContainer,'pre',true); if(pre){ pre.className = 'brush:'+lang+';toolbar:false;'; }else{ var code = ''; if(rng.collapsed){ code = browser.ie && browser.ie11below ? (browser.version <= 8 ? ' ':''):'
    '; }else{ var frag = rng.extractContents(); var div = me.document.createElement('div'); div.appendChild(frag); utils.each(UE.filterNode(UE.htmlparser(div.innerHTML.replace(/[\r\t]/g,'')),me.options.filterTxtRules).children,function(node){ if(browser.ie && browser.ie11below && browser.version > 8){ if(node.type =='element'){ if(node.tagName == 'br'){ code += '\n' }else if(!dtd.$empty[node.tagName]){ utils.each(node.children,function(cn){ if(cn.type =='element'){ if(cn.tagName == 'br'){ code += '\n' }else if(!dtd.$empty[node.tagName]){ code += cn.innerText(); } }else{ code += cn.data } }) if(!/\n$/.test(code)){ code += '\n'; } } }else{ code += node.data + '\n' } if(!node.nextSibling() && /\n$/.test(code)){ code = code.replace(/\n$/,''); } }else{ if(browser.ie && browser.ie11below){ if(node.type =='element'){ if(node.tagName == 'br'){ code += '
    ' }else if(!dtd.$empty[node.tagName]){ utils.each(node.children,function(cn){ if(cn.type =='element'){ if(cn.tagName == 'br'){ code += '
    ' }else if(!dtd.$empty[node.tagName]){ code += cn.innerText(); } }else{ code += cn.data } }); if(!/br>$/.test(code)){ code += '
    '; } } }else{ code += node.data + '
    ' } if(!node.nextSibling() && /
    $/.test(code)){ code = code.replace(/
    $/,''); } }else{ code += (node.type == 'element' ? (dtd.$empty[node.tagName] ? '' : node.innerText()) : node.data); if(!/br\/?\s*>$/.test(code)){ if(!node.nextSibling()) return; code += '
    ' } } } }); } me.execCommand('inserthtml','
    '+code+'
    ',true); pre = me.document.getElementById('coder'); domUtils.removeAttributes(pre,'id'); var tmpNode = pre.previousSibling; if(tmpNode && (tmpNode.nodeType == 3 && tmpNode.nodeValue.length == 1 && browser.ie && browser.version == 6 || domUtils.isEmptyBlock(tmpNode))){ domUtils.remove(tmpNode) } var rng = me.selection.getRange(); if(domUtils.isEmptyBlock(pre)){ rng.setStart(pre,0).setCursor(false,true) }else{ rng.selectNodeContents(pre).select() } } }, queryCommandValue : function(){ var path = this.selection.getStartElementPath(); var lang = ''; utils.each(path,function(node){ if(node.nodeName =='PRE'){ var match = node.className.match(/brush:([^;]+)/); lang = match && match[1] ? match[1] : ''; return false; } }); return lang; } }; me.addInputRule(function(root){ utils.each(root.getNodesByTagName('pre'),function(pre){ var brs = pre.getNodesByTagName('br'); if(brs.length){ browser.ie && browser.ie11below && browser.version > 8 && utils.each(brs,function(br){ var txt = UE.uNode.createText('\n'); br.parentNode.insertBefore(txt,br); br.parentNode.removeChild(br); }); return; } if(browser.ie && browser.ie11below && browser.version > 8) return; var code = pre.innerText().split(/\n/); pre.innerHTML(''); utils.each(code,function(c){ if(c.length){ pre.appendChild(UE.uNode.createText(c)); } pre.appendChild(UE.uNode.createElement('br')) }) }) }); me.addOutputRule(function(root){ utils.each(root.getNodesByTagName('pre'),function(pre){ var code = ''; utils.each(pre.children,function(n){ if(n.type == 'text'){ //在ie下文本内容有可能末尾带有\n要去掉 //trace:3396 code += n.data.replace(/[ ]/g,' ').replace(/\n$/,''); }else{ if(n.tagName == 'br'){ code += '\n' }else{ code += (!dtd.$empty[n.tagName] ? '' : n.innerText()); } } }); pre.innerText(code.replace(/( |\n)+$/,'')) }) }); //不需要判断highlight的command列表 me.notNeedCodeQuery ={ help:1, undo:1, redo:1, source:1, print:1, searchreplace:1, fullscreen:1, preview:1, insertparagraph:1, elementpath:1, insertcode:1, inserthtml:1, selectall:1 }; //将queyCommamndState重置 var orgQuery = me.queryCommandState; me.queryCommandState = function(cmd){ var me = this; if(!me.notNeedCodeQuery[cmd.toLowerCase()] && me.selection && me.queryCommandValue('insertcode')){ return -1; } return UE.Editor.prototype.queryCommandState.apply(this,arguments) }; me.addListener('beforeenterkeydown',function(){ var rng = me.selection.getRange(); var pre = domUtils.findParentByTagName(rng.startContainer,'pre',true); if(pre){ me.fireEvent('saveScene'); if(!rng.collapsed){ rng.deleteContents(); } if(!browser.ie || browser.ie9above){ var tmpNode = me.document.createElement('br'),pre; rng.insertNode(tmpNode).setStartAfter(tmpNode).collapse(true); var next = tmpNode.nextSibling; if(!next && (!browser.ie || browser.version > 10)){ rng.insertNode(tmpNode.cloneNode(false)); }else{ rng.setStartAfter(tmpNode); } pre = tmpNode.previousSibling; var tmp; while(pre ){ tmp = pre; pre = pre.previousSibling; if(!pre || pre.nodeName == 'BR'){ pre = tmp; break; } } if(pre){ var str = ''; while(pre && pre.nodeName != 'BR' && new RegExp('^[\\s'+domUtils.fillChar+']*$').test(pre.nodeValue)){ str += pre.nodeValue; pre = pre.nextSibling; } if(pre.nodeName != 'BR'){ var match = pre.nodeValue.match(new RegExp('^([\\s'+domUtils.fillChar+']+)')); if(match && match[1]){ str += match[1] } } if(str){ str = me.document.createTextNode(str); rng.insertNode(str).setStartAfter(str); } } rng.collapse(true).select(true); }else{ if(browser.version > 8){ var txt = me.document.createTextNode('\n'); var start = rng.startContainer; if(rng.startOffset == 0){ var preNode = start.previousSibling; if(preNode){ rng.insertNode(txt); var fillchar = me.document.createTextNode(' '); rng.setStartAfter(txt).insertNode(fillchar).setStart(fillchar,0).collapse(true).select(true) } }else{ rng.insertNode(txt).setStartAfter(txt); var fillchar = me.document.createTextNode(' '); start = rng.startContainer.childNodes[rng.startOffset]; if(start && !/^\n/.test(start.nodeValue)){ rng.setStartBefore(txt) } rng.insertNode(fillchar).setStart(fillchar,0).collapse(true).select(true) } }else{ var tmpNode = me.document.createElement('br'); rng.insertNode(tmpNode); rng.insertNode(me.document.createTextNode(domUtils.fillChar)); rng.setStartAfter(tmpNode); pre = tmpNode.previousSibling; var tmp; while(pre ){ tmp = pre; pre = pre.previousSibling; if(!pre || pre.nodeName == 'BR'){ pre = tmp; break; } } if(pre){ var str = ''; while(pre && pre.nodeName != 'BR' && new RegExp('^[ '+domUtils.fillChar+']*$').test(pre.nodeValue)){ str += pre.nodeValue; pre = pre.nextSibling; } if(pre.nodeName != 'BR'){ var match = pre.nodeValue.match(new RegExp('^([ '+domUtils.fillChar+']+)')); if(match && match[1]){ str += match[1] } } str = me.document.createTextNode(str); rng.insertNode(str).setStartAfter(str); } rng.collapse(true).select(); } } me.fireEvent('saveScene'); return true; } }); me.addListener('tabkeydown',function(cmd,evt){ var rng = me.selection.getRange(); var pre = domUtils.findParentByTagName(rng.startContainer,'pre',true); if(pre){ me.fireEvent('saveScene'); if(evt.shiftKey){ }else{ if(!rng.collapsed){ var bk = rng.createBookmark(); var start = bk.start.previousSibling; while(start){ if(pre.firstChild === start && !domUtils.isBr(start)){ pre.insertBefore(me.document.createTextNode(' '),start); break; } if(domUtils.isBr(start)){ pre.insertBefore(me.document.createTextNode(' '),start.nextSibling); break; } start = start.previousSibling; } var end = bk.end; start = bk.start.nextSibling; if(pre.firstChild === bk.start){ pre.insertBefore(me.document.createTextNode(' '),start.nextSibling) } while(start && start !== end){ if(domUtils.isBr(start) && start.nextSibling){ if(start.nextSibling === end){ break; } pre.insertBefore(me.document.createTextNode(' '),start.nextSibling) } start = start.nextSibling; } rng.moveToBookmark(bk).select(); }else{ var tmpNode = me.document.createTextNode(' '); rng.insertNode(tmpNode).setStartAfter(tmpNode).collapse(true).select(true); } } me.fireEvent('saveScene'); return true; } }); me.addListener('beforeinserthtml',function(evtName,html){ var me = this, rng = me.selection.getRange(), pre = domUtils.findParentByTagName(rng.startContainer,'pre',true); if(pre){ if(!rng.collapsed){ rng.deleteContents() } var htmlstr = ''; if(browser.ie && browser.version > 8){ utils.each(UE.filterNode(UE.htmlparser(html),me.options.filterTxtRules).children,function(node){ if(node.type =='element'){ if(node.tagName == 'br'){ htmlstr += '\n' }else if(!dtd.$empty[node.tagName]){ utils.each(node.children,function(cn){ if(cn.type =='element'){ if(cn.tagName == 'br'){ htmlstr += '\n' }else if(!dtd.$empty[node.tagName]){ htmlstr += cn.innerText(); } }else{ htmlstr += cn.data } }) if(!/\n$/.test(htmlstr)){ htmlstr += '\n'; } } }else{ htmlstr += node.data + '\n' } if(!node.nextSibling() && /\n$/.test(htmlstr)){ htmlstr = htmlstr.replace(/\n$/,''); } }); var tmpNode = me.document.createTextNode(utils.html(htmlstr.replace(/ /g,' '))); rng.insertNode(tmpNode).selectNode(tmpNode).select(); }else{ var frag = me.document.createDocumentFragment(); utils.each(UE.filterNode(UE.htmlparser(html),me.options.filterTxtRules).children,function(node){ if(node.type =='element'){ if(node.tagName == 'br'){ frag.appendChild(me.document.createElement('br')) }else if(!dtd.$empty[node.tagName]){ utils.each(node.children,function(cn){ if(cn.type =='element'){ if(cn.tagName == 'br'){ frag.appendChild(me.document.createElement('br')) }else if(!dtd.$empty[node.tagName]){ frag.appendChild(me.document.createTextNode(utils.html(cn.innerText().replace(/ /g,' ')))); } }else{ frag.appendChild(me.document.createTextNode(utils.html( cn.data.replace(/ /g,' ')))); } }) if(frag.lastChild.nodeName != 'BR'){ frag.appendChild(me.document.createElement('br')) } } }else{ frag.appendChild(me.document.createTextNode(utils.html( node.data.replace(/ /g,' ')))); } if(!node.nextSibling() && frag.lastChild.nodeName == 'BR'){ frag.removeChild(frag.lastChild) } }); rng.insertNode(frag).select(); } return true; } }); //方向键的处理 me.addListener('keydown',function(cmd,evt){ var me = this,keyCode = evt.keyCode || evt.which; if(keyCode == 40){ var rng = me.selection.getRange(),pre,start = rng.startContainer; if(rng.collapsed && (pre = domUtils.findParentByTagName(rng.startContainer,'pre',true)) && !pre.nextSibling){ var last = pre.lastChild while(last && last.nodeName == 'BR'){ last = last.previousSibling; } if(last === start || rng.startContainer === pre && rng.startOffset == pre.childNodes.length){ me.execCommand('insertparagraph'); domUtils.preventDefault(evt) } } } }); //trace:3395 me.addListener('delkeydown',function(type,evt){ var rng = this.selection.getRange(); rng.txtToElmBoundary(true); var start = rng.startContainer; if(domUtils.isTagNode(start,'pre') && rng.collapsed && domUtils.isStartInblock(rng)){ var p = me.document.createElement('p'); domUtils.fillNode(me.document,p); start.parentNode.insertBefore(p,start); domUtils.remove(start); rng.setStart(p,0).setCursor(false,true); domUtils.preventDefault(evt); return true; } }) }; // plugins/cleardoc.js /** * 清空文档插件 * @file * @since 1.2.6.1 */ /** * 清空文档 * @command cleardoc * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * //editor 是编辑器实例 * editor.execCommand('cleardoc'); * ``` */ UE.commands['cleardoc'] = { execCommand : function( cmdName) { var me = this, enterTag = me.options.enterTag, range = me.selection.getRange(); if(enterTag == "br"){ me.body.innerHTML = "
    "; range.setStart(me.body,0).setCursor(); }else{ me.body.innerHTML = "

    "+(ie ? "" : "
    ")+"

    "; range.setStart(me.body.firstChild,0).setCursor(false,true); } setTimeout(function(){ me.fireEvent("clearDoc"); },0); } }; // plugins/anchor.js /** * 锚点插件,为UEditor提供插入锚点支持 * @file * @since 1.2.6.1 */ UE.plugin.register('anchor', function (){ return { bindEvents:{ 'ready':function(){ utils.cssRule('anchor', '.anchorclass{background: url(\'' + this.options.themePath + this.options.theme +'/images/anchor.gif\') no-repeat scroll left center transparent;cursor: auto;display: inline-block;height: 16px;width: 15px;}', this.document); } }, outputRule: function(root){ utils.each(root.getNodesByTagName('img'),function(a){ var val; if(val = a.getAttr('anchorname')){ a.tagName = 'a'; a.setAttr({ anchorname : '', name : val, 'class' : '' }) } }) }, inputRule:function(root){ utils.each(root.getNodesByTagName('a'),function(a){ var val; if((val = a.getAttr('name')) && !a.getAttr('href')){ a.tagName = 'img'; a.setAttr({ anchorname :a.getAttr('name'), 'class' : 'anchorclass' }); a.setAttr('name') } }) }, commands:{ /** * 插入锚点 * @command anchor * @method execCommand * @param { String } cmd 命令字符串 * @param { String } name 锚点名称字符串 * @example * ```javascript * //editor 是编辑器实例 * editor.execCommand('anchor', 'anchor1'); * ``` */ 'anchor':{ execCommand:function (cmd, name) { var range = this.selection.getRange(),img = range.getClosedNode(); if (img && img.getAttribute('anchorname')) { if (name) { img.setAttribute('anchorname', name); } else { range.setStartBefore(img).setCursor(); domUtils.remove(img); } } else { if (name) { //只在选区的开始插入 var anchor = this.document.createElement('img'); range.collapse(true); domUtils.setAttributes(anchor,{ 'anchorname':name, 'class':'anchorclass' }); range.insertNode(anchor).setStartAfter(anchor).setCursor(false,true); } } } } } } }); // plugins/wordcount.js ///import core ///commands 字数统计 ///commandsName WordCount,wordCount ///commandsTitle 字数统计 /* * Created by JetBrains WebStorm. * User: taoqili * Date: 11-9-7 * Time: 下午8:18 * To change this template use File | Settings | File Templates. */ UE.plugins['wordcount'] = function(){ var me = this; me.setOpt('wordCount',true); me.addListener('contentchange',function(){ me.fireEvent('wordcount'); }); var timer; me.addListener('ready',function(){ var me = this; domUtils.on(me.body,"keyup",function(evt){ var code = evt.keyCode||evt.which, //忽略的按键,ctr,alt,shift,方向键 ignores = {"16":1,"18":1,"20":1,"37":1,"38":1,"39":1,"40":1}; if(code in ignores) return; clearTimeout(timer); timer = setTimeout(function(){ me.fireEvent('wordcount'); },200) }) }); }; // plugins/pagebreak.js /** * 分页功能插件 * @file * @since 1.2.6.1 */ UE.plugins['pagebreak'] = function () { var me = this, notBreakTags = ['td']; me.setOpt('pageBreakTag','_ueditor_page_break_tag_'); function fillNode(node){ if(domUtils.isEmptyBlock(node)){ var firstChild = node.firstChild,tmpNode; while(firstChild && firstChild.nodeType == 1 && domUtils.isEmptyBlock(firstChild)){ tmpNode = firstChild; firstChild = firstChild.firstChild; } !tmpNode && (tmpNode = node); domUtils.fillNode(me.document,tmpNode); } } //分页符样式添加 me.ready(function(){ utils.cssRule('pagebreak','.pagebreak{display:block;clear:both !important;cursor:default !important;width: 100% !important;margin:0;}',me.document); }); function isHr(node){ return node && node.nodeType == 1 && node.tagName == 'HR' && node.className == 'pagebreak'; } me.addInputRule(function(root){ root.traversal(function(node){ if(node.type == 'text' && node.data == me.options.pageBreakTag){ var hr = UE.uNode.createElement('
    '); node.parentNode.insertBefore(hr,node); node.parentNode.removeChild(node) } }) }); me.addOutputRule(function(node){ utils.each(node.getNodesByTagName('hr'),function(n){ if(n.getAttr('class') == 'pagebreak'){ var txt = UE.uNode.createText(me.options.pageBreakTag); n.parentNode.insertBefore(txt,n); n.parentNode.removeChild(n); } }) }); /** * 插入分页符 * @command pagebreak * @method execCommand * @param { String } cmd 命令字符串 * @remind 在表格中插入分页符会把表格切分成两部分 * @remind 获取编辑器内的数据时, 编辑器会把分页符转换成“_ueditor_page_break_tag_”字符串, * 以便于提交数据到服务器端后处理分页。 * @example * ```javascript * editor.execCommand( 'pagebreak'); //插入一个hr标签,带有样式类名pagebreak * ``` */ me.commands['pagebreak'] = { execCommand:function () { var range = me.selection.getRange(),hr = me.document.createElement('hr'); domUtils.setAttributes(hr,{ 'class' : 'pagebreak', noshade:"noshade", size:"5" }); domUtils.unSelectable(hr); //table单独处理 var node = domUtils.findParentByTagName(range.startContainer, notBreakTags, true), parents = [], pN; if (node) { switch (node.tagName) { case 'TD': pN = node.parentNode; if (!pN.previousSibling) { var table = domUtils.findParentByTagName(pN, 'table'); // var tableWrapDiv = table.parentNode; // if(tableWrapDiv && tableWrapDiv.nodeType == 1 // && tableWrapDiv.tagName == 'DIV' // && tableWrapDiv.getAttribute('dropdrag') // ){ // domUtils.remove(tableWrapDiv,true); // } table.parentNode.insertBefore(hr, table); parents = domUtils.findParents(hr, true); } else { pN.parentNode.insertBefore(hr, pN); parents = domUtils.findParents(hr); } pN = parents[1]; if (hr !== pN) { domUtils.breakParent(hr, pN); } //table要重写绑定一下拖拽 me.fireEvent('afteradjusttable',me.document); } } else { if (!range.collapsed) { range.deleteContents(); var start = range.startContainer; while ( !domUtils.isBody(start) && domUtils.isBlockElm(start) && domUtils.isEmptyNode(start)) { range.setStartBefore(start).collapse(true); domUtils.remove(start); start = range.startContainer; } } range.insertNode(hr); var pN = hr.parentNode, nextNode; while (!domUtils.isBody(pN)) { domUtils.breakParent(hr, pN); nextNode = hr.nextSibling; if (nextNode && domUtils.isEmptyBlock(nextNode)) { domUtils.remove(nextNode); } pN = hr.parentNode; } nextNode = hr.nextSibling; var pre = hr.previousSibling; if(isHr(pre)){ domUtils.remove(pre); }else{ pre && fillNode(pre); } if(!nextNode){ var p = me.document.createElement('p'); hr.parentNode.appendChild(p); domUtils.fillNode(me.document,p); range.setStart(p,0).collapse(true); }else{ if(isHr(nextNode)){ domUtils.remove(nextNode); }else{ fillNode(nextNode); } range.setEndAfter(hr).collapse(false); } range.select(true); } } }; }; // plugins/wordimage.js ///import core ///commands 本地图片引导上传 ///commandsName WordImage ///commandsTitle 本地图片引导上传 ///commandsDialog dialogs\wordimage UE.plugin.register('wordimage',function(){ var me = this, images = []; return { commands : { 'wordimage':{ execCommand:function () { var images = domUtils.getElementsByTagName(me.body, "img"); var urlList = []; for (var i = 0, ci; ci = images[i++];) { var url = ci.getAttribute("word_img"); url && urlList.push(url); } return urlList; }, queryCommandState:function () { images = domUtils.getElementsByTagName(me.body, "img"); for (var i = 0, ci; ci = images[i++];) { if (ci.getAttribute("word_img")) { return 1; } } return -1; }, notNeedUndo:true } }, inputRule : function (root) { utils.each(root.getNodesByTagName('img'), function (img) { var attrs = img.attrs, flag = parseInt(attrs.width) < 128 || parseInt(attrs.height) < 43, opt = me.options, src = opt.UEDITOR_HOME_URL + 'themes/default/images/spacer.gif'; if (attrs['src'] && /^(?:(file:\/+))/.test(attrs['src'])) { img.setAttr({ width:attrs.width, height:attrs.height, alt:attrs.alt, word_img: attrs.src, src:src, 'style':'background:url(' + ( flag ? opt.themePath + opt.theme + '/images/word.gif' : opt.langPath + opt.lang + '/images/localimage.png') + ') no-repeat center center;border:1px solid #ddd' }) } }) } } }); // plugins/dragdrop.js UE.plugins['dragdrop'] = function (){ var me = this; me.ready(function(){ domUtils.on(this.body,'dragend',function(){ var rng = me.selection.getRange(); var node = rng.getClosedNode()||me.selection.getStart(); if(node && node.tagName == 'IMG'){ var pre = node.previousSibling,next; while(next = node.nextSibling){ if(next.nodeType == 1 && next.tagName == 'SPAN' && !next.firstChild){ domUtils.remove(next) }else{ break; } } if((pre && pre.nodeType == 1 && !domUtils.isEmptyBlock(pre) || !pre) && (!next || next && !domUtils.isEmptyBlock(next))){ if(pre && pre.tagName == 'P' && !domUtils.isEmptyBlock(pre)){ pre.appendChild(node); domUtils.moveChild(next,pre); domUtils.remove(next); }else if(next && next.tagName == 'P' && !domUtils.isEmptyBlock(next)){ next.insertBefore(node,next.firstChild); } if(pre && pre.tagName == 'P' && domUtils.isEmptyBlock(pre)){ domUtils.remove(pre) } if(next && next.tagName == 'P' && domUtils.isEmptyBlock(next)){ domUtils.remove(next) } rng.selectNode(node).select(); me.fireEvent('saveScene'); } } }) }); me.addListener('keyup', function(type, evt) { var keyCode = evt.keyCode || evt.which; if (keyCode == 13) { var rng = me.selection.getRange(),node; if(node = domUtils.findParentByTagName(rng.startContainer,'p',true)){ if(domUtils.getComputedStyle(node,'text-align') == 'center'){ domUtils.removeStyle(node,'text-align') } } } }) }; // plugins/undo.js /** * undo redo * @file * @since 1.2.6.1 */ /** * 撤销上一次执行的命令 * @command undo * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'undo' ); * ``` */ /** * 重做上一次执行的命令 * @command redo * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'redo' ); * ``` */ UE.plugins['undo'] = function () { var saveSceneTimer; var me = this, maxUndoCount = me.options.maxUndoCount || 20, maxInputCount = me.options.maxInputCount || 20, fillchar = new RegExp(domUtils.fillChar + '|<\/hr>', 'gi');// ie会产生多余的 var noNeedFillCharTags = { ol:1,ul:1,table:1,tbody:1,tr:1,body:1 }; var orgState = me.options.autoClearEmptyNode; function compareAddr(indexA, indexB) { if (indexA.length != indexB.length) return 0; for (var i = 0, l = indexA.length; i < l; i++) { if (indexA[i] != indexB[i]) return 0 } return 1; } function compareRangeAddress(rngAddrA, rngAddrB) { if (rngAddrA.collapsed != rngAddrB.collapsed) { return 0; } if (!compareAddr(rngAddrA.startAddress, rngAddrB.startAddress) || !compareAddr(rngAddrA.endAddress, rngAddrB.endAddress)) { return 0; } return 1; } function UndoManager() { this.list = []; this.index = 0; this.hasUndo = false; this.hasRedo = false; this.undo = function () { if (this.hasUndo) { if (!this.list[this.index - 1] && this.list.length == 1) { this.reset(); return; } while (this.list[this.index].content == this.list[this.index - 1].content) { this.index--; if (this.index == 0) { return this.restore(0); } } this.restore(--this.index); } }; this.redo = function () { if (this.hasRedo) { while (this.list[this.index].content == this.list[this.index + 1].content) { this.index++; if (this.index == this.list.length - 1) { return this.restore(this.index); } } this.restore(++this.index); } }; this.restore = function () { var me = this.editor; var scene = this.list[this.index]; var root = UE.htmlparser(scene.content.replace(fillchar, '')); me.options.autoClearEmptyNode = false; me.filterInputRule(root); me.options.autoClearEmptyNode = orgState; //trace:873 //去掉展位符 me.document.body.innerHTML = root.toHtml(); me.fireEvent('afterscencerestore'); //处理undo后空格不展位的问题 if (browser.ie) { utils.each(domUtils.getElementsByTagName(me.document,'td th caption p'),function(node){ if(domUtils.isEmptyNode(node)){ domUtils.fillNode(me.document, node); } }) } try{ var rng = new dom.Range(me.document).moveToAddress(scene.address); rng.select(noNeedFillCharTags[rng.startContainer.nodeName.toLowerCase()]); }catch(e){} this.update(); this.clearKey(); //不能把自己reset了 me.fireEvent('reset', true); }; this.getScene = function () { var me = this.editor; var rng = me.selection.getRange(), rngAddress = rng.createAddress(false,true); me.fireEvent('beforegetscene'); var root = UE.htmlparser(me.body.innerHTML); me.options.autoClearEmptyNode = false; me.filterOutputRule(root); me.options.autoClearEmptyNode = orgState; var cont = root.toHtml(); //trace:3461 //这个会引起回退时导致空格丢失的情况 // browser.ie && (cont = cont.replace(/> <').replace(/\s*\s*/g, '>')); me.fireEvent('aftergetscene'); return { address:rngAddress, content:cont } }; this.save = function (notCompareRange,notSetCursor) { clearTimeout(saveSceneTimer); var currentScene = this.getScene(notSetCursor), lastScene = this.list[this.index]; if(lastScene && lastScene.content != currentScene.content){ me.trigger('contentchange') } //内容相同位置相同不存 if (lastScene && lastScene.content == currentScene.content && ( notCompareRange ? 1 : compareRangeAddress(lastScene.address, currentScene.address) ) ) { return; } this.list = this.list.slice(0, this.index + 1); this.list.push(currentScene); //如果大于最大数量了,就把最前的剔除 if (this.list.length > maxUndoCount) { this.list.shift(); } this.index = this.list.length - 1; this.clearKey(); //跟新undo/redo状态 this.update(); }; this.update = function () { this.hasRedo = !!this.list[this.index + 1]; this.hasUndo = !!this.list[this.index - 1]; }; this.reset = function () { this.list = []; this.index = 0; this.hasUndo = false; this.hasRedo = false; this.clearKey(); }; this.clearKey = function () { keycont = 0; lastKeyCode = null; }; } me.undoManger = new UndoManager(); me.undoManger.editor = me; function saveScene() { this.undoManger.save(); } me.addListener('saveScene', function () { var args = Array.prototype.splice.call(arguments,1); this.undoManger.save.apply(this.undoManger,args); }); // me.addListener('beforeexeccommand', saveScene); // me.addListener('afterexeccommand', saveScene); me.addListener('reset', function (type, exclude) { if (!exclude) { this.undoManger.reset(); } }); me.commands['redo'] = me.commands['undo'] = { execCommand:function (cmdName) { this.undoManger[cmdName](); }, queryCommandState:function (cmdName) { return this.undoManger['has' + (cmdName.toLowerCase() == 'undo' ? 'Undo' : 'Redo')] ? 0 : -1; }, notNeedUndo:1 }; var keys = { // /*Backspace*/ 8:1, /*Delete*/ 46:1, /*Shift*/ 16:1, /*Ctrl*/ 17:1, /*Alt*/ 18:1, 37:1, 38:1, 39:1, 40:1 }, keycont = 0, lastKeyCode; //输入法状态下不计算字符数 var inputType = false; me.addListener('ready', function () { domUtils.on(this.body, 'compositionstart', function () { inputType = true; }); domUtils.on(this.body, 'compositionend', function () { inputType = false; }) }); //快捷键 me.addshortcutkey({ "Undo":"ctrl+90", //undo "Redo":"ctrl+89" //redo }); var isCollapsed = true; me.addListener('keydown', function (type, evt) { var me = this; var keyCode = evt.keyCode || evt.which; if (!keys[keyCode] && !evt.ctrlKey && !evt.metaKey && !evt.shiftKey && !evt.altKey) { if (inputType) return; if(!me.selection.getRange().collapsed){ me.undoManger.save(false,true); isCollapsed = false; return; } if (me.undoManger.list.length == 0) { me.undoManger.save(true); } clearTimeout(saveSceneTimer); function save(cont){ cont.undoManger.save(false,true); cont.fireEvent('selectionchange'); } saveSceneTimer = setTimeout(function(){ if(inputType){ var interalTimer = setInterval(function(){ if(!inputType){ save(me); clearInterval(interalTimer) } },300) return; } save(me); },200); lastKeyCode = keyCode; keycont++; if (keycont >= maxInputCount ) { save(me) } } }); me.addListener('keyup', function (type, evt) { var keyCode = evt.keyCode || evt.which; if (!keys[keyCode] && !evt.ctrlKey && !evt.metaKey && !evt.shiftKey && !evt.altKey) { if (inputType) return; if(!isCollapsed){ this.undoManger.save(false,true); isCollapsed = true; } } }); //扩展实例,添加关闭和开启命令undo me.stopCmdUndo = function(){ me.__hasEnterExecCommand = true; }; me.startCmdUndo = function(){ me.__hasEnterExecCommand = false; } }; // plugins/copy.js UE.plugin.register('copy', function () { var me = this; function initZeroClipboard() { ZeroClipboard.config({ debug: false, swfPath: me.options.UEDITOR_HOME_URL + 'third-party/zeroclipboard/ZeroClipboard.swf' }); var client = me.zeroclipboard = new ZeroClipboard(); // 复制内容 client.on('copy', function (e) { var client = e.client, rng = me.selection.getRange(), div = document.createElement('div'); div.appendChild(rng.cloneContents()); client.setText(div.innerText || div.textContent); client.setHtml(div.innerHTML); rng.select(); }); // hover事件传递到target client.on('mouseover mouseout', function (e) { var target = e.target; if (e.type == 'mouseover') { domUtils.addClass(target, 'edui-state-hover'); } else if (e.type == 'mouseout') { domUtils.removeClasses(target, 'edui-state-hover'); } }); // flash加载不成功 client.on('wrongflash noflash', function () { ZeroClipboard.destroy(); }); } return { bindEvents: { 'ready': function () { if (!browser.ie) { if (window.ZeroClipboard) { initZeroClipboard(); } else { utils.loadFile(document, { src: me.options.UEDITOR_HOME_URL + "third-party/zeroclipboard/ZeroClipboard.js", tag: "script", type: "text/javascript", defer: "defer" }, function () { initZeroClipboard(); }); } } } }, commands: { 'copy': { execCommand: function (cmd) { if (!me.document.execCommand('copy')) { alert(me.getLang('copymsg')); } } } } } }); // plugins/paste.js ///import core ///import plugins/inserthtml.js ///import plugins/undo.js ///import plugins/serialize.js ///commands 粘贴 ///commandsName PastePlain ///commandsTitle 纯文本粘贴模式 /** * @description 粘贴 * @author zhanyi */ UE.plugins['paste'] = function () { function getClipboardData(callback) { var doc = this.document; if (doc.getElementById('baidu_pastebin')) { return; } var range = this.selection.getRange(), bk = range.createBookmark(), //创建剪贴的容器div pastebin = doc.createElement('div'); pastebin.id = 'baidu_pastebin'; // Safari 要求div必须有内容,才能粘贴内容进来 browser.webkit && pastebin.appendChild(doc.createTextNode(domUtils.fillChar + domUtils.fillChar)); doc.body.appendChild(pastebin); //trace:717 隐藏的span不能得到top //bk.start.innerHTML = ' '; bk.start.style.display = ''; pastebin.style.cssText = "position:absolute;width:1px;height:1px;overflow:hidden;left:-1000px;white-space:nowrap;top:" + //要在现在光标平行的位置加入,否则会出现跳动的问题 domUtils.getXY(bk.start).y + 'px'; range.selectNodeContents(pastebin).select(true); setTimeout(function () { if (browser.webkit) { for (var i = 0, pastebins = doc.querySelectorAll('#baidu_pastebin'), pi; pi = pastebins[i++];) { if (domUtils.isEmptyNode(pi)) { domUtils.remove(pi); } else { pastebin = pi; break; } } } try { pastebin.parentNode.removeChild(pastebin); } catch (e) { } range.moveToBookmark(bk).select(true); callback(pastebin); }, 0); } var me = this; me.setOpt({ retainOnlyLabelPasted : false }); var txtContent, htmlContent, address; function getPureHtml(html){ return html.replace(/<(\/?)([\w\-]+)([^>]*)>/gi, function (a, b, tagName, attrs) { tagName = tagName.toLowerCase(); if ({img: 1}[tagName]) { return a; } attrs = attrs.replace(/([\w\-]*?)\s*=\s*(("([^"]*)")|('([^']*)')|([^\s>]+))/gi, function (str, atr, val) { if ({ 'src': 1, 'href': 1, 'name': 1 }[atr.toLowerCase()]) { return atr + '=' + val + ' ' } return '' }); if ({ 'span': 1, 'div': 1 }[tagName]) { return '' } else { return '<' + b + tagName + ' ' + utils.trim(attrs) + '>' } }); } function filter(div) { var html; if (div.firstChild) { //去掉cut中添加的边界值 var nodes = domUtils.getElementsByTagName(div, 'span'); for (var i = 0, ni; ni = nodes[i++];) { if (ni.id == '_baidu_cut_start' || ni.id == '_baidu_cut_end') { domUtils.remove(ni); } } if (browser.webkit) { var brs = div.querySelectorAll('div br'); for (var i = 0, bi; bi = brs[i++];) { var pN = bi.parentNode; if (pN.tagName == 'DIV' && pN.childNodes.length == 1) { pN.innerHTML = '


    '; domUtils.remove(pN); } } var divs = div.querySelectorAll('#baidu_pastebin'); for (var i = 0, di; di = divs[i++];) { var tmpP = me.document.createElement('p'); di.parentNode.insertBefore(tmpP, di); while (di.firstChild) { tmpP.appendChild(di.firstChild); } domUtils.remove(di); } var metas = div.querySelectorAll('meta'); for (var i = 0, ci; ci = metas[i++];) { domUtils.remove(ci); } var brs = div.querySelectorAll('br'); for (i = 0; ci = brs[i++];) { if (/^apple-/i.test(ci.className)) { domUtils.remove(ci); } } } if (browser.gecko) { var dirtyNodes = div.querySelectorAll('[_moz_dirty]'); for (i = 0; ci = dirtyNodes[i++];) { ci.removeAttribute('_moz_dirty'); } } if (!browser.ie) { var spans = div.querySelectorAll('span.Apple-style-span'); for (var i = 0, ci; ci = spans[i++];) { domUtils.remove(ci, true); } } //ie下使用innerHTML会产生多余的\r\n字符,也会产生 这里过滤掉 html = div.innerHTML;//.replace(/>(?:(\s| )*?)<'); //过滤word粘贴过来的冗余属性 html = UE.filterWord(html); //取消了忽略空白的第二个参数,粘贴过来的有些是有空白的,会被套上相关的标签 var root = UE.htmlparser(html); //如果给了过滤规则就先进行过滤 if (me.options.filterRules) { UE.filterNode(root, me.options.filterRules); } //执行默认的处理 me.filterInputRule(root); //针对chrome的处理 if (browser.webkit) { var br = root.lastChild(); if (br && br.type == 'element' && br.tagName == 'br') { root.removeChild(br) } utils.each(me.body.querySelectorAll('div'), function (node) { if (domUtils.isEmptyBlock(node)) { domUtils.remove(node,true) } }) } html = {'html': root.toHtml()}; me.fireEvent('beforepaste', html, root); //抢了默认的粘贴,那后边的内容就不执行了,比如表格粘贴 if(!html.html){ return; } root = UE.htmlparser(html.html,true); //如果开启了纯文本模式 if (me.queryCommandState('pasteplain') === 1) { me.execCommand('insertHtml', UE.filterNode(root, me.options.filterTxtRules).toHtml(), true); } else { //文本模式 UE.filterNode(root, me.options.filterTxtRules); txtContent = root.toHtml(); //完全模式 htmlContent = html.html; address = me.selection.getRange().createAddress(true); me.execCommand('insertHtml', me.getOpt('retainOnlyLabelPasted') === true ? getPureHtml(htmlContent) : htmlContent, true); } me.fireEvent("afterpaste", html); } } me.addListener('pasteTransfer', function (cmd, plainType) { if (address && txtContent && htmlContent && txtContent != htmlContent) { var range = me.selection.getRange(); range.moveToAddress(address, true); if (!range.collapsed) { while (!domUtils.isBody(range.startContainer) ) { var start = range.startContainer; if(start.nodeType == 1){ start = start.childNodes[range.startOffset]; if(!start){ range.setStartBefore(range.startContainer); continue; } var pre = start.previousSibling; if(pre && pre.nodeType == 3 && new RegExp('^[\n\r\t '+domUtils.fillChar+']*$').test(pre.nodeValue)){ range.setStartBefore(pre) } } if(range.startOffset == 0){ range.setStartBefore(range.startContainer); }else{ break; } } while (!domUtils.isBody(range.endContainer) ) { var end = range.endContainer; if(end.nodeType == 1){ end = end.childNodes[range.endOffset]; if(!end){ range.setEndAfter(range.endContainer); continue; } var next = end.nextSibling; if(next && next.nodeType == 3 && new RegExp('^[\n\r\t'+domUtils.fillChar+']*$').test(next.nodeValue)){ range.setEndAfter(next) } } if(range.endOffset == range.endContainer[range.endContainer.nodeType == 3 ? 'nodeValue' : 'childNodes'].length){ range.setEndAfter(range.endContainer); }else{ break; } } } range.deleteContents(); range.select(true); me.__hasEnterExecCommand = true; var html = htmlContent; if (plainType === 2 ) { html = getPureHtml(html); } else if (plainType) { html = txtContent; } me.execCommand('inserthtml', html, true); me.__hasEnterExecCommand = false; var rng = me.selection.getRange(); while (!domUtils.isBody(rng.startContainer) && !rng.startOffset && rng.startContainer[rng.startContainer.nodeType == 3 ? 'nodeValue' : 'childNodes'].length ) { rng.setStartBefore(rng.startContainer); } var tmpAddress = rng.createAddress(true); address.endAddress = tmpAddress.startAddress; } }); me.addListener('ready', function () { domUtils.on(me.body, 'cut', function () { var range = me.selection.getRange(); if (!range.collapsed && me.undoManger) { me.undoManger.save(); } }); //ie下beforepaste在点击右键时也会触发,所以用监控键盘才处理 domUtils.on(me.body, browser.ie || browser.opera ? 'keydown' : 'paste', function (e) { if ((browser.ie || browser.opera) && ((!e.ctrlKey && !e.metaKey) || e.keyCode != '86')) { return; } getClipboardData.call(me, function (div) { filter(div); }); }); }); me.commands['paste'] = { execCommand: function (cmd) { if (browser.ie) { getClipboardData.call(me, function (div) { filter(div); }); me.document.execCommand('paste'); } else { alert(me.getLang('pastemsg')); } } } }; // plugins/puretxtpaste.js /** * 纯文本粘贴插件 * @file * @since 1.2.6.1 */ UE.plugins['pasteplain'] = function(){ var me = this; me.setOpt({ 'pasteplain':false, 'filterTxtRules' : function(){ function transP(node){ node.tagName = 'p'; node.setStyle(); } function removeNode(node){ node.parentNode.removeChild(node,true) } return { //直接删除及其字节点内容 '-' : 'script style object iframe embed input select', 'p': {$:{}}, 'br':{$:{}}, div: function (node) { var tmpNode, p = UE.uNode.createElement('p'); while (tmpNode = node.firstChild()) { if (tmpNode.type == 'text' || !UE.dom.dtd.$block[tmpNode.tagName]) { p.appendChild(tmpNode); } else { if (p.firstChild()) { node.parentNode.insertBefore(p, node); p = UE.uNode.createElement('p'); } else { node.parentNode.insertBefore(tmpNode, node); } } } if (p.firstChild()) { node.parentNode.insertBefore(p, node); } node.parentNode.removeChild(node); }, ol: removeNode, ul: removeNode, dl:removeNode, dt:removeNode, dd:removeNode, 'li':removeNode, 'caption':transP, 'th':transP, 'tr':transP, 'h1':transP,'h2':transP,'h3':transP,'h4':transP,'h5':transP,'h6':transP, 'td':function(node){ //没有内容的td直接删掉 var txt = !!node.innerText(); if(txt){ node.parentNode.insertAfter(UE.uNode.createText('    '),node); } node.parentNode.removeChild(node,node.innerText()) } } }() }); //暂时这里支持一下老版本的属性 var pasteplain = me.options.pasteplain; /** * 启用或取消纯文本粘贴模式 * @command pasteplain * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.queryCommandState( 'pasteplain' ); * ``` */ /** * 查询当前是否处于纯文本粘贴模式 * @command pasteplain * @method queryCommandState * @param { String } cmd 命令字符串 * @return { int } 如果处于纯文本模式,返回1,否则,返回0 * @example * ```javascript * editor.queryCommandState( 'pasteplain' ); * ``` */ me.commands['pasteplain'] = { queryCommandState: function (){ return pasteplain ? 1 : 0; }, execCommand: function (){ pasteplain = !pasteplain|0; }, notNeedUndo : 1 }; }; // plugins/list.js /** * 有序列表,无序列表插件 * @file * @since 1.2.6.1 */ UE.plugins['list'] = function () { var me = this, notExchange = { 'TD':1, 'PRE':1, 'BLOCKQUOTE':1 }; var customStyle = { 'cn' : 'cn-1-', 'cn1' : 'cn-2-', 'cn2' : 'cn-3-', 'num': 'num-1-', 'num1' : 'num-2-', 'num2' : 'num-3-', 'dash' : 'dash', 'dot':'dot' }; me.setOpt( { 'autoTransWordToList':false, 'insertorderedlist':{ 'num':'', 'num1':'', 'num2':'', 'cn':'', 'cn1':'', 'cn2':'', 'decimal':'', 'lower-alpha':'', 'lower-roman':'', 'upper-alpha':'', 'upper-roman':'' }, 'insertunorderedlist':{ 'circle':'', 'disc':'', 'square':'', 'dash' : '', 'dot':'' }, listDefaultPaddingLeft : '30', listiconpath : 'http://bs.baidu.com/listicon/', maxListLevel : -1,//-1不限制 disablePInList:false } ); function listToArray(list){ var arr = []; for(var p in list){ arr.push(p) } return arr; } var listStyle = { 'OL':listToArray(me.options.insertorderedlist), 'UL':listToArray(me.options.insertunorderedlist) }; var liiconpath = me.options.listiconpath; //根据用户配置,调整customStyle for(var s in customStyle){ if(!me.options.insertorderedlist.hasOwnProperty(s) && !me.options.insertunorderedlist.hasOwnProperty(s)){ delete customStyle[s]; } } me.ready(function () { var customCss = []; for(var p in customStyle){ if(p == 'dash' || p == 'dot'){ customCss.push('li.list-' + customStyle[p] + '{background-image:url(' + liiconpath +customStyle[p]+'.gif)}'); customCss.push('ul.custom_'+p+'{list-style:none;}ul.custom_'+p+' li{background-position:0 3px;background-repeat:no-repeat}'); }else{ for(var i= 0;i<99;i++){ customCss.push('li.list-' + customStyle[p] + i + '{background-image:url(' + liiconpath + 'list-'+customStyle[p] + i + '.gif)}') } customCss.push('ol.custom_'+p+'{list-style:none;}ol.custom_'+p+' li{background-position:0 3px;background-repeat:no-repeat}'); } switch(p){ case 'cn': customCss.push('li.list-'+p+'-paddingleft-1{padding-left:25px}'); customCss.push('li.list-'+p+'-paddingleft-2{padding-left:40px}'); customCss.push('li.list-'+p+'-paddingleft-3{padding-left:55px}'); break; case 'cn1': customCss.push('li.list-'+p+'-paddingleft-1{padding-left:30px}'); customCss.push('li.list-'+p+'-paddingleft-2{padding-left:40px}'); customCss.push('li.list-'+p+'-paddingleft-3{padding-left:55px}'); break; case 'cn2': customCss.push('li.list-'+p+'-paddingleft-1{padding-left:40px}'); customCss.push('li.list-'+p+'-paddingleft-2{padding-left:55px}'); customCss.push('li.list-'+p+'-paddingleft-3{padding-left:68px}'); break; case 'num': case 'num1': customCss.push('li.list-'+p+'-paddingleft-1{padding-left:25px}'); break; case 'num2': customCss.push('li.list-'+p+'-paddingleft-1{padding-left:35px}'); customCss.push('li.list-'+p+'-paddingleft-2{padding-left:40px}'); break; case 'dash': customCss.push('li.list-'+p+'-paddingleft{padding-left:35px}'); break; case 'dot': customCss.push('li.list-'+p+'-paddingleft{padding-left:20px}'); } } customCss.push('.list-paddingleft-1{padding-left:0}'); customCss.push('.list-paddingleft-2{padding-left:'+me.options.listDefaultPaddingLeft+'px}'); customCss.push('.list-paddingleft-3{padding-left:'+me.options.listDefaultPaddingLeft*2+'px}'); //如果不给宽度会在自定应样式里出现滚动条 utils.cssRule('list', 'ol,ul{margin:0;pading:0;'+(browser.ie ? '' : 'width:95%')+'}li{clear:both;}'+customCss.join('\n'), me.document); }); //单独处理剪切的问题 me.ready(function(){ domUtils.on(me.body,'cut',function(){ setTimeout(function(){ var rng = me.selection.getRange(),li; //trace:3416 if(!rng.collapsed){ if(li = domUtils.findParentByTagName(rng.startContainer,'li',true)){ if(!li.nextSibling && domUtils.isEmptyBlock(li)){ var pn = li.parentNode,node; if(node = pn.previousSibling){ domUtils.remove(pn); rng.setStartAtLast(node).collapse(true); rng.select(true); }else if(node = pn.nextSibling){ domUtils.remove(pn); rng.setStartAtFirst(node).collapse(true); rng.select(true); }else{ var tmpNode = me.document.createElement('p'); domUtils.fillNode(me.document,tmpNode); pn.parentNode.insertBefore(tmpNode,pn); domUtils.remove(pn); rng.setStart(tmpNode,0).collapse(true); rng.select(true); } } } } }) }) }); function getStyle(node){ var cls = node.className; if(domUtils.hasClass(node,/custom_/)){ return cls.match(/custom_(\w+)/)[1] } return domUtils.getStyle(node, 'list-style-type') } me.addListener('beforepaste',function(type,html){ var me = this, rng = me.selection.getRange(),li; var root = UE.htmlparser(html.html,true); if(li = domUtils.findParentByTagName(rng.startContainer,'li',true)){ var list = li.parentNode,tagName = list.tagName == 'OL' ? 'ul':'ol'; utils.each(root.getNodesByTagName(tagName),function(n){ n.tagName = list.tagName; n.setAttr(); if(n.parentNode === root){ type = getStyle(list) || (list.tagName == 'OL' ? 'decimal' : 'disc') }else{ var className = n.parentNode.getAttr('class'); if(className && /custom_/.test(className)){ type = className.match(/custom_(\w+)/)[1] }else{ type = n.parentNode.getStyle('list-style-type'); } if(!type){ type = list.tagName == 'OL' ? 'decimal' : 'disc'; } } var index = utils.indexOf(listStyle[list.tagName], type); if(n.parentNode !== root) index = index + 1 == listStyle[list.tagName].length ? 0 : index + 1; var currentStyle = listStyle[list.tagName][index]; if(customStyle[currentStyle]){ n.setAttr('class', 'custom_' + currentStyle) }else{ n.setStyle('list-style-type',currentStyle) } }) } html.html = root.toHtml(); }); //导出时,去掉p标签 me.getOpt('disablePInList') === true && me.addOutputRule(function(root){ utils.each(root.getNodesByTagName('li'),function(li){ var newChildrens = [],index=0; utils.each(li.children,function(n){ if(n.tagName == 'p'){ var tmpNode; while(tmpNode = n.children.pop()) { newChildrens.splice(index,0,tmpNode); tmpNode.parentNode = li; lastNode = tmpNode; } tmpNode = newChildrens[newChildrens.length-1]; if(!tmpNode || tmpNode.type != 'element' || tmpNode.tagName != 'br'){ var br = UE.uNode.createElement('br'); br.parentNode = li; newChildrens.push(br); } index = newChildrens.length; } }); if(newChildrens.length){ li.children = newChildrens; } }); }); //进入编辑器的li要套p标签 me.addInputRule(function(root){ utils.each(root.getNodesByTagName('li'),function(li){ var tmpP = UE.uNode.createElement('p'); for(var i= 0,ci;ci=li.children[i];){ if(ci.type == 'text' || dtd.p[ci.tagName]){ tmpP.appendChild(ci); }else{ if(tmpP.firstChild()){ li.insertBefore(tmpP,ci); tmpP = UE.uNode.createElement('p'); i = i + 2; }else{ i++; } } } if(tmpP.firstChild() && !tmpP.parentNode || !li.firstChild()){ li.appendChild(tmpP); } //trace:3357 //p不能为空 if (!tmpP.firstChild()) { tmpP.innerHTML(browser.ie ? ' ' : '
    ') } //去掉末尾的空白 var p = li.firstChild(); var lastChild = p.lastChild(); if(lastChild && lastChild.type == 'text' && /^\s*$/.test(lastChild.data)){ p.removeChild(lastChild) } }); if(me.options.autoTransWordToList){ var orderlisttype = { 'num1':/^\d+\)/, 'decimal':/^\d+\./, 'lower-alpha':/^[a-z]+\)/, 'upper-alpha':/^[A-Z]+\./, 'cn':/^[\u4E00\u4E8C\u4E09\u56DB\u516d\u4e94\u4e03\u516b\u4e5d]+[\u3001]/, 'cn2':/^\([\u4E00\u4E8C\u4E09\u56DB\u516d\u4e94\u4e03\u516b\u4e5d]+\)/ }, unorderlisttype = { 'square':'n' }; function checkListType(content,container){ var span = container.firstChild(); if(span && span.type == 'element' && span.tagName == 'span' && /Wingdings|Symbol/.test(span.getStyle('font-family'))){ for(var p in unorderlisttype){ if(unorderlisttype[p] == span.data){ return p } } return 'disc' } for(var p in orderlisttype){ if(orderlisttype[p].test(content)){ return p; } } } utils.each(root.getNodesByTagName('p'),function(node){ if(node.getAttr('class') != 'MsoListParagraph'){ return } //word粘贴过来的会带有margin要去掉,但这样也可能会误命中一些央视 node.setStyle('margin',''); node.setStyle('margin-left',''); node.setAttr('class',''); function appendLi(list,p,type){ if(list.tagName == 'ol'){ if(browser.ie){ var first = p.firstChild(); if(first.type =='element' && first.tagName == 'span' && orderlisttype[type].test(first.innerText())){ p.removeChild(first); } }else{ p.innerHTML(p.innerHTML().replace(orderlisttype[type],'')); } }else{ p.removeChild(p.firstChild()) } var li = UE.uNode.createElement('li'); li.appendChild(p); list.appendChild(li); } var tmp = node,type,cacheNode = node; if(node.parentNode.tagName != 'li' && (type = checkListType(node.innerText(),node))){ var list = UE.uNode.createElement(me.options.insertorderedlist.hasOwnProperty(type) ? 'ol' : 'ul'); if(customStyle[type]){ list.setAttr('class','custom_'+type) }else{ list.setStyle('list-style-type',type) } while(node && node.parentNode.tagName != 'li' && checkListType(node.innerText(),node)){ tmp = node.nextSibling(); if(!tmp){ node.parentNode.insertBefore(list,node) } appendLi(list,node,type); node = tmp; } if(!list.parentNode && node && node.parentNode){ node.parentNode.insertBefore(list,node) } } var span = cacheNode.firstChild(); if(span && span.type == 'element' && span.tagName == 'span' && /^\s*( )+\s*$/.test(span.innerText())){ span.parentNode.removeChild(span) } }) } }); //调整索引标签 me.addListener('contentchange',function(){ adjustListStyle(me.document) }); function adjustListStyle(doc,ignore){ utils.each(domUtils.getElementsByTagName(doc,'ol ul'),function(node){ if(!domUtils.inDoc(node,doc)) return; var parent = node.parentNode; if(parent.tagName == node.tagName){ var nodeStyleType = getStyle(node) || (node.tagName == 'OL' ? 'decimal' : 'disc'), parentStyleType = getStyle(parent) || (parent.tagName == 'OL' ? 'decimal' : 'disc'); if(nodeStyleType == parentStyleType){ var styleIndex = utils.indexOf(listStyle[node.tagName], nodeStyleType); styleIndex = styleIndex + 1 == listStyle[node.tagName].length ? 0 : styleIndex + 1; setListStyle(node,listStyle[node.tagName][styleIndex]) } } var index = 0,type = 2; if( domUtils.hasClass(node,/custom_/)){ if(!(/[ou]l/i.test(parent.tagName) && domUtils.hasClass(parent,/custom_/))){ type = 1; } }else{ if(/[ou]l/i.test(parent.tagName) && domUtils.hasClass(parent,/custom_/)){ type = 3; } } var style = domUtils.getStyle(node, 'list-style-type'); style && (node.style.cssText = 'list-style-type:' + style); node.className = utils.trim(node.className.replace(/list-paddingleft-\w+/,'')) + ' list-paddingleft-' + type; utils.each(domUtils.getElementsByTagName(node,'li'),function(li){ li.style.cssText && (li.style.cssText = ''); if(!li.firstChild){ domUtils.remove(li); return; } if(li.parentNode !== node){ return; } index++; if(domUtils.hasClass(node,/custom_/) ){ var paddingLeft = 1,currentStyle = getStyle(node); if(node.tagName == 'OL'){ if(currentStyle){ switch(currentStyle){ case 'cn' : case 'cn1': case 'cn2': if(index > 10 && (index % 10 == 0 || index > 10 && index < 20)){ paddingLeft = 2 }else if(index > 20){ paddingLeft = 3 } break; case 'num2' : if(index > 9){ paddingLeft = 2 } } } li.className = 'list-'+customStyle[currentStyle]+ index + ' ' + 'list-'+currentStyle+'-paddingleft-' + paddingLeft; }else{ li.className = 'list-'+customStyle[currentStyle] + ' ' + 'list-'+currentStyle+'-paddingleft'; } }else{ li.className = li.className.replace(/list-[\w\-]+/gi,''); } var className = li.getAttribute('class'); if(className !== null && !className.replace(/\s/g,'')){ domUtils.removeAttributes(li,'class') } }); !ignore && adjustList(node,node.tagName.toLowerCase(),getStyle(node)||domUtils.getStyle(node, 'list-style-type'),true); }) } function adjustList(list, tag, style,ignoreEmpty) { var nextList = list.nextSibling; if (nextList && nextList.nodeType == 1 && nextList.tagName.toLowerCase() == tag && (getStyle(nextList) || domUtils.getStyle(nextList, 'list-style-type') || (tag == 'ol' ? 'decimal' : 'disc')) == style) { domUtils.moveChild(nextList, list); if (nextList.childNodes.length == 0) { domUtils.remove(nextList); } } if(nextList && domUtils.isFillChar(nextList)){ domUtils.remove(nextList); } var preList = list.previousSibling; if (preList && preList.nodeType == 1 && preList.tagName.toLowerCase() == tag && (getStyle(preList) || domUtils.getStyle(preList, 'list-style-type') || (tag == 'ol' ? 'decimal' : 'disc')) == style) { domUtils.moveChild(list, preList); } if(preList && domUtils.isFillChar(preList)){ domUtils.remove(preList); } !ignoreEmpty && domUtils.isEmptyBlock(list) && domUtils.remove(list); if(getStyle(list)){ adjustListStyle(list.ownerDocument,true) } } function setListStyle(list,style){ if(customStyle[style]){ list.className = 'custom_' + style; } try{ domUtils.setStyle(list, 'list-style-type', style); }catch(e){} } function clearEmptySibling(node) { var tmpNode = node.previousSibling; if (tmpNode && domUtils.isEmptyBlock(tmpNode)) { domUtils.remove(tmpNode); } tmpNode = node.nextSibling; if (tmpNode && domUtils.isEmptyBlock(tmpNode)) { domUtils.remove(tmpNode); } } me.addListener('keydown', function (type, evt) { function preventAndSave() { evt.preventDefault ? evt.preventDefault() : (evt.returnValue = false); me.fireEvent('contentchange'); me.undoManger && me.undoManger.save(); } function findList(node,filterFn){ while(node && !domUtils.isBody(node)){ if(filterFn(node)){ return null } if(node.nodeType == 1 && /[ou]l/i.test(node.tagName)){ return node; } node = node.parentNode; } return null; } var keyCode = evt.keyCode || evt.which; if (keyCode == 13 && !evt.shiftKey) {//回车 var rng = me.selection.getRange(), parent = domUtils.findParent(rng.startContainer,function(node){return domUtils.isBlockElm(node)},true), li = domUtils.findParentByTagName(rng.startContainer,'li',true); if(parent && parent.tagName != 'PRE' && !li){ var html = parent.innerHTML.replace(new RegExp(domUtils.fillChar, 'g'),''); if(/^\s*1\s*\.[^\d]/.test(html)){ parent.innerHTML = html.replace(/^\s*1\s*\./,''); rng.setStartAtLast(parent).collapse(true).select(); me.__hasEnterExecCommand = true; me.execCommand('insertorderedlist'); me.__hasEnterExecCommand = false; } } var range = me.selection.getRange(), start = findList(range.startContainer,function (node) { return node.tagName == 'TABLE'; }), end = range.collapsed ? start : findList(range.endContainer,function (node) { return node.tagName == 'TABLE'; }); if (start && end && start === end) { if (!range.collapsed) { start = domUtils.findParentByTagName(range.startContainer, 'li', true); end = domUtils.findParentByTagName(range.endContainer, 'li', true); if (start && end && start === end) { range.deleteContents(); li = domUtils.findParentByTagName(range.startContainer, 'li', true); if (li && domUtils.isEmptyBlock(li)) { pre = li.previousSibling; next = li.nextSibling; p = me.document.createElement('p'); domUtils.fillNode(me.document, p); parentList = li.parentNode; if (pre && next) { range.setStart(next, 0).collapse(true).select(true); domUtils.remove(li); } else { if (!pre && !next || !pre) { parentList.parentNode.insertBefore(p, parentList); } else { li.parentNode.parentNode.insertBefore(p, parentList.nextSibling); } domUtils.remove(li); if (!parentList.firstChild) { domUtils.remove(parentList); } range.setStart(p, 0).setCursor(); } preventAndSave(); return; } } else { var tmpRange = range.cloneRange(), bk = tmpRange.collapse(false).createBookmark(); range.deleteContents(); tmpRange.moveToBookmark(bk); var li = domUtils.findParentByTagName(tmpRange.startContainer, 'li', true); clearEmptySibling(li); tmpRange.select(); preventAndSave(); return; } } li = domUtils.findParentByTagName(range.startContainer, 'li', true); if (li) { if (domUtils.isEmptyBlock(li)) { bk = range.createBookmark(); var parentList = li.parentNode; if (li !== parentList.lastChild) { domUtils.breakParent(li, parentList); clearEmptySibling(li); } else { parentList.parentNode.insertBefore(li, parentList.nextSibling); if (domUtils.isEmptyNode(parentList)) { domUtils.remove(parentList); } } //嵌套不处理 if (!dtd.$list[li.parentNode.tagName]) { if (!domUtils.isBlockElm(li.firstChild)) { p = me.document.createElement('p'); li.parentNode.insertBefore(p, li); while (li.firstChild) { p.appendChild(li.firstChild); } domUtils.remove(li); } else { domUtils.remove(li, true); } } range.moveToBookmark(bk).select(); } else { var first = li.firstChild; if (!first || !domUtils.isBlockElm(first)) { var p = me.document.createElement('p'); !li.firstChild && domUtils.fillNode(me.document, p); while (li.firstChild) { p.appendChild(li.firstChild); } li.appendChild(p); first = p; } var span = me.document.createElement('span'); range.insertNode(span); domUtils.breakParent(span, li); var nextLi = span.nextSibling; first = nextLi.firstChild; if (!first) { p = me.document.createElement('p'); domUtils.fillNode(me.document, p); nextLi.appendChild(p); first = p; } if (domUtils.isEmptyNode(first)) { first.innerHTML = ''; domUtils.fillNode(me.document, first); } range.setStart(first, 0).collapse(true).shrinkBoundary().select(); domUtils.remove(span); var pre = nextLi.previousSibling; if (pre && domUtils.isEmptyBlock(pre)) { pre.innerHTML = '

    '; domUtils.fillNode(me.document, pre.firstChild); } } // } preventAndSave(); } } } if (keyCode == 8) { //修中ie中li下的问题 range = me.selection.getRange(); if (range.collapsed && domUtils.isStartInblock(range)) { tmpRange = range.cloneRange().trimBoundary(); li = domUtils.findParentByTagName(range.startContainer, 'li', true); //要在li的最左边,才能处理 if (li && domUtils.isStartInblock(tmpRange)) { start = domUtils.findParentByTagName(range.startContainer, 'p', true); if (start && start !== li.firstChild) { var parentList = domUtils.findParentByTagName(start,['ol','ul']); domUtils.breakParent(start,parentList); clearEmptySibling(start); me.fireEvent('contentchange'); range.setStart(start,0).setCursor(false,true); me.fireEvent('saveScene'); domUtils.preventDefault(evt); return; } if (li && (pre = li.previousSibling)) { if (keyCode == 46 && li.childNodes.length) { return; } //有可能上边的兄弟节点是个2级菜单,要追加到2级菜单的最后的li if (dtd.$list[pre.tagName]) { pre = pre.lastChild; } me.undoManger && me.undoManger.save(); first = li.firstChild; if (domUtils.isBlockElm(first)) { if (domUtils.isEmptyNode(first)) { // range.setEnd(pre, pre.childNodes.length).shrinkBoundary().collapse().select(true); pre.appendChild(first); range.setStart(first, 0).setCursor(false, true); //first不是唯一的节点 while (li.firstChild) { pre.appendChild(li.firstChild); } } else { span = me.document.createElement('span'); range.insertNode(span); //判断pre是否是空的节点,如果是


    类型的空节点,干掉p标签防止它占位 if (domUtils.isEmptyBlock(pre)) { pre.innerHTML = ''; } domUtils.moveChild(li, pre); range.setStartBefore(span).collapse(true).select(true); domUtils.remove(span); } } else { if (domUtils.isEmptyNode(li)) { var p = me.document.createElement('p'); pre.appendChild(p); range.setStart(p, 0).setCursor(); // range.setEnd(pre, pre.childNodes.length).shrinkBoundary().collapse().select(true); } else { range.setEnd(pre, pre.childNodes.length).collapse().select(true); while (li.firstChild) { pre.appendChild(li.firstChild); } } } domUtils.remove(li); me.fireEvent('contentchange'); me.fireEvent('saveScene'); domUtils.preventDefault(evt); return; } //trace:980 if (li && !li.previousSibling) { var parentList = li.parentNode; var bk = range.createBookmark(); if(domUtils.isTagNode(parentList.parentNode,'ol ul')){ parentList.parentNode.insertBefore(li,parentList); if(domUtils.isEmptyNode(parentList)){ domUtils.remove(parentList) } }else{ while(li.firstChild){ parentList.parentNode.insertBefore(li.firstChild,parentList); } domUtils.remove(li); if(domUtils.isEmptyNode(parentList)){ domUtils.remove(parentList) } } range.moveToBookmark(bk).setCursor(false,true); me.fireEvent('contentchange'); me.fireEvent('saveScene'); domUtils.preventDefault(evt); return; } } } } }); me.addListener('keyup',function(type, evt){ var keyCode = evt.keyCode || evt.which; if (keyCode == 8) { var rng = me.selection.getRange(),list; if(list = domUtils.findParentByTagName(rng.startContainer,['ol', 'ul'],true)){ adjustList(list,list.tagName.toLowerCase(),getStyle(list)||domUtils.getComputedStyle(list,'list-style-type'),true) } } }); //处理tab键 me.addListener('tabkeydown',function(){ var range = me.selection.getRange(); //控制级数 function checkLevel(li){ if(me.options.maxListLevel != -1){ var level = li.parentNode,levelNum = 0; while(/[ou]l/i.test(level.tagName)){ levelNum++; level = level.parentNode; } if(levelNum >= me.options.maxListLevel){ return true; } } } //只以开始为准 //todo 后续改进 var li = domUtils.findParentByTagName(range.startContainer, 'li', true); if(li){ var bk; if(range.collapsed){ if(checkLevel(li)) return true; var parentLi = li.parentNode, list = me.document.createElement(parentLi.tagName), index = utils.indexOf(listStyle[list.tagName], getStyle(parentLi)||domUtils.getComputedStyle(parentLi, 'list-style-type')); index = index + 1 == listStyle[list.tagName].length ? 0 : index + 1; var currentStyle = listStyle[list.tagName][index]; setListStyle(list,currentStyle); if(domUtils.isStartInblock(range)){ me.fireEvent('saveScene'); bk = range.createBookmark(); parentLi.insertBefore(list, li); list.appendChild(li); adjustList(list,list.tagName.toLowerCase(),currentStyle); me.fireEvent('contentchange'); range.moveToBookmark(bk).select(true); return true; } }else{ me.fireEvent('saveScene'); bk = range.createBookmark(); for(var i= 0,closeList,parents = domUtils.findParents(li),ci;ci=parents[i++];){ if(domUtils.isTagNode(ci,'ol ul')){ closeList = ci; break; } } var current = li; if(bk.end){ while(current && !(domUtils.getPosition(current, bk.end) & domUtils.POSITION_FOLLOWING)){ if(checkLevel(current)){ current = domUtils.getNextDomNode(current,false,null,function(node){return node !== closeList}); continue; } var parentLi = current.parentNode, list = me.document.createElement(parentLi.tagName), index = utils.indexOf(listStyle[list.tagName], getStyle(parentLi)||domUtils.getComputedStyle(parentLi, 'list-style-type')); var currentIndex = index + 1 == listStyle[list.tagName].length ? 0 : index + 1; var currentStyle = listStyle[list.tagName][currentIndex]; setListStyle(list,currentStyle); parentLi.insertBefore(list, current); while(current && !(domUtils.getPosition(current, bk.end) & domUtils.POSITION_FOLLOWING)){ li = current.nextSibling; list.appendChild(current); if(!li || domUtils.isTagNode(li,'ol ul')){ if(li){ while(li = li.firstChild){ if(li.tagName == 'LI'){ break; } } }else{ li = domUtils.getNextDomNode(current,false,null,function(node){return node !== closeList}); } break; } current = li; } adjustList(list,list.tagName.toLowerCase(),currentStyle); current = li; } } me.fireEvent('contentchange'); range.moveToBookmark(bk).select(); return true; } } }); function getLi(start){ while(start && !domUtils.isBody(start)){ if(start.nodeName == 'TABLE'){ return null; } if(start.nodeName == 'LI'){ return start } start = start.parentNode; } } /** * 有序列表,与“insertunorderedlist”命令互斥 * @command insertorderedlist * @method execCommand * @param { String } command 命令字符串 * @param { String } style 插入的有序列表类型,值为:decimal,lower-alpha,lower-roman,upper-alpha,upper-roman,cn,cn1,cn2,num,num1,num2 * @example * ```javascript * editor.execCommand( 'insertorderedlist','decimal'); * ``` */ /** * 查询当前选区内容是否有序列表 * @command insertorderedlist * @method queryCommandState * @param { String } cmd 命令字符串 * @return { int } 如果当前选区是有序列表返回1,否则返回0 * @example * ```javascript * editor.queryCommandState( 'insertorderedlist' ); * ``` */ /** * 查询当前选区内容是否有序列表 * @command insertorderedlist * @method queryCommandValue * @param { String } cmd 命令字符串 * @return { String } 返回当前有序列表的类型,值为null或decimal,lower-alpha,lower-roman,upper-alpha,upper-roman,cn,cn1,cn2,num,num1,num2 * @example * ```javascript * editor.queryCommandValue( 'insertorderedlist' ); * ``` */ /** * 无序列表,与“insertorderedlist”命令互斥 * @command insertunorderedlist * @method execCommand * @param { String } command 命令字符串 * @param { String } style 插入的无序列表类型,值为:circle,disc,square,dash,dot * @example * ```javascript * editor.execCommand( 'insertunorderedlist','circle'); * ``` */ /** * 查询当前是否有word文档粘贴进来的图片 * @command insertunorderedlist * @method insertunorderedlist * @param { String } command 命令字符串 * @return { int } 如果当前选区是无序列表返回1,否则返回0 * @example * ```javascript * editor.queryCommandState( 'insertunorderedlist' ); * ``` */ /** * 查询当前选区内容是否有序列表 * @command insertunorderedlist * @method queryCommandValue * @param { String } command 命令字符串 * @return { String } 返回当前无序列表的类型,值为null或circle,disc,square,dash,dot * @example * ```javascript * editor.queryCommandValue( 'insertunorderedlist' ); * ``` */ me.commands['insertorderedlist'] = me.commands['insertunorderedlist'] = { execCommand:function (command, style) { if (!style) { style = command.toLowerCase() == 'insertorderedlist' ? 'decimal' : 'disc'; } var me = this, range = this.selection.getRange(), filterFn = function (node) { return node.nodeType == 1 ? node.tagName.toLowerCase() != 'br' : !domUtils.isWhitespace(node); }, tag = command.toLowerCase() == 'insertorderedlist' ? 'ol' : 'ul', frag = me.document.createDocumentFragment(); //去掉是因为会出现选到末尾,导致adjustmentBoundary缩到ol/ul的位置 //range.shrinkBoundary();//.adjustmentBoundary(); range.adjustmentBoundary().shrinkBoundary(); var bko = range.createBookmark(true), start = getLi(me.document.getElementById(bko.start)), modifyStart = 0, end = getLi(me.document.getElementById(bko.end)), modifyEnd = 0, startParent, endParent, list, tmp; if (start || end) { start && (startParent = start.parentNode); if (!bko.end) { end = start; } end && (endParent = end.parentNode); if (startParent === endParent) { while (start !== end) { tmp = start; start = start.nextSibling; if (!domUtils.isBlockElm(tmp.firstChild)) { var p = me.document.createElement('p'); while (tmp.firstChild) { p.appendChild(tmp.firstChild); } tmp.appendChild(p); } frag.appendChild(tmp); } tmp = me.document.createElement('span'); startParent.insertBefore(tmp, end); if (!domUtils.isBlockElm(end.firstChild)) { p = me.document.createElement('p'); while (end.firstChild) { p.appendChild(end.firstChild); } end.appendChild(p); } frag.appendChild(end); domUtils.breakParent(tmp, startParent); if (domUtils.isEmptyNode(tmp.previousSibling)) { domUtils.remove(tmp.previousSibling); } if (domUtils.isEmptyNode(tmp.nextSibling)) { domUtils.remove(tmp.nextSibling) } var nodeStyle = getStyle(startParent) || domUtils.getComputedStyle(startParent, 'list-style-type') || (command.toLowerCase() == 'insertorderedlist' ? 'decimal' : 'disc'); if (startParent.tagName.toLowerCase() == tag && nodeStyle == style) { for (var i = 0, ci, tmpFrag = me.document.createDocumentFragment(); ci = frag.firstChild;) { if(domUtils.isTagNode(ci,'ol ul')){ // 删除时,子列表不处理 // utils.each(domUtils.getElementsByTagName(ci,'li'),function(li){ // while(li.firstChild){ // tmpFrag.appendChild(li.firstChild); // } // // }); tmpFrag.appendChild(ci); }else{ while (ci.firstChild) { tmpFrag.appendChild(ci.firstChild); domUtils.remove(ci); } } } tmp.parentNode.insertBefore(tmpFrag, tmp); } else { list = me.document.createElement(tag); setListStyle(list,style); list.appendChild(frag); tmp.parentNode.insertBefore(list, tmp); } domUtils.remove(tmp); list && adjustList(list, tag, style); range.moveToBookmark(bko).select(); return; } //开始 if (start) { while (start) { tmp = start.nextSibling; if (domUtils.isTagNode(start, 'ol ul')) { frag.appendChild(start); } else { var tmpfrag = me.document.createDocumentFragment(), hasBlock = 0; while (start.firstChild) { if (domUtils.isBlockElm(start.firstChild)) { hasBlock = 1; } tmpfrag.appendChild(start.firstChild); } if (!hasBlock) { var tmpP = me.document.createElement('p'); tmpP.appendChild(tmpfrag); frag.appendChild(tmpP); } else { frag.appendChild(tmpfrag); } domUtils.remove(start); } start = tmp; } startParent.parentNode.insertBefore(frag, startParent.nextSibling); if (domUtils.isEmptyNode(startParent)) { range.setStartBefore(startParent); domUtils.remove(startParent); } else { range.setStartAfter(startParent); } modifyStart = 1; } if (end && domUtils.inDoc(endParent, me.document)) { //结束 start = endParent.firstChild; while (start && start !== end) { tmp = start.nextSibling; if (domUtils.isTagNode(start, 'ol ul')) { frag.appendChild(start); } else { tmpfrag = me.document.createDocumentFragment(); hasBlock = 0; while (start.firstChild) { if (domUtils.isBlockElm(start.firstChild)) { hasBlock = 1; } tmpfrag.appendChild(start.firstChild); } if (!hasBlock) { tmpP = me.document.createElement('p'); tmpP.appendChild(tmpfrag); frag.appendChild(tmpP); } else { frag.appendChild(tmpfrag); } domUtils.remove(start); } start = tmp; } var tmpDiv = domUtils.createElement(me.document, 'div', { 'tmpDiv':1 }); domUtils.moveChild(end, tmpDiv); frag.appendChild(tmpDiv); domUtils.remove(end); endParent.parentNode.insertBefore(frag, endParent); range.setEndBefore(endParent); if (domUtils.isEmptyNode(endParent)) { domUtils.remove(endParent); } modifyEnd = 1; } } if (!modifyStart) { range.setStartBefore(me.document.getElementById(bko.start)); } if (bko.end && !modifyEnd) { range.setEndAfter(me.document.getElementById(bko.end)); } range.enlarge(true, function (node) { return notExchange[node.tagName]; }); frag = me.document.createDocumentFragment(); var bk = range.createBookmark(), current = domUtils.getNextDomNode(bk.start, false, filterFn), tmpRange = range.cloneRange(), tmpNode, block = domUtils.isBlockElm; while (current && current !== bk.end && (domUtils.getPosition(current, bk.end) & domUtils.POSITION_PRECEDING)) { if (current.nodeType == 3 || dtd.li[current.tagName]) { if (current.nodeType == 1 && dtd.$list[current.tagName]) { while (current.firstChild) { frag.appendChild(current.firstChild); } tmpNode = domUtils.getNextDomNode(current, false, filterFn); domUtils.remove(current); current = tmpNode; continue; } tmpNode = current; tmpRange.setStartBefore(current); while (current && current !== bk.end && (!block(current) || domUtils.isBookmarkNode(current) )) { tmpNode = current; current = domUtils.getNextDomNode(current, false, null, function (node) { return !notExchange[node.tagName]; }); } if (current && block(current)) { tmp = domUtils.getNextDomNode(tmpNode, false, filterFn); if (tmp && domUtils.isBookmarkNode(tmp)) { current = domUtils.getNextDomNode(tmp, false, filterFn); tmpNode = tmp; } } tmpRange.setEndAfter(tmpNode); current = domUtils.getNextDomNode(tmpNode, false, filterFn); var li = range.document.createElement('li'); li.appendChild(tmpRange.extractContents()); if(domUtils.isEmptyNode(li)){ var tmpNode = range.document.createElement('p'); while(li.firstChild){ tmpNode.appendChild(li.firstChild) } li.appendChild(tmpNode); } frag.appendChild(li); } else { current = domUtils.getNextDomNode(current, true, filterFn); } } range.moveToBookmark(bk).collapse(true); list = me.document.createElement(tag); setListStyle(list,style); list.appendChild(frag); range.insertNode(list); //当前list上下看能否合并 adjustList(list, tag, style); //去掉冗余的tmpDiv for (var i = 0, ci, tmpDivs = domUtils.getElementsByTagName(list, 'div'); ci = tmpDivs[i++];) { if (ci.getAttribute('tmpDiv')) { domUtils.remove(ci, true) } } range.moveToBookmark(bko).select(); }, queryCommandState:function (command) { var tag = command.toLowerCase() == 'insertorderedlist' ? 'ol' : 'ul'; var path = this.selection.getStartElementPath(); for(var i= 0,ci;ci = path[i++];){ if(ci.nodeName == 'TABLE'){ return 0 } if(tag == ci.nodeName.toLowerCase()){ return 1 }; } return 0; }, queryCommandValue:function (command) { var tag = command.toLowerCase() == 'insertorderedlist' ? 'ol' : 'ul'; var path = this.selection.getStartElementPath(), node; for(var i= 0,ci;ci = path[i++];){ if(ci.nodeName == 'TABLE'){ node = null; break; } if(tag == ci.nodeName.toLowerCase()){ node = ci; break; }; } return node ? getStyle(node) || domUtils.getComputedStyle(node, 'list-style-type') : null; } }; }; // plugins/source.js /** * 源码编辑插件 * @file * @since 1.2.6.1 */ (function (){ var sourceEditors = { textarea: function (editor, holder){ var textarea = holder.ownerDocument.createElement('textarea'); textarea.style.cssText = 'position:absolute;resize:none;width:100%;height:100%;border:0;padding:0;margin:0;overflow-y:auto;'; // todo: IE下只有onresize属性可用... 很纠结 if (browser.ie && browser.version < 8) { textarea.style.width = holder.offsetWidth + 'px'; textarea.style.height = holder.offsetHeight + 'px'; holder.onresize = function (){ textarea.style.width = holder.offsetWidth + 'px'; textarea.style.height = holder.offsetHeight + 'px'; }; } holder.appendChild(textarea); return { setContent: function (content){ textarea.value = content; }, getContent: function (){ return textarea.value; }, select: function (){ var range; if (browser.ie) { range = textarea.createTextRange(); range.collapse(true); range.select(); } else { //todo: chrome下无法设置焦点 textarea.setSelectionRange(0, 0); textarea.focus(); } }, dispose: function (){ holder.removeChild(textarea); // todo holder.onresize = null; textarea = null; holder = null; } }; }, codemirror: function (editor, holder){ var codeEditor = window.CodeMirror(holder, { mode: "text/html", tabMode: "indent", lineNumbers: true, lineWrapping:true }); var dom = codeEditor.getWrapperElement(); dom.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:100%;font-family:consolas,"Courier new",monospace;font-size:13px;'; codeEditor.getScrollerElement().style.cssText = 'position:absolute;left:0;top:0;width:100%;height:100%;'; codeEditor.refresh(); return { getCodeMirror:function(){ return codeEditor; }, setContent: function (content){ codeEditor.setValue(content); }, getContent: function (){ return codeEditor.getValue(); }, select: function (){ codeEditor.focus(); }, dispose: function (){ holder.removeChild(dom); dom = null; codeEditor = null; } }; } }; UE.plugins['source'] = function (){ var me = this; var opt = this.options; var sourceMode = false; var sourceEditor; var orgSetContent; opt.sourceEditor = browser.ie ? 'textarea' : (opt.sourceEditor || 'codemirror'); me.setOpt({ sourceEditorFirst:false }); function createSourceEditor(holder){ return sourceEditors[opt.sourceEditor == 'codemirror' && window.CodeMirror ? 'codemirror' : 'textarea'](me, holder); } var bakCssText; //解决在源码模式下getContent不能得到最新的内容问题 var oldGetContent, bakAddress; /** * 切换源码模式和编辑模式 * @command source * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'source'); * ``` */ /** * 查询当前编辑区域的状态是源码模式还是可视化模式 * @command source * @method queryCommandState * @param { String } cmd 命令字符串 * @return { int } 如果当前是源码编辑模式,返回1,否则返回0 * @example * ```javascript * editor.queryCommandState( 'source' ); * ``` */ me.commands['source'] = { execCommand: function (){ sourceMode = !sourceMode; if (sourceMode) { bakAddress = me.selection.getRange().createAddress(false,true); me.undoManger && me.undoManger.save(true); if(browser.gecko){ me.body.contentEditable = false; } bakCssText = me.iframe.style.cssText; me.iframe.style.cssText += 'position:absolute;left:-32768px;top:-32768px;'; me.fireEvent('beforegetcontent'); var root = UE.htmlparser(me.body.innerHTML); me.filterOutputRule(root); root.traversal(function (node) { if (node.type == 'element') { switch (node.tagName) { case 'td': case 'th': case 'caption': if(node.children && node.children.length == 1){ if(node.firstChild().tagName == 'br' ){ node.removeChild(node.firstChild()) } }; break; case 'pre': node.innerText(node.innerText().replace(/ /g,' ')) } } }); me.fireEvent('aftergetcontent'); var content = root.toHtml(true); sourceEditor = createSourceEditor(me.iframe.parentNode); sourceEditor.setContent(content); orgSetContent = me.setContent; me.setContent = function(html){ //这里暂时不触发事件,防止报错 var root = UE.htmlparser(html); me.filterInputRule(root); html = root.toHtml(); sourceEditor.setContent(html); }; setTimeout(function (){ sourceEditor.select(); me.addListener('fullscreenchanged', function(){ try{ sourceEditor.getCodeMirror().refresh() }catch(e){} }); }); //重置getContent,源码模式下取值也能是最新的数据 oldGetContent = me.getContent; me.getContent = function (){ return sourceEditor.getContent() || '

    ' + (browser.ie ? '' : '
    ')+'

    '; }; } else { me.iframe.style.cssText = bakCssText; var cont = sourceEditor.getContent() || '

    ' + (browser.ie ? '' : '
    ')+'

    '; //处理掉block节点前后的空格,有可能会误命中,暂时不考虑 cont = cont.replace(new RegExp('[\\r\\t\\n ]*<\/?(\\w+)\\s*(?:[^>]*)>','g'), function(a,b){ if(b && !dtd.$inlineWithA[b.toLowerCase()]){ return a.replace(/(^[\n\r\t ]*)|([\n\r\t ]*$)/g,''); } return a.replace(/(^[\n\r\t]*)|([\n\r\t]*$)/g,'') }); me.setContent = orgSetContent; me.setContent(cont); sourceEditor.dispose(); sourceEditor = null; //还原getContent方法 me.getContent = oldGetContent; var first = me.body.firstChild; //trace:1106 都删除空了,下边会报错,所以补充一个p占位 if(!first){ me.body.innerHTML = '

    '+(browser.ie?'':'
    ')+'

    '; first = me.body.firstChild; } //要在ifm为显示时ff才能取到selection,否则报错 //这里不能比较位置了 me.undoManger && me.undoManger.save(true); if(browser.gecko){ var input = document.createElement('input'); input.style.cssText = 'position:absolute;left:0;top:-32768px'; document.body.appendChild(input); me.body.contentEditable = false; setTimeout(function(){ domUtils.setViewportOffset(input, { left: -32768, top: 0 }); input.focus(); setTimeout(function(){ me.body.contentEditable = true; me.selection.getRange().moveToAddress(bakAddress).select(true); domUtils.remove(input); }); }); }else{ //ie下有可能报错,比如在代码顶头的情况 try{ me.selection.getRange().moveToAddress(bakAddress).select(true); }catch(e){} } } this.fireEvent('sourcemodechanged', sourceMode); }, queryCommandState: function (){ return sourceMode|0; }, notNeedUndo : 1 }; var oldQueryCommandState = me.queryCommandState; me.queryCommandState = function (cmdName){ cmdName = cmdName.toLowerCase(); if (sourceMode) { //源码模式下可以开启的命令 return cmdName in { 'source' : 1, 'fullscreen' : 1 } ? 1 : -1 } return oldQueryCommandState.apply(this, arguments); }; if(opt.sourceEditor == "codemirror"){ me.addListener("ready",function(){ utils.loadFile(document,{ src : opt.codeMirrorJsUrl || opt.UEDITOR_HOME_URL + "third-party/codemirror/codemirror.js", tag : "script", type : "text/javascript", defer : "defer" },function(){ if(opt.sourceEditorFirst){ setTimeout(function(){ me.execCommand("source"); },0); } }); utils.loadFile(document,{ tag : "link", rel : "stylesheet", type : "text/css", href : opt.codeMirrorCssUrl || opt.UEDITOR_HOME_URL + "third-party/codemirror/codemirror.css" }); }); } }; })(); // plugins/enterkey.js ///import core ///import plugins/undo.js ///commands 设置回车标签p或br ///commandsName EnterKey ///commandsTitle 设置回车标签p或br /** * @description 处理回车 * @author zhanyi */ UE.plugins['enterkey'] = function() { var hTag, me = this, tag = me.options.enterTag; me.addListener('keyup', function(type, evt) { var keyCode = evt.keyCode || evt.which; if (keyCode == 13) { var range = me.selection.getRange(), start = range.startContainer, doSave; //修正在h1-h6里边回车后不能嵌套p的问题 if (!browser.ie) { if (/h\d/i.test(hTag)) { if (browser.gecko) { var h = domUtils.findParentByTagName(start, [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6','blockquote','caption','table'], true); if (!h) { me.document.execCommand('formatBlock', false, '

    '); doSave = 1; } } else { //chrome remove div if (start.nodeType == 1) { var tmp = me.document.createTextNode(''),div; range.insertNode(tmp); div = domUtils.findParentByTagName(tmp, 'div', true); if (div) { var p = me.document.createElement('p'); while (div.firstChild) { p.appendChild(div.firstChild); } div.parentNode.insertBefore(p, div); domUtils.remove(div); range.setStartBefore(tmp).setCursor(); doSave = 1; } domUtils.remove(tmp); } } if (me.undoManger && doSave) { me.undoManger.save(); } } //没有站位符,会出现多行的问题 browser.opera && range.select(); }else{ me.fireEvent('saveScene',true,true) } } }); me.addListener('keydown', function(type, evt) { var keyCode = evt.keyCode || evt.which; if (keyCode == 13) {//回车 if(me.fireEvent('beforeenterkeydown')){ domUtils.preventDefault(evt); return; } me.fireEvent('saveScene',true,true); hTag = ''; var range = me.selection.getRange(); if (!range.collapsed) { //跨td不能删 var start = range.startContainer, end = range.endContainer, startTd = domUtils.findParentByTagName(start, 'td', true), endTd = domUtils.findParentByTagName(end, 'td', true); if (startTd && endTd && startTd !== endTd || !startTd && endTd || startTd && !endTd) { evt.preventDefault ? evt.preventDefault() : ( evt.returnValue = false); return; } } if (tag == 'p') { if (!browser.ie) { start = domUtils.findParentByTagName(range.startContainer, ['ol','ul','p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6','blockquote','caption'], true); //opera下执行formatblock会在table的场景下有问题,回车在opera原生支持很好,所以暂时在opera去掉调用这个原生的command //trace:2431 if (!start && !browser.opera) { me.document.execCommand('formatBlock', false, '

    '); if (browser.gecko) { range = me.selection.getRange(); start = domUtils.findParentByTagName(range.startContainer, 'p', true); start && domUtils.removeDirtyAttr(start); } } else { hTag = start.tagName; start.tagName.toLowerCase() == 'p' && browser.gecko && domUtils.removeDirtyAttr(start); } } } else { evt.preventDefault ? evt.preventDefault() : ( evt.returnValue = false); if (!range.collapsed) { range.deleteContents(); start = range.startContainer; if (start.nodeType == 1 && (start = start.childNodes[range.startOffset])) { while (start.nodeType == 1) { if (dtd.$empty[start.tagName]) { range.setStartBefore(start).setCursor(); if (me.undoManger) { me.undoManger.save(); } return false; } if (!start.firstChild) { var br = range.document.createElement('br'); start.appendChild(br); range.setStart(start, 0).setCursor(); if (me.undoManger) { me.undoManger.save(); } return false; } start = start.firstChild; } if (start === range.startContainer.childNodes[range.startOffset]) { br = range.document.createElement('br'); range.insertNode(br).setCursor(); } else { range.setStart(start, 0).setCursor(); } } else { br = range.document.createElement('br'); range.insertNode(br).setStartAfter(br).setCursor(); } } else { br = range.document.createElement('br'); range.insertNode(br); var parent = br.parentNode; if (parent.lastChild === br) { br.parentNode.insertBefore(br.cloneNode(true), br); range.setStartBefore(br); } else { range.setStartAfter(br); } range.setCursor(); } } } }); }; // plugins/keystrokes.js /* 处理特殊键的兼容性问题 */ UE.plugins['keystrokes'] = function() { var me = this; var collapsed = true; me.addListener('keydown', function(type, evt) { var keyCode = evt.keyCode || evt.which, rng = me.selection.getRange(); //处理全选的情况 if(!rng.collapsed && !(evt.ctrlKey || evt.shiftKey || evt.altKey || evt.metaKey) && (keyCode >= 65 && keyCode <=90 || keyCode >= 48 && keyCode <= 57 || keyCode >= 96 && keyCode <= 111 || { 13:1, 8:1, 46:1 }[keyCode]) ){ var tmpNode = rng.startContainer; if(domUtils.isFillChar(tmpNode)){ rng.setStartBefore(tmpNode) } tmpNode = rng.endContainer; if(domUtils.isFillChar(tmpNode)){ rng.setEndAfter(tmpNode) } rng.txtToElmBoundary(); //结束边界可能放到了br的前边,要把br包含进来 // x[xxx]
    if(rng.endContainer && rng.endContainer.nodeType == 1){ tmpNode = rng.endContainer.childNodes[rng.endOffset]; if(tmpNode && domUtils.isBr(tmpNode)){ rng.setEndAfter(tmpNode); } } if(rng.startOffset == 0){ tmpNode = rng.startContainer; if(domUtils.isBoundaryNode(tmpNode,'firstChild') ){ tmpNode = rng.endContainer; if(rng.endOffset == (tmpNode.nodeType == 3 ? tmpNode.nodeValue.length : tmpNode.childNodes.length) && domUtils.isBoundaryNode(tmpNode,'lastChild')){ me.fireEvent('saveScene'); me.body.innerHTML = '

    '+(browser.ie ? '' : '
    ')+'

    '; rng.setStart(me.body.firstChild,0).setCursor(false,true); me._selectionChange(); return; } } } } //处理backspace if (keyCode == keymap.Backspace) { rng = me.selection.getRange(); collapsed = rng.collapsed; if(me.fireEvent('delkeydown',evt)){ return; } var start,end; //避免按两次删除才能生效的问题 if(rng.collapsed && rng.inFillChar()){ start = rng.startContainer; if(domUtils.isFillChar(start)){ rng.setStartBefore(start).shrinkBoundary(true).collapse(true); domUtils.remove(start) }else{ start.nodeValue = start.nodeValue.replace(new RegExp('^' + domUtils.fillChar ),''); rng.startOffset--; rng.collapse(true).select(true) } } //解决选中control元素不能删除的问题 if (start = rng.getClosedNode()) { me.fireEvent('saveScene'); rng.setStartBefore(start); domUtils.remove(start); rng.setCursor(); me.fireEvent('saveScene'); domUtils.preventDefault(evt); return; } //阻止在table上的删除 if (!browser.ie) { start = domUtils.findParentByTagName(rng.startContainer, 'table', true); end = domUtils.findParentByTagName(rng.endContainer, 'table', true); if (start && !end || !start && end || start !== end) { evt.preventDefault(); return; } } } //处理tab键的逻辑 if (keyCode == keymap.Tab) { //不处理以下标签 var excludeTagNameForTabKey = { 'ol' : 1, 'ul' : 1, 'table':1 }; //处理组件里的tab按下事件 if(me.fireEvent('tabkeydown',evt)){ domUtils.preventDefault(evt); return; } var range = me.selection.getRange(); me.fireEvent('saveScene'); for (var i = 0,txt = '',tabSize = me.options.tabSize|| 4,tabNode = me.options.tabNode || ' '; i < tabSize; i++) { txt += tabNode; } var span = me.document.createElement('span'); span.innerHTML = txt + domUtils.fillChar; if (range.collapsed) { range.insertNode(span.cloneNode(true).firstChild).setCursor(true); } else { var filterFn = function(node) { return domUtils.isBlockElm(node) && !excludeTagNameForTabKey[node.tagName.toLowerCase()] }; //普通的情况 start = domUtils.findParent(range.startContainer, filterFn,true); end = domUtils.findParent(range.endContainer, filterFn,true); if (start && end && start === end) { range.deleteContents(); range.insertNode(span.cloneNode(true).firstChild).setCursor(true); } else { var bookmark = range.createBookmark(); range.enlarge(true); var bookmark2 = range.createBookmark(), current = domUtils.getNextDomNode(bookmark2.start, false, filterFn); while (current && !(domUtils.getPosition(current, bookmark2.end) & domUtils.POSITION_FOLLOWING)) { current.insertBefore(span.cloneNode(true).firstChild, current.firstChild); current = domUtils.getNextDomNode(current, false, filterFn); } range.moveToBookmark(bookmark2).moveToBookmark(bookmark).select(); } } domUtils.preventDefault(evt) } //trace:1634 //ff的del键在容器空的时候,也会删除 if(browser.gecko && keyCode == 46){ range = me.selection.getRange(); if(range.collapsed){ start = range.startContainer; if(domUtils.isEmptyBlock(start)){ var parent = start.parentNode; while(domUtils.getChildCount(parent) == 1 && !domUtils.isBody(parent)){ start = parent; parent = parent.parentNode; } if(start === parent.lastChild) evt.preventDefault(); return; } } } }); me.addListener('keyup', function(type, evt) { var keyCode = evt.keyCode || evt.which, rng,me = this; if(keyCode == keymap.Backspace){ if(me.fireEvent('delkeyup')){ return; } rng = me.selection.getRange(); if(rng.collapsed){ var tmpNode, autoClearTagName = ['h1','h2','h3','h4','h5','h6']; if(tmpNode = domUtils.findParentByTagName(rng.startContainer,autoClearTagName,true)){ if(domUtils.isEmptyBlock(tmpNode)){ var pre = tmpNode.previousSibling; if(pre && pre.nodeName != 'TABLE'){ domUtils.remove(tmpNode); rng.setStartAtLast(pre).setCursor(false,true); return; }else{ var next = tmpNode.nextSibling; if(next && next.nodeName != 'TABLE'){ domUtils.remove(tmpNode); rng.setStartAtFirst(next).setCursor(false,true); return; } } } } //处理当删除到body时,要重新给p标签展位 if(domUtils.isBody(rng.startContainer)){ var tmpNode = domUtils.createElement(me.document,'p',{ 'innerHTML' : browser.ie ? domUtils.fillChar : '
    ' }); rng.insertNode(tmpNode).setStart(tmpNode,0).setCursor(false,true); } } //chrome下如果删除了inline标签,浏览器会有记忆,在输入文字还是会套上刚才删除的标签,所以这里再选一次就不会了 if( !collapsed && (rng.startContainer.nodeType == 3 || rng.startContainer.nodeType == 1 && domUtils.isEmptyBlock(rng.startContainer))){ if(browser.ie){ var span = rng.document.createElement('span'); rng.insertNode(span).setStartBefore(span).collapse(true); rng.select(); domUtils.remove(span) }else{ rng.select() } } } }) }; // plugins/fiximgclick.js ///import core ///commands 修复chrome下图片不能点击的问题,出现八个角可改变大小 ///commandsName FixImgClick ///commandsTitle 修复chrome下图片不能点击的问题,出现八个角可改变大小 //修复chrome下图片不能点击的问题,出现八个角可改变大小 UE.plugins['fiximgclick'] = (function () { var elementUpdated = false; function Scale() { this.editor = null; this.resizer = null; this.cover = null; this.doc = document; this.prePos = {x: 0, y: 0}; this.startPos = {x: 0, y: 0}; } (function () { var rect = [ //[left, top, width, height] [0, 0, -1, -1], [0, 0, 0, -1], [0, 0, 1, -1], [0, 0, -1, 0], [0, 0, 1, 0], [0, 0, -1, 1], [0, 0, 0, 1], [0, 0, 1, 1] ]; Scale.prototype = { init: function (editor) { var me = this; me.editor = editor; me.startPos = this.prePos = {x: 0, y: 0}; me.dragId = -1; var hands = [], cover = me.cover = document.createElement('div'), resizer = me.resizer = document.createElement('div'); cover.id = me.editor.ui.id + '_imagescale_cover'; cover.style.cssText = 'position:absolute;display:none;z-index:' + (me.editor.options.zIndex) + ';filter:alpha(opacity=0); opacity:0;background:#CCC;'; domUtils.on(cover, 'mousedown click', function () { me.hide(); }); for (i = 0; i < 8; i++) { hands.push(''); } resizer.id = me.editor.ui.id + '_imagescale'; resizer.className = 'edui-editor-imagescale'; resizer.innerHTML = hands.join(''); resizer.style.cssText += ';display:none;border:1px solid #3b77ff;z-index:' + (me.editor.options.zIndex) + ';'; me.editor.ui.getDom().appendChild(cover); me.editor.ui.getDom().appendChild(resizer); me.initStyle(); me.initEvents(); }, initStyle: function () { utils.cssRule('imagescale', '.edui-editor-imagescale{display:none;position:absolute;border:1px solid #38B2CE;cursor:hand;-webkit-box-sizing: content-box;-moz-box-sizing: content-box;box-sizing: content-box;}' + '.edui-editor-imagescale span{position:absolute;width:6px;height:6px;overflow:hidden;font-size:0px;display:block;background-color:#3C9DD0;}' + '.edui-editor-imagescale .edui-editor-imagescale-hand0{cursor:nw-resize;top:0;margin-top:-4px;left:0;margin-left:-4px;}' + '.edui-editor-imagescale .edui-editor-imagescale-hand1{cursor:n-resize;top:0;margin-top:-4px;left:50%;margin-left:-4px;}' + '.edui-editor-imagescale .edui-editor-imagescale-hand2{cursor:ne-resize;top:0;margin-top:-4px;left:100%;margin-left:-3px;}' + '.edui-editor-imagescale .edui-editor-imagescale-hand3{cursor:w-resize;top:50%;margin-top:-4px;left:0;margin-left:-4px;}' + '.edui-editor-imagescale .edui-editor-imagescale-hand4{cursor:e-resize;top:50%;margin-top:-4px;left:100%;margin-left:-3px;}' + '.edui-editor-imagescale .edui-editor-imagescale-hand5{cursor:sw-resize;top:100%;margin-top:-3px;left:0;margin-left:-4px;}' + '.edui-editor-imagescale .edui-editor-imagescale-hand6{cursor:s-resize;top:100%;margin-top:-3px;left:50%;margin-left:-4px;}' + '.edui-editor-imagescale .edui-editor-imagescale-hand7{cursor:se-resize;top:100%;margin-top:-3px;left:100%;margin-left:-3px;}'); }, initEvents: function () { var me = this; me.startPos.x = me.startPos.y = 0; me.isDraging = false; }, _eventHandler: function (e) { var me = this; switch (e.type) { case 'mousedown': var hand = e.target || e.srcElement, hand; if (hand.className.indexOf('edui-editor-imagescale-hand') != -1 && me.dragId == -1) { me.dragId = hand.className.slice(-1); me.startPos.x = me.prePos.x = e.clientX; me.startPos.y = me.prePos.y = e.clientY; domUtils.on(me.doc,'mousemove', me.proxy(me._eventHandler, me)); } break; case 'mousemove': if (me.dragId != -1) { me.updateContainerStyle(me.dragId, {x: e.clientX - me.prePos.x, y: e.clientY - me.prePos.y}); me.prePos.x = e.clientX; me.prePos.y = e.clientY; elementUpdated = true; me.updateTargetElement(); } break; case 'mouseup': if (me.dragId != -1) { me.updateContainerStyle(me.dragId, {x: e.clientX - me.prePos.x, y: e.clientY - me.prePos.y}); me.updateTargetElement(); if (me.target.parentNode) me.attachTo(me.target); me.dragId = -1; } domUtils.un(me.doc,'mousemove', me.proxy(me._eventHandler, me)); //修复只是点击挪动点,但没有改变大小,不应该触发contentchange if(elementUpdated){ elementUpdated = false; me.editor.fireEvent('contentchange'); } break; default: break; } }, updateTargetElement: function () { var me = this; domUtils.setStyles(me.target, { 'width': me.resizer.style.width, 'height': me.resizer.style.height }); me.target.width = parseInt(me.resizer.style.width); me.target.height = parseInt(me.resizer.style.height); me.attachTo(me.target); }, updateContainerStyle: function (dir, offset) { var me = this, dom = me.resizer, tmp; if (rect[dir][0] != 0) { tmp = parseInt(dom.style.left) + offset.x; dom.style.left = me._validScaledProp('left', tmp) + 'px'; } if (rect[dir][1] != 0) { tmp = parseInt(dom.style.top) + offset.y; dom.style.top = me._validScaledProp('top', tmp) + 'px'; } if (rect[dir][2] != 0) { tmp = dom.clientWidth + rect[dir][2] * offset.x; dom.style.width = me._validScaledProp('width', tmp) + 'px'; } if (rect[dir][3] != 0) { tmp = dom.clientHeight + rect[dir][3] * offset.y; dom.style.height = me._validScaledProp('height', tmp) + 'px'; } }, _validScaledProp: function (prop, value) { var ele = this.resizer, wrap = document; value = isNaN(value) ? 0 : value; switch (prop) { case 'left': return value < 0 ? 0 : (value + ele.clientWidth) > wrap.clientWidth ? wrap.clientWidth - ele.clientWidth : value; case 'top': return value < 0 ? 0 : (value + ele.clientHeight) > wrap.clientHeight ? wrap.clientHeight - ele.clientHeight : value; case 'width': return value <= 0 ? 1 : (value + ele.offsetLeft) > wrap.clientWidth ? wrap.clientWidth - ele.offsetLeft : value; case 'height': return value <= 0 ? 1 : (value + ele.offsetTop) > wrap.clientHeight ? wrap.clientHeight - ele.offsetTop : value; } }, hideCover: function () { this.cover.style.display = 'none'; }, showCover: function () { var me = this, editorPos = domUtils.getXY(me.editor.ui.getDom()), iframePos = domUtils.getXY(me.editor.iframe); domUtils.setStyles(me.cover, { 'width': me.editor.iframe.offsetWidth + 'px', 'height': me.editor.iframe.offsetHeight + 'px', 'top': iframePos.y - editorPos.y + 'px', 'left': iframePos.x - editorPos.x + 'px', 'position': 'absolute', 'display': '' }) }, show: function (targetObj) { var me = this; me.resizer.style.display = 'block'; if(targetObj) me.attachTo(targetObj); domUtils.on(this.resizer, 'mousedown', me.proxy(me._eventHandler, me)); domUtils.on(me.doc, 'mouseup', me.proxy(me._eventHandler, me)); me.showCover(); me.editor.fireEvent('afterscaleshow', me); me.editor.fireEvent('saveScene'); }, hide: function () { var me = this; me.hideCover(); me.resizer.style.display = 'none'; domUtils.un(me.resizer, 'mousedown', me.proxy(me._eventHandler, me)); domUtils.un(me.doc, 'mouseup', me.proxy(me._eventHandler, me)); me.editor.fireEvent('afterscalehide', me); }, proxy: function( fn, context ) { return function(e) { return fn.apply( context || this, arguments); }; }, attachTo: function (targetObj) { var me = this, target = me.target = targetObj, resizer = this.resizer, imgPos = domUtils.getXY(target), iframePos = domUtils.getXY(me.editor.iframe), editorPos = domUtils.getXY(resizer.parentNode); domUtils.setStyles(resizer, { 'width': target.width + 'px', 'height': target.height + 'px', 'left': iframePos.x + imgPos.x - me.editor.document.body.scrollLeft - editorPos.x - parseInt(resizer.style.borderLeftWidth) + 'px', 'top': iframePos.y + imgPos.y - me.editor.document.body.scrollTop - editorPos.y - parseInt(resizer.style.borderTopWidth) + 'px' }); } } })(); return function () { var me = this, imageScale; me.setOpt('imageScaleEnabled', true); if ( !browser.ie && me.options.imageScaleEnabled) { me.addListener('click', function (type, e) { var range = me.selection.getRange(), img = range.getClosedNode(); if (img && img.tagName == 'IMG' && me.body.contentEditable!="false") { if (img.className.indexOf("edui-faked-music") != -1 || img.getAttribute("anchorname") || domUtils.hasClass(img, 'loadingclass') || domUtils.hasClass(img, 'loaderrorclass')) { return } if (!imageScale) { imageScale = new Scale(); imageScale.init(me); me.ui.getDom().appendChild(imageScale.resizer); var _keyDownHandler = function (e) { imageScale.hide(); if(imageScale.target) me.selection.getRange().selectNode(imageScale.target).select(); }, _mouseDownHandler = function (e) { var ele = e.target || e.srcElement; if (ele && (ele.className===undefined || ele.className.indexOf('edui-editor-imagescale') == -1)) { _keyDownHandler(e); } }, timer; me.addListener('afterscaleshow', function (e) { me.addListener('beforekeydown', _keyDownHandler); me.addListener('beforemousedown', _mouseDownHandler); domUtils.on(document, 'keydown', _keyDownHandler); domUtils.on(document,'mousedown', _mouseDownHandler); me.selection.getNative().removeAllRanges(); }); me.addListener('afterscalehide', function (e) { me.removeListener('beforekeydown', _keyDownHandler); me.removeListener('beforemousedown', _mouseDownHandler); domUtils.un(document, 'keydown', _keyDownHandler); domUtils.un(document,'mousedown', _mouseDownHandler); var target = imageScale.target; if (target.parentNode) { me.selection.getRange().selectNode(target).select(); } }); //TODO 有iframe的情况,mousedown不能往下传。。 domUtils.on(imageScale.resizer, 'mousedown', function (e) { me.selection.getNative().removeAllRanges(); var ele = e.target || e.srcElement; if (ele && ele.className.indexOf('edui-editor-imagescale-hand') == -1) { timer = setTimeout(function () { imageScale.hide(); if(imageScale.target) me.selection.getRange().selectNode(ele).select(); }, 200); } }); domUtils.on(imageScale.resizer, 'mouseup', function (e) { var ele = e.target || e.srcElement; if (ele && ele.className.indexOf('edui-editor-imagescale-hand') == -1) { clearTimeout(timer); } }); } imageScale.show(img); } else { if (imageScale && imageScale.resizer.style.display != 'none') imageScale.hide(); } }); } if (browser.webkit) { me.addListener('click', function (type, e) { if (e.target.tagName == 'IMG' && me.body.contentEditable!="false") { var range = new dom.Range(me.document); range.selectNode(e.target).select(); } }); } } })(); // plugins/autolink.js ///import core ///commands 为非ie浏览器自动添加a标签 ///commandsName AutoLink ///commandsTitle 自动增加链接 /** * @description 为非ie浏览器自动添加a标签 * @author zhanyi */ UE.plugin.register('autolink',function(){ var cont = 0; return !browser.ie ? { bindEvents:{ 'reset' : function(){ cont = 0; }, 'keydown':function(type, evt) { var me = this; var keyCode = evt.keyCode || evt.which; if (keyCode == 32 || keyCode == 13) { var sel = me.selection.getNative(), range = sel.getRangeAt(0).cloneRange(), offset, charCode; var start = range.startContainer; while (start.nodeType == 1 && range.startOffset > 0) { start = range.startContainer.childNodes[range.startOffset - 1]; if (!start){ break; } range.setStart(start, start.nodeType == 1 ? start.childNodes.length : start.nodeValue.length); range.collapse(true); start = range.startContainer; } do{ if (range.startOffset == 0) { start = range.startContainer.previousSibling; while (start && start.nodeType == 1) { start = start.lastChild; } if (!start || domUtils.isFillChar(start)){ break; } offset = start.nodeValue.length; } else { start = range.startContainer; offset = range.startOffset; } range.setStart(start, offset - 1); charCode = range.toString().charCodeAt(0); } while (charCode != 160 && charCode != 32); if (range.toString().replace(new RegExp(domUtils.fillChar, 'g'), '').match(/(?:https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.)/i)) { while(range.toString().length){ if(/^(?:https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.)/i.test(range.toString())){ break; } try{ range.setStart(range.startContainer,range.startOffset+1); }catch(e){ //trace:2121 var start = range.startContainer; while(!(next = start.nextSibling)){ if(domUtils.isBody(start)){ return; } start = start.parentNode; } range.setStart(next,0); } } //range的开始边界已经在a标签里的不再处理 if(domUtils.findParentByTagName(range.startContainer,'a',true)){ return; } var a = me.document.createElement('a'),text = me.document.createTextNode(' '),href; me.undoManger && me.undoManger.save(); a.appendChild(range.extractContents()); a.href = a.innerHTML = a.innerHTML.replace(/<[^>]+>/g,''); href = a.getAttribute("href").replace(new RegExp(domUtils.fillChar,'g'),''); href = /^(?:https?:\/\/)/ig.test(href) ? href : "http://"+ href; a.setAttribute('_src',utils.html(href)); a.href = utils.html(href); range.insertNode(a); a.parentNode.insertBefore(text, a.nextSibling); range.setStart(text, 0); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); me.undoManger && me.undoManger.save(); } } } } }:{} },function(){ var keyCodes = { 37:1, 38:1, 39:1, 40:1, 13:1,32:1 }; function checkIsCludeLink(node){ if(node.nodeType == 3){ return null } if(node.nodeName == 'A'){ return node; } var lastChild = node.lastChild; while(lastChild){ if(lastChild.nodeName == 'A'){ return lastChild; } if(lastChild.nodeType == 3){ if(domUtils.isWhitespace(lastChild)){ lastChild = lastChild.previousSibling; continue; } return null } lastChild = lastChild.lastChild; } } browser.ie && this.addListener('keyup',function(cmd,evt){ var me = this,keyCode = evt.keyCode; if(keyCodes[keyCode]){ var rng = me.selection.getRange(); var start = rng.startContainer; if(keyCode == 13){ while(start && !domUtils.isBody(start) && !domUtils.isBlockElm(start)){ start = start.parentNode; } if(start && !domUtils.isBody(start) && start.nodeName == 'P'){ var pre = start.previousSibling; if(pre && pre.nodeType == 1){ var pre = checkIsCludeLink(pre); if(pre && !pre.getAttribute('_href')){ domUtils.remove(pre,true); } } } }else if(keyCode == 32 ){ if(start.nodeType == 3 && /^\s$/.test(start.nodeValue)){ start = start.previousSibling; if(start && start.nodeName == 'A' && !start.getAttribute('_href')){ domUtils.remove(start,true); } } }else { start = domUtils.findParentByTagName(start,'a',true); if(start && !start.getAttribute('_href')){ var bk = rng.createBookmark(); domUtils.remove(start,true); rng.moveToBookmark(bk).select(true) } } } }); } ); // plugins/autoheight.js ///import core ///commands 当输入内容超过编辑器高度时,编辑器自动增高 ///commandsName AutoHeight,autoHeightEnabled ///commandsTitle 自动增高 /** * @description 自动伸展 * @author zhanyi */ UE.plugins['autoheight'] = function () { var me = this; //提供开关,就算加载也可以关闭 me.autoHeightEnabled = me.options.autoHeightEnabled !== false; if (!me.autoHeightEnabled) { return; } var bakOverflow, lastHeight = 0, options = me.options, currentHeight, timer; function adjustHeight() { var me = this; clearTimeout(timer); if(isFullscreen)return; if (!me.queryCommandState || me.queryCommandState && me.queryCommandState('source') != 1) { timer = setTimeout(function(){ var node = me.body.lastChild; while(node && node.nodeType != 1){ node = node.previousSibling; } if(node && node.nodeType == 1){ node.style.clear = 'both'; currentHeight = Math.max(domUtils.getXY(node).y + node.offsetHeight + 25 ,Math.max(options.minFrameHeight, options.initialFrameHeight)) ; if (currentHeight != lastHeight) { if (currentHeight !== parseInt(me.iframe.parentNode.style.height)) { me.iframe.parentNode.style.height = currentHeight + 'px'; } me.body.style.height = currentHeight + 'px'; lastHeight = currentHeight; } domUtils.removeStyle(node,'clear'); } },50) } } var isFullscreen; me.addListener('fullscreenchanged',function(cmd,f){ isFullscreen = f }); me.addListener('destroy', function () { me.removeListener('contentchange afterinserthtml keyup mouseup',adjustHeight) }); me.enableAutoHeight = function () { var me = this; if (!me.autoHeightEnabled) { return; } var doc = me.document; me.autoHeightEnabled = true; bakOverflow = doc.body.style.overflowY; doc.body.style.overflowY = 'hidden'; me.addListener('contentchange afterinserthtml keyup mouseup',adjustHeight); //ff不给事件算得不对 setTimeout(function () { adjustHeight.call(me); }, browser.gecko ? 100 : 0); me.fireEvent('autoheightchanged', me.autoHeightEnabled); }; me.disableAutoHeight = function () { me.body.style.overflowY = bakOverflow || ''; me.removeListener('contentchange', adjustHeight); me.removeListener('keyup', adjustHeight); me.removeListener('mouseup', adjustHeight); me.autoHeightEnabled = false; me.fireEvent('autoheightchanged', me.autoHeightEnabled); }; me.on('setHeight',function(){ me.disableAutoHeight() }); me.addListener('ready', function () { me.enableAutoHeight(); //trace:1764 var timer; domUtils.on(browser.ie ? me.body : me.document, browser.webkit ? 'dragover' : 'drop', function () { clearTimeout(timer); timer = setTimeout(function () { //trace:3681 adjustHeight.call(me); }, 100); }); //修复内容过多时,回到顶部,顶部内容被工具栏遮挡问题 var lastScrollY; window.onscroll = function(){ if(lastScrollY === null){ lastScrollY = this.scrollY }else if(this.scrollY == 0 && lastScrollY != 0){ me.window.scrollTo(0,0); lastScrollY = null; } } }); }; // plugins/autofloat.js ///import core ///commands 悬浮工具栏 ///commandsName AutoFloat,autoFloatEnabled ///commandsTitle 悬浮工具栏 /** * modified by chengchao01 * 注意: 引入此功能后,在IE6下会将body的背景图片覆盖掉! */ UE.plugins['autofloat'] = function() { var me = this, lang = me.getLang(); me.setOpt({ topOffset:0 }); var optsAutoFloatEnabled = me.options.autoFloatEnabled !== false, topOffset = me.options.topOffset; //如果不固定toolbar的位置,则直接退出 if(!optsAutoFloatEnabled){ return; } var uiUtils = UE.ui.uiUtils, LteIE6 = browser.ie && browser.version <= 6, quirks = browser.quirks; function checkHasUI(){ if(!UE.ui){ alert(lang.autofloatMsg); return 0; } return 1; } function fixIE6FixedPos(){ var docStyle = document.body.style; docStyle.backgroundImage = 'url("about:blank")'; docStyle.backgroundAttachment = 'fixed'; } var bakCssText, placeHolder = document.createElement('div'), toolbarBox,orgTop, getPosition, flag =true; //ie7模式下需要偏移 function setFloating(){ var toobarBoxPos = domUtils.getXY(toolbarBox), origalFloat = domUtils.getComputedStyle(toolbarBox,'position'), origalLeft = domUtils.getComputedStyle(toolbarBox,'left'); toolbarBox.style.width = toolbarBox.offsetWidth + 'px'; toolbarBox.style.zIndex = me.options.zIndex * 1 + 1; toolbarBox.parentNode.insertBefore(placeHolder, toolbarBox); if (LteIE6 || (quirks && browser.ie)) { if(toolbarBox.style.position != 'absolute'){ toolbarBox.style.position = 'absolute'; } toolbarBox.style.top = (document.body.scrollTop||document.documentElement.scrollTop) - orgTop + topOffset + 'px'; } else { if (browser.ie7Compat && flag) { flag = false; toolbarBox.style.left = domUtils.getXY(toolbarBox).x - document.documentElement.getBoundingClientRect().left+2 + 'px'; } if(toolbarBox.style.position != 'fixed'){ toolbarBox.style.position = 'fixed'; toolbarBox.style.top = topOffset +"px"; ((origalFloat == 'absolute' || origalFloat == 'relative') && parseFloat(origalLeft)) && (toolbarBox.style.left = toobarBoxPos.x + 'px'); } } } function unsetFloating(){ flag = true; if(placeHolder.parentNode){ placeHolder.parentNode.removeChild(placeHolder); } toolbarBox.style.cssText = bakCssText; } function updateFloating(){ var rect3 = getPosition(me.container); var offset=me.options.toolbarTopOffset||0; if (rect3.top < 0 && rect3.bottom - toolbarBox.offsetHeight > offset) { setFloating(); }else{ unsetFloating(); } } var defer_updateFloating = utils.defer(function(){ updateFloating(); },browser.ie ? 200 : 100,true); me.addListener('destroy',function(){ domUtils.un(window, ['scroll','resize'], updateFloating); me.removeListener('keydown', defer_updateFloating); }); me.addListener('ready', function(){ if(checkHasUI(me)){ //加载了ui组件,但在new时,没有加载ui,导致编辑器实例上没有ui类,所以这里做判断 if(!me.ui){ return; } getPosition = uiUtils.getClientRect; toolbarBox = me.ui.getDom('toolbarbox'); orgTop = getPosition(toolbarBox).top; bakCssText = toolbarBox.style.cssText; placeHolder.style.height = toolbarBox.offsetHeight + 'px'; if(LteIE6){ fixIE6FixedPos(); } domUtils.on(window, ['scroll','resize'], updateFloating); me.addListener('keydown', defer_updateFloating); me.addListener('beforefullscreenchange', function (t, enabled){ if (enabled) { unsetFloating(); } }); me.addListener('fullscreenchanged', function (t, enabled){ if (!enabled) { updateFloating(); } }); me.addListener('sourcemodechanged', function (t, enabled){ setTimeout(function (){ updateFloating(); },0); }); me.addListener("clearDoc",function(){ setTimeout(function(){ updateFloating(); },0); }) } }); }; // plugins/video.js /** * video插件, 为UEditor提供视频插入支持 * @file * @since 1.2.6.1 */ UE.plugins['video'] = function (){ var me =this; /** * 创建插入视频字符窜 * @param url 视频地址 * @param width 视频宽度 * @param height 视频高度 * @param align 视频对齐 * @param toEmbed 是否以flash代替显示 * @param addParagraph 是否需要添加P 标签 */ function creatInsertStr(url,width,height,id,align,classname,type){ url = utils.unhtmlForUrl(url); align = utils.unhtml(align); classname = utils.unhtml(classname).trim(); width = parseInt(width, 10) || 0; height = parseInt(height, 10) || 0; var str; switch (type){ case 'image': str = '' break; case 'embed': str = ''; break; case 'video': var ext = url.substr(url.lastIndexOf('.') + 1); if(ext == 'ogv') ext = 'ogg'; str = '' + ''; break; } return str; } function switchImgAndVideo(root,img2video){ utils.each(root.getNodesByTagName(img2video ? 'img' : 'embed video'),function(node){ var className = node.getAttr('class'); if(className && className.indexOf('edui-faked-video') != -1){ var html = creatInsertStr( img2video ? node.getAttr('_url') : node.getAttr('src'),node.getAttr('width'),node.getAttr('height'),null,node.getStyle('float') || '',className,img2video ? 'embed':'image'); node.parentNode.replaceChild(UE.uNode.createElement(html),node); } if(className && className.indexOf('edui-upload-video') != -1){ var html = creatInsertStr( img2video ? node.getAttr('_url') : node.getAttr('src'),node.getAttr('width'),node.getAttr('height'),null,node.getStyle('float') || '',className,img2video ? 'video':'image'); node.parentNode.replaceChild(UE.uNode.createElement(html),node); } }) } me.addOutputRule(function(root){ switchImgAndVideo(root,true) }); me.addInputRule(function(root){ switchImgAndVideo(root) }); /** * 插入视频 * @command insertvideo * @method execCommand * @param { String } cmd 命令字符串 * @param { Object } videoAttr 键值对对象, 描述一个视频的所有属性 * @example * ```javascript * * var videoAttr = { * //视频地址 * url: 'http://www.youku.com/xxx', * //视频宽高值, 单位px * width: 200, * height: 100 * }; * * //editor 是编辑器实例 * //向编辑器插入单个视频 * editor.execCommand( 'insertvideo', videoAttr ); * ``` */ /** * 插入视频 * @command insertvideo * @method execCommand * @param { String } cmd 命令字符串 * @param { Array } videoArr 需要插入的视频的数组, 其中的每一个元素都是一个键值对对象, 描述了一个视频的所有属性 * @example * ```javascript * * var videoAttr1 = { * //视频地址 * url: 'http://www.youku.com/xxx', * //视频宽高值, 单位px * width: 200, * height: 100 * }, * videoAttr2 = { * //视频地址 * url: 'http://www.youku.com/xxx', * //视频宽高值, 单位px * width: 200, * height: 100 * } * * //editor 是编辑器实例 * //该方法将会向编辑器内插入两个视频 * editor.execCommand( 'insertvideo', [ videoAttr1, videoAttr2 ] ); * ``` */ /** * 查询当前光标所在处是否是一个视频 * @command insertvideo * @method queryCommandState * @param { String } cmd 需要查询的命令字符串 * @return { int } 如果当前光标所在处的元素是一个视频对象, 则返回1,否则返回0 * @example * ```javascript * * //editor 是编辑器实例 * editor.queryCommandState( 'insertvideo' ); * ``` */ me.commands["insertvideo"] = { execCommand: function (cmd, videoObjs, type){ videoObjs = utils.isArray(videoObjs)?videoObjs:[videoObjs]; var html = [],id = 'tmpVedio', cl; for(var i=0,vi,len = videoObjs.length;i 0) { return 0; } for (var i in dtd.$isNotEmpty) if (dtd.$isNotEmpty.hasOwnProperty(i)) { if (node.getElementsByTagName(i).length) { return 0; } } return 1; }; UETable.getWidth = function (cell) { if (!cell)return 0; return parseInt(domUtils.getComputedStyle(cell, "width"), 10); }; /** * 获取单元格或者单元格组的“对齐”状态。 如果当前的检测对象是一个单元格组, 只有在满足所有单元格的 水平和竖直 对齐属性都相同的 * 条件时才会返回其状态值,否则将返回null; 如果当前只检测了一个单元格, 则直接返回当前单元格的对齐状态; * @param table cell or table cells , 支持单个单元格dom对象 或者 单元格dom对象数组 * @return { align: 'left' || 'right' || 'center', valign: 'top' || 'middle' || 'bottom' } 或者 null */ UETable.getTableCellAlignState = function ( cells ) { !utils.isArray( cells ) && ( cells = [cells] ); var result = {}, status = ['align', 'valign'], tempStatus = null, isSame = true;//状态是否相同 utils.each( cells, function( cellNode ){ utils.each( status, function( currentState ){ tempStatus = cellNode.getAttribute( currentState ); if( !result[ currentState ] && tempStatus ) { result[ currentState ] = tempStatus; } else if( !result[ currentState ] || ( tempStatus !== result[ currentState ] ) ) { isSame = false; return false; } } ); return isSame; }); return isSame ? result : null; }; /** * 根据当前选区获取相关的table信息 * @return {Object} */ UETable.getTableItemsByRange = function (editor) { var start = editor.selection.getStart(); //ff下会选中bookmark if( start && start.id && start.id.indexOf('_baidu_bookmark_start_') === 0 && start.nextSibling) { start = start.nextSibling; } //在table或者td边缘有可能存在选中tr的情况 var cell = start && domUtils.findParentByTagName(start, ["td", "th"], true), tr = cell && cell.parentNode, caption = start && domUtils.findParentByTagName(start, 'caption', true), table = caption ? caption.parentNode : tr && tr.parentNode.parentNode; return { cell:cell, tr:tr, table:table, caption:caption } }; UETable.getUETableBySelected = function (editor) { var table = UETable.getTableItemsByRange(editor).table; if (table && table.ueTable && table.ueTable.selectedTds.length) { return table.ueTable; } return null; }; UETable.getDefaultValue = function (editor, table) { var borderMap = { thin:'0px', medium:'1px', thick:'2px' }, tableBorder, tdPadding, tdBorder, tmpValue; if (!table) { table = editor.document.createElement('table'); table.insertRow(0).insertCell(0).innerHTML = 'xxx'; editor.body.appendChild(table); var td = table.getElementsByTagName('td')[0]; tmpValue = domUtils.getComputedStyle(table, 'border-left-width'); tableBorder = parseInt(borderMap[tmpValue] || tmpValue, 10); tmpValue = domUtils.getComputedStyle(td, 'padding-left'); tdPadding = parseInt(borderMap[tmpValue] || tmpValue, 10); tmpValue = domUtils.getComputedStyle(td, 'border-left-width'); tdBorder = parseInt(borderMap[tmpValue] || tmpValue, 10); domUtils.remove(table); return { tableBorder:tableBorder, tdPadding:tdPadding, tdBorder:tdBorder }; } else { td = table.getElementsByTagName('td')[0]; tmpValue = domUtils.getComputedStyle(table, 'border-left-width'); tableBorder = parseInt(borderMap[tmpValue] || tmpValue, 10); tmpValue = domUtils.getComputedStyle(td, 'padding-left'); tdPadding = parseInt(borderMap[tmpValue] || tmpValue, 10); tmpValue = domUtils.getComputedStyle(td, 'border-left-width'); tdBorder = parseInt(borderMap[tmpValue] || tmpValue, 10); return { tableBorder:tableBorder, tdPadding:tdPadding, tdBorder:tdBorder }; } }; /** * 根据当前点击的td或者table获取索引对象 * @param tdOrTable */ UETable.getUETable = function (tdOrTable) { var tag = tdOrTable.tagName.toLowerCase(); tdOrTable = (tag == "td" || tag == "th" || tag == 'caption') ? domUtils.findParentByTagName(tdOrTable, "table", true) : tdOrTable; if (!tdOrTable.ueTable) { tdOrTable.ueTable = new UETable(tdOrTable); } return tdOrTable.ueTable; }; UETable.cloneCell = function(cell,ignoreMerge,keepPro){ if (!cell || utils.isString(cell)) { return this.table.ownerDocument.createElement(cell || 'td'); } var flag = domUtils.hasClass(cell, "selectTdClass"); flag && domUtils.removeClasses(cell, "selectTdClass"); var tmpCell = cell.cloneNode(true); if (ignoreMerge) { tmpCell.rowSpan = tmpCell.colSpan = 1; } //去掉宽高 !keepPro && domUtils.removeAttributes(tmpCell,'width height'); !keepPro && domUtils.removeAttributes(tmpCell,'style'); tmpCell.style.borderLeftStyle = ""; tmpCell.style.borderTopStyle = ""; tmpCell.style.borderLeftColor = cell.style.borderRightColor; tmpCell.style.borderLeftWidth = cell.style.borderRightWidth; tmpCell.style.borderTopColor = cell.style.borderBottomColor; tmpCell.style.borderTopWidth = cell.style.borderBottomWidth; flag && domUtils.addClass(cell, "selectTdClass"); return tmpCell; } UETable.prototype = { getMaxRows:function () { var rows = this.table.rows, maxLen = 1; for (var i = 0, row; row = rows[i]; i++) { var currentMax = 1; for (var j = 0, cj; cj = row.cells[j++];) { currentMax = Math.max(cj.rowSpan || 1, currentMax); } maxLen = Math.max(currentMax + i, maxLen); } return maxLen; }, /** * 获取当前表格的最大列数 */ getMaxCols:function () { var rows = this.table.rows, maxLen = 0, cellRows = {}; for (var i = 0, row; row = rows[i]; i++) { var cellsNum = 0; for (var j = 0, cj; cj = row.cells[j++];) { cellsNum += (cj.colSpan || 1); if (cj.rowSpan && cj.rowSpan > 1) { for (var k = 1; k < cj.rowSpan; k++) { if (!cellRows['row_' + (i + k)]) { cellRows['row_' + (i + k)] = (cj.colSpan || 1); } else { cellRows['row_' + (i + k)]++ } } } } cellsNum += cellRows['row_' + i] || 0; maxLen = Math.max(cellsNum, maxLen); } return maxLen; }, getCellColIndex:function (cell) { }, /** * 获取当前cell旁边的单元格, * @param cell * @param right */ getHSideCell:function (cell, right) { try { var cellInfo = this.getCellInfo(cell), previewRowIndex, previewColIndex; var len = this.selectedTds.length, range = this.cellsRange; //首行或者首列没有前置单元格 if ((!right && (!len ? !cellInfo.colIndex : !range.beginColIndex)) || (right && (!len ? (cellInfo.colIndex == (this.colsNum - 1)) : (range.endColIndex == this.colsNum - 1)))) return null; previewRowIndex = !len ? cellInfo.rowIndex : range.beginRowIndex; previewColIndex = !right ? ( !len ? (cellInfo.colIndex < 1 ? 0 : (cellInfo.colIndex - 1)) : range.beginColIndex - 1) : ( !len ? cellInfo.colIndex + 1 : range.endColIndex + 1); return this.getCell(this.indexTable[previewRowIndex][previewColIndex].rowIndex, this.indexTable[previewRowIndex][previewColIndex].cellIndex); } catch (e) { showError(e); } }, getTabNextCell:function (cell, preRowIndex) { var cellInfo = this.getCellInfo(cell), rowIndex = preRowIndex || cellInfo.rowIndex, colIndex = cellInfo.colIndex + 1 + (cellInfo.colSpan - 1), nextCell; try { nextCell = this.getCell(this.indexTable[rowIndex][colIndex].rowIndex, this.indexTable[rowIndex][colIndex].cellIndex); } catch (e) { try { rowIndex = rowIndex * 1 + 1; colIndex = 0; nextCell = this.getCell(this.indexTable[rowIndex][colIndex].rowIndex, this.indexTable[rowIndex][colIndex].cellIndex); } catch (e) { } } return nextCell; }, /** * 获取视觉上的后置单元格 * @param cell * @param bottom */ getVSideCell:function (cell, bottom, ignoreRange) { try { var cellInfo = this.getCellInfo(cell), nextRowIndex, nextColIndex; var len = this.selectedTds.length && !ignoreRange, range = this.cellsRange; //末行或者末列没有后置单元格 if ((!bottom && (cellInfo.rowIndex == 0)) || (bottom && (!len ? (cellInfo.rowIndex + cellInfo.rowSpan > this.rowsNum - 1) : (range.endRowIndex == this.rowsNum - 1)))) return null; nextRowIndex = !bottom ? ( !len ? cellInfo.rowIndex - 1 : range.beginRowIndex - 1) : ( !len ? (cellInfo.rowIndex + cellInfo.rowSpan) : range.endRowIndex + 1); nextColIndex = !len ? cellInfo.colIndex : range.beginColIndex; return this.getCell(this.indexTable[nextRowIndex][nextColIndex].rowIndex, this.indexTable[nextRowIndex][nextColIndex].cellIndex); } catch (e) { showError(e); } }, /** * 获取相同结束位置的单元格,xOrY指代了是获取x轴相同还是y轴相同 */ getSameEndPosCells:function (cell, xOrY) { try { var flag = (xOrY.toLowerCase() === "x"), end = domUtils.getXY(cell)[flag ? 'x' : 'y'] + cell["offset" + (flag ? 'Width' : 'Height')], rows = this.table.rows, cells = null, returns = []; for (var i = 0; i < this.rowsNum; i++) { cells = rows[i].cells; for (var j = 0, tmpCell; tmpCell = cells[j++];) { var tmpEnd = domUtils.getXY(tmpCell)[flag ? 'x' : 'y'] + tmpCell["offset" + (flag ? 'Width' : 'Height')]; //对应行的td已经被上面行rowSpan了 if (tmpEnd > end && flag) break; if (cell == tmpCell || end == tmpEnd) { //只获取单一的单元格 //todo 仅获取单一单元格在特定情况下会造成returns为空,从而影响后续的拖拽实现,修正这个。需考虑性能 if (tmpCell[flag ? "colSpan" : "rowSpan"] == 1) { returns.push(tmpCell); } if (flag) break; } } } return returns; } catch (e) { showError(e); } }, setCellContent:function (cell, content) { cell.innerHTML = content || (browser.ie ? domUtils.fillChar : "
    "); }, cloneCell:UETable.cloneCell, /** * 获取跟当前单元格的右边竖线为左边的所有未合并单元格 */ getSameStartPosXCells:function (cell) { try { var start = domUtils.getXY(cell).x + cell.offsetWidth, rows = this.table.rows, cells , returns = []; for (var i = 0; i < this.rowsNum; i++) { cells = rows[i].cells; for (var j = 0, tmpCell; tmpCell = cells[j++];) { var tmpStart = domUtils.getXY(tmpCell).x; if (tmpStart > start) break; if (tmpStart == start && tmpCell.colSpan == 1) { returns.push(tmpCell); break; } } } return returns; } catch (e) { showError(e); } }, /** * 更新table对应的索引表 */ update:function (table) { this.table = table || this.table; this.selectedTds = []; this.cellsRange = {}; this.indexTable = []; var rows = this.table.rows, rowsNum = this.getMaxRows(), dNum = rowsNum - rows.length, colsNum = this.getMaxCols(); while (dNum--) { this.table.insertRow(rows.length); } this.rowsNum = rowsNum; this.colsNum = colsNum; for (var i = 0, len = rows.length; i < len; i++) { this.indexTable[i] = new Array(colsNum); } //填充索引表 for (var rowIndex = 0, row; row = rows[rowIndex]; rowIndex++) { for (var cellIndex = 0, cell, cells = row.cells; cell = cells[cellIndex]; cellIndex++) { //修正整行被rowSpan时导致的行数计算错误 if (cell.rowSpan > rowsNum) { cell.rowSpan = rowsNum; } var colIndex = cellIndex, rowSpan = cell.rowSpan || 1, colSpan = cell.colSpan || 1; //当已经被上一行rowSpan或者被前一列colSpan了,则跳到下一个单元格进行 while (this.indexTable[rowIndex][colIndex]) colIndex++; for (var j = 0; j < rowSpan; j++) { for (var k = 0; k < colSpan; k++) { this.indexTable[rowIndex + j][colIndex + k] = { rowIndex:rowIndex, cellIndex:cellIndex, colIndex:colIndex, rowSpan:rowSpan, colSpan:colSpan } } } } } //修复残缺td for (j = 0; j < rowsNum; j++) { for (k = 0; k < colsNum; k++) { if (this.indexTable[j][k] === undefined) { row = rows[j]; cell = row.cells[row.cells.length - 1]; cell = cell ? cell.cloneNode(true) : this.table.ownerDocument.createElement("td"); this.setCellContent(cell); if (cell.colSpan !== 1)cell.colSpan = 1; if (cell.rowSpan !== 1)cell.rowSpan = 1; row.appendChild(cell); this.indexTable[j][k] = { rowIndex:j, cellIndex:cell.cellIndex, colIndex:k, rowSpan:1, colSpan:1 } } } } //当框选后删除行或者列后撤销,需要重建选区。 var tds = domUtils.getElementsByTagName(this.table, "td"), selectTds = []; utils.each(tds, function (td) { if (domUtils.hasClass(td, "selectTdClass")) { selectTds.push(td); } }); if (selectTds.length) { var start = selectTds[0], end = selectTds[selectTds.length - 1], startInfo = this.getCellInfo(start), endInfo = this.getCellInfo(end); this.selectedTds = selectTds; this.cellsRange = { beginRowIndex:startInfo.rowIndex, beginColIndex:startInfo.colIndex, endRowIndex:endInfo.rowIndex + endInfo.rowSpan - 1, endColIndex:endInfo.colIndex + endInfo.colSpan - 1 }; } //给第一行设置firstRow的样式名称,在排序图标的样式上使用到 if(!domUtils.hasClass(this.table.rows[0], "firstRow")) { domUtils.addClass(this.table.rows[0], "firstRow"); for(var i = 1; i< this.table.rows.length; i++) { domUtils.removeClasses(this.table.rows[i], "firstRow"); } } }, /** * 获取单元格的索引信息 */ getCellInfo:function (cell) { if (!cell) return; var cellIndex = cell.cellIndex, rowIndex = cell.parentNode.rowIndex, rowInfo = this.indexTable[rowIndex], numCols = this.colsNum; for (var colIndex = cellIndex; colIndex < numCols; colIndex++) { var cellInfo = rowInfo[colIndex]; if (cellInfo.rowIndex === rowIndex && cellInfo.cellIndex === cellIndex) { return cellInfo; } } }, /** * 根据行列号获取单元格 */ getCell:function (rowIndex, cellIndex) { return rowIndex < this.rowsNum && this.table.rows[rowIndex].cells[cellIndex] || null; }, /** * 删除单元格 */ deleteCell:function (cell, rowIndex) { rowIndex = typeof rowIndex == 'number' ? rowIndex : cell.parentNode.rowIndex; var row = this.table.rows[rowIndex]; row.deleteCell(cell.cellIndex); }, /** * 根据始末两个单元格获取被框选的所有单元格范围 */ getCellsRange:function (cellA, cellB) { function checkRange(beginRowIndex, beginColIndex, endRowIndex, endColIndex) { var tmpBeginRowIndex = beginRowIndex, tmpBeginColIndex = beginColIndex, tmpEndRowIndex = endRowIndex, tmpEndColIndex = endColIndex, cellInfo, colIndex, rowIndex; // 通过indexTable检查是否存在超出TableRange上边界的情况 if (beginRowIndex > 0) { for (colIndex = beginColIndex; colIndex < endColIndex; colIndex++) { cellInfo = me.indexTable[beginRowIndex][colIndex]; rowIndex = cellInfo.rowIndex; if (rowIndex < beginRowIndex) { tmpBeginRowIndex = Math.min(rowIndex, tmpBeginRowIndex); } } } // 通过indexTable检查是否存在超出TableRange右边界的情况 if (endColIndex < me.colsNum) { for (rowIndex = beginRowIndex; rowIndex < endRowIndex; rowIndex++) { cellInfo = me.indexTable[rowIndex][endColIndex]; colIndex = cellInfo.colIndex + cellInfo.colSpan - 1; if (colIndex > endColIndex) { tmpEndColIndex = Math.max(colIndex, tmpEndColIndex); } } } // 检查是否有超出TableRange下边界的情况 if (endRowIndex < me.rowsNum) { for (colIndex = beginColIndex; colIndex < endColIndex; colIndex++) { cellInfo = me.indexTable[endRowIndex][colIndex]; rowIndex = cellInfo.rowIndex + cellInfo.rowSpan - 1; if (rowIndex > endRowIndex) { tmpEndRowIndex = Math.max(rowIndex, tmpEndRowIndex); } } } // 检查是否有超出TableRange左边界的情况 if (beginColIndex > 0) { for (rowIndex = beginRowIndex; rowIndex < endRowIndex; rowIndex++) { cellInfo = me.indexTable[rowIndex][beginColIndex]; colIndex = cellInfo.colIndex; if (colIndex < beginColIndex) { tmpBeginColIndex = Math.min(cellInfo.colIndex, tmpBeginColIndex); } } } //递归调用直至所有完成所有框选单元格的扩展 if (tmpBeginRowIndex != beginRowIndex || tmpBeginColIndex != beginColIndex || tmpEndRowIndex != endRowIndex || tmpEndColIndex != endColIndex) { return checkRange(tmpBeginRowIndex, tmpBeginColIndex, tmpEndRowIndex, tmpEndColIndex); } else { // 不需要扩展TableRange的情况 return { beginRowIndex:beginRowIndex, beginColIndex:beginColIndex, endRowIndex:endRowIndex, endColIndex:endColIndex }; } } try { var me = this, cellAInfo = me.getCellInfo(cellA); if (cellA === cellB) { return { beginRowIndex:cellAInfo.rowIndex, beginColIndex:cellAInfo.colIndex, endRowIndex:cellAInfo.rowIndex + cellAInfo.rowSpan - 1, endColIndex:cellAInfo.colIndex + cellAInfo.colSpan - 1 }; } var cellBInfo = me.getCellInfo(cellB); // 计算TableRange的四个边 var beginRowIndex = Math.min(cellAInfo.rowIndex, cellBInfo.rowIndex), beginColIndex = Math.min(cellAInfo.colIndex, cellBInfo.colIndex), endRowIndex = Math.max(cellAInfo.rowIndex + cellAInfo.rowSpan - 1, cellBInfo.rowIndex + cellBInfo.rowSpan - 1), endColIndex = Math.max(cellAInfo.colIndex + cellAInfo.colSpan - 1, cellBInfo.colIndex + cellBInfo.colSpan - 1); return checkRange(beginRowIndex, beginColIndex, endRowIndex, endColIndex); } catch (e) { //throw e; } }, /** * 依据cellsRange获取对应的单元格集合 */ getCells:function (range) { //每次获取cells之前必须先清除上次的选择,否则会对后续获取操作造成影响 this.clearSelected(); var beginRowIndex = range.beginRowIndex, beginColIndex = range.beginColIndex, endRowIndex = range.endRowIndex, endColIndex = range.endColIndex, cellInfo, rowIndex, colIndex, tdHash = {}, returnTds = []; for (var i = beginRowIndex; i <= endRowIndex; i++) { for (var j = beginColIndex; j <= endColIndex; j++) { cellInfo = this.indexTable[i][j]; rowIndex = cellInfo.rowIndex; colIndex = cellInfo.colIndex; // 如果Cells里已经包含了此Cell则跳过 var key = rowIndex + '|' + colIndex; if (tdHash[key]) continue; tdHash[key] = 1; if (rowIndex < i || colIndex < j || rowIndex + cellInfo.rowSpan - 1 > endRowIndex || colIndex + cellInfo.colSpan - 1 > endColIndex) { return null; } returnTds.push(this.getCell(rowIndex, cellInfo.cellIndex)); } } return returnTds; }, /** * 清理已经选中的单元格 */ clearSelected:function () { UETable.removeSelectedClass(this.selectedTds); this.selectedTds = []; this.cellsRange = {}; }, /** * 根据range设置已经选中的单元格 */ setSelected:function (range) { var cells = this.getCells(range); UETable.addSelectedClass(cells); this.selectedTds = cells; this.cellsRange = range; }, isFullRow:function () { var range = this.cellsRange; return (range.endColIndex - range.beginColIndex + 1) == this.colsNum; }, isFullCol:function () { var range = this.cellsRange, table = this.table, ths = table.getElementsByTagName("th"), rows = range.endRowIndex - range.beginRowIndex + 1; return !ths.length ? rows == this.rowsNum : rows == this.rowsNum || (rows == this.rowsNum - 1); }, /** * 获取视觉上的前置单元格,默认是左边,top传入时 * @param cell * @param top */ getNextCell:function (cell, bottom, ignoreRange) { try { var cellInfo = this.getCellInfo(cell), nextRowIndex, nextColIndex; var len = this.selectedTds.length && !ignoreRange, range = this.cellsRange; //末行或者末列没有后置单元格 if ((!bottom && (cellInfo.rowIndex == 0)) || (bottom && (!len ? (cellInfo.rowIndex + cellInfo.rowSpan > this.rowsNum - 1) : (range.endRowIndex == this.rowsNum - 1)))) return null; nextRowIndex = !bottom ? ( !len ? cellInfo.rowIndex - 1 : range.beginRowIndex - 1) : ( !len ? (cellInfo.rowIndex + cellInfo.rowSpan) : range.endRowIndex + 1); nextColIndex = !len ? cellInfo.colIndex : range.beginColIndex; return this.getCell(this.indexTable[nextRowIndex][nextColIndex].rowIndex, this.indexTable[nextRowIndex][nextColIndex].cellIndex); } catch (e) { showError(e); } }, getPreviewCell:function (cell, top) { try { var cellInfo = this.getCellInfo(cell), previewRowIndex, previewColIndex; var len = this.selectedTds.length, range = this.cellsRange; //首行或者首列没有前置单元格 if ((!top && (!len ? !cellInfo.colIndex : !range.beginColIndex)) || (top && (!len ? (cellInfo.rowIndex > (this.colsNum - 1)) : (range.endColIndex == this.colsNum - 1)))) return null; previewRowIndex = !top ? ( !len ? cellInfo.rowIndex : range.beginRowIndex ) : ( !len ? (cellInfo.rowIndex < 1 ? 0 : (cellInfo.rowIndex - 1)) : range.beginRowIndex); previewColIndex = !top ? ( !len ? (cellInfo.colIndex < 1 ? 0 : (cellInfo.colIndex - 1)) : range.beginColIndex - 1) : ( !len ? cellInfo.colIndex : range.endColIndex + 1); return this.getCell(this.indexTable[previewRowIndex][previewColIndex].rowIndex, this.indexTable[previewRowIndex][previewColIndex].cellIndex); } catch (e) { showError(e); } }, /** * 移动单元格中的内容 */ moveContent:function (cellTo, cellFrom) { if (UETable.isEmptyBlock(cellFrom)) return; if (UETable.isEmptyBlock(cellTo)) { cellTo.innerHTML = cellFrom.innerHTML; return; } var child = cellTo.lastChild; if (child.nodeType == 3 || !dtd.$block[child.tagName]) { cellTo.appendChild(cellTo.ownerDocument.createElement('br')) } while (child = cellFrom.firstChild) { cellTo.appendChild(child); } }, /** * 向右合并单元格 */ mergeRight:function (cell) { var cellInfo = this.getCellInfo(cell), rightColIndex = cellInfo.colIndex + cellInfo.colSpan, rightCellInfo = this.indexTable[cellInfo.rowIndex][rightColIndex], rightCell = this.getCell(rightCellInfo.rowIndex, rightCellInfo.cellIndex); //合并 cell.colSpan = cellInfo.colSpan + rightCellInfo.colSpan; //被合并的单元格不应存在宽度属性 cell.removeAttribute("width"); //移动内容 this.moveContent(cell, rightCell); //删掉被合并的Cell this.deleteCell(rightCell, rightCellInfo.rowIndex); this.update(); }, /** * 向下合并单元格 */ mergeDown:function (cell) { var cellInfo = this.getCellInfo(cell), downRowIndex = cellInfo.rowIndex + cellInfo.rowSpan, downCellInfo = this.indexTable[downRowIndex][cellInfo.colIndex], downCell = this.getCell(downCellInfo.rowIndex, downCellInfo.cellIndex); cell.rowSpan = cellInfo.rowSpan + downCellInfo.rowSpan; cell.removeAttribute("height"); this.moveContent(cell, downCell); this.deleteCell(downCell, downCellInfo.rowIndex); this.update(); }, /** * 合并整个range中的内容 */ mergeRange:function () { //由于合并操作可以在任意时刻进行,所以无法通过鼠标位置等信息实时生成range,只能通过缓存实例中的cellsRange对象来访问 var range = this.cellsRange, leftTopCell = this.getCell(range.beginRowIndex, this.indexTable[range.beginRowIndex][range.beginColIndex].cellIndex); if (leftTopCell.tagName == "TH" && range.endRowIndex !== range.beginRowIndex) { var index = this.indexTable, info = this.getCellInfo(leftTopCell); leftTopCell = this.getCell(1, index[1][info.colIndex].cellIndex); range = this.getCellsRange(leftTopCell, this.getCell(index[this.rowsNum - 1][info.colIndex].rowIndex, index[this.rowsNum - 1][info.colIndex].cellIndex)); } // 删除剩余的Cells var cells = this.getCells(range); for(var i= 0,ci;ci=cells[i++];){ if (ci !== leftTopCell) { this.moveContent(leftTopCell, ci); this.deleteCell(ci); } } // 修改左上角Cell的rowSpan和colSpan,并调整宽度属性设置 leftTopCell.rowSpan = range.endRowIndex - range.beginRowIndex + 1; leftTopCell.rowSpan > 1 && leftTopCell.removeAttribute("height"); leftTopCell.colSpan = range.endColIndex - range.beginColIndex + 1; leftTopCell.colSpan > 1 && leftTopCell.removeAttribute("width"); if (leftTopCell.rowSpan == this.rowsNum && leftTopCell.colSpan != 1) { leftTopCell.colSpan = 1; } if (leftTopCell.colSpan == this.colsNum && leftTopCell.rowSpan != 1) { var rowIndex = leftTopCell.parentNode.rowIndex; //解决IE下的表格操作问题 if( this.table.deleteRow ) { for (var i = rowIndex+ 1, curIndex=rowIndex+ 1, len=leftTopCell.rowSpan; i < len; i++) { this.table.deleteRow(curIndex); } } else { for (var i = 0, len=leftTopCell.rowSpan - 1; i < len; i++) { var row = this.table.rows[rowIndex + 1]; row.parentNode.removeChild(row); } } leftTopCell.rowSpan = 1; } this.update(); }, /** * 插入一行单元格 */ insertRow:function (rowIndex, sourceCell) { var numCols = this.colsNum, table = this.table, row = table.insertRow(rowIndex), cell, isInsertTitle = typeof sourceCell == 'string' && sourceCell.toUpperCase() == 'TH'; function replaceTdToTh(colIndex, cell, tableRow) { if (colIndex == 0) { var tr = tableRow.nextSibling || tableRow.previousSibling, th = tr.cells[colIndex]; if (th.tagName == 'TH') { th = cell.ownerDocument.createElement("th"); th.appendChild(cell.firstChild); tableRow.insertBefore(th, cell); domUtils.remove(cell) } }else{ if (cell.tagName == 'TH') { var td = cell.ownerDocument.createElement("td"); td.appendChild(cell.firstChild); tableRow.insertBefore(td, cell); domUtils.remove(cell) } } } //首行直接插入,无需考虑部分单元格被rowspan的情况 if (rowIndex == 0 || rowIndex == this.rowsNum) { for (var colIndex = 0; colIndex < numCols; colIndex++) { cell = this.cloneCell(sourceCell, true); this.setCellContent(cell); cell.getAttribute('vAlign') && cell.setAttribute('vAlign', cell.getAttribute('vAlign')); row.appendChild(cell); if(!isInsertTitle) replaceTdToTh(colIndex, cell, row); } } else { var infoRow = this.indexTable[rowIndex], cellIndex = 0; for (colIndex = 0; colIndex < numCols; colIndex++) { var cellInfo = infoRow[colIndex]; //如果存在某个单元格的rowspan穿过待插入行的位置,则修改该单元格的rowspan即可,无需插入单元格 if (cellInfo.rowIndex < rowIndex) { cell = this.getCell(cellInfo.rowIndex, cellInfo.cellIndex); cell.rowSpan = cellInfo.rowSpan + 1; } else { cell = this.cloneCell(sourceCell, true); this.setCellContent(cell); row.appendChild(cell); } if(!isInsertTitle) replaceTdToTh(colIndex, cell, row); } } //框选时插入不触发contentchange,需要手动更新索引。 this.update(); return row; }, /** * 删除一行单元格 * @param rowIndex */ deleteRow:function (rowIndex) { var row = this.table.rows[rowIndex], infoRow = this.indexTable[rowIndex], colsNum = this.colsNum, count = 0; //处理计数 for (var colIndex = 0; colIndex < colsNum;) { var cellInfo = infoRow[colIndex], cell = this.getCell(cellInfo.rowIndex, cellInfo.cellIndex); if (cell.rowSpan > 1) { if (cellInfo.rowIndex == rowIndex) { var clone = cell.cloneNode(true); clone.rowSpan = cell.rowSpan - 1; clone.innerHTML = ""; cell.rowSpan = 1; var nextRowIndex = rowIndex + 1, nextRow = this.table.rows[nextRowIndex], insertCellIndex, preMerged = this.getPreviewMergedCellsNum(nextRowIndex, colIndex) - count; if (preMerged < colIndex) { insertCellIndex = colIndex - preMerged - 1; //nextRow.insertCell(insertCellIndex); domUtils.insertAfter(nextRow.cells[insertCellIndex], clone); } else { if (nextRow.cells.length) nextRow.insertBefore(clone, nextRow.cells[0]) } count += 1; //cell.parentNode.removeChild(cell); } } colIndex += cell.colSpan || 1; } var deleteTds = [], cacheMap = {}; for (colIndex = 0; colIndex < colsNum; colIndex++) { var tmpRowIndex = infoRow[colIndex].rowIndex, tmpCellIndex = infoRow[colIndex].cellIndex, key = tmpRowIndex + "_" + tmpCellIndex; if (cacheMap[key])continue; cacheMap[key] = 1; cell = this.getCell(tmpRowIndex, tmpCellIndex); deleteTds.push(cell); } var mergeTds = []; utils.each(deleteTds, function (td) { if (td.rowSpan == 1) { td.parentNode.removeChild(td); } else { mergeTds.push(td); } }); utils.each(mergeTds, function (td) { td.rowSpan--; }); row.parentNode.removeChild(row); //浏览器方法本身存在bug,采用自定义方法删除 //this.table.deleteRow(rowIndex); this.update(); }, insertCol:function (colIndex, sourceCell, defaultValue) { var rowsNum = this.rowsNum, rowIndex = 0, tableRow, cell, backWidth = parseInt((this.table.offsetWidth - (this.colsNum + 1) * 20 - (this.colsNum + 1)) / (this.colsNum + 1), 10), isInsertTitleCol = typeof sourceCell == 'string' && sourceCell.toUpperCase() == 'TH'; function replaceTdToTh(rowIndex, cell, tableRow) { if (rowIndex == 0) { var th = cell.nextSibling || cell.previousSibling; if (th.tagName == 'TH') { th = cell.ownerDocument.createElement("th"); th.appendChild(cell.firstChild); tableRow.insertBefore(th, cell); domUtils.remove(cell) } }else{ if (cell.tagName == 'TH') { var td = cell.ownerDocument.createElement("td"); td.appendChild(cell.firstChild); tableRow.insertBefore(td, cell); domUtils.remove(cell) } } } var preCell; if (colIndex == 0 || colIndex == this.colsNum) { for (; rowIndex < rowsNum; rowIndex++) { tableRow = this.table.rows[rowIndex]; preCell = tableRow.cells[colIndex == 0 ? colIndex : tableRow.cells.length]; cell = this.cloneCell(sourceCell, true); //tableRow.insertCell(colIndex == 0 ? colIndex : tableRow.cells.length); this.setCellContent(cell); cell.setAttribute('vAlign', cell.getAttribute('vAlign')); preCell && cell.setAttribute('width', preCell.getAttribute('width')); if (!colIndex) { tableRow.insertBefore(cell, tableRow.cells[0]); } else { domUtils.insertAfter(tableRow.cells[tableRow.cells.length - 1], cell); } if(!isInsertTitleCol) replaceTdToTh(rowIndex, cell, tableRow) } } else { for (; rowIndex < rowsNum; rowIndex++) { var cellInfo = this.indexTable[rowIndex][colIndex]; if (cellInfo.colIndex < colIndex) { cell = this.getCell(cellInfo.rowIndex, cellInfo.cellIndex); cell.colSpan = cellInfo.colSpan + 1; } else { tableRow = this.table.rows[rowIndex]; preCell = tableRow.cells[cellInfo.cellIndex]; cell = this.cloneCell(sourceCell, true);//tableRow.insertCell(cellInfo.cellIndex); this.setCellContent(cell); cell.setAttribute('vAlign', cell.getAttribute('vAlign')); preCell && cell.setAttribute('width', preCell.getAttribute('width')); //防止IE下报错 preCell ? tableRow.insertBefore(cell, preCell) : tableRow.appendChild(cell); } if(!isInsertTitleCol) replaceTdToTh(rowIndex, cell, tableRow); } } //框选时插入不触发contentchange,需要手动更新索引 this.update(); this.updateWidth(backWidth, defaultValue || {tdPadding:10, tdBorder:1}); }, updateWidth:function (width, defaultValue) { var table = this.table, tmpWidth = UETable.getWidth(table) - defaultValue.tdPadding * 2 - defaultValue.tdBorder + width; if (tmpWidth < table.ownerDocument.body.offsetWidth) { table.setAttribute("width", tmpWidth); return; } var tds = domUtils.getElementsByTagName(this.table, "td th"); utils.each(tds, function (td) { td.setAttribute("width", width); }) }, deleteCol:function (colIndex) { var indexTable = this.indexTable, tableRows = this.table.rows, backTableWidth = this.table.getAttribute("width"), backTdWidth = 0, rowsNum = this.rowsNum, cacheMap = {}; for (var rowIndex = 0; rowIndex < rowsNum;) { var infoRow = indexTable[rowIndex], cellInfo = infoRow[colIndex], key = cellInfo.rowIndex + '_' + cellInfo.colIndex; // 跳过已经处理过的Cell if (cacheMap[key])continue; cacheMap[key] = 1; var cell = this.getCell(cellInfo.rowIndex, cellInfo.cellIndex); if (!backTdWidth) backTdWidth = cell && parseInt(cell.offsetWidth / cell.colSpan, 10).toFixed(0); // 如果Cell的colSpan大于1, 就修改colSpan, 否则就删掉这个Cell if (cell.colSpan > 1) { cell.colSpan--; } else { tableRows[rowIndex].deleteCell(cellInfo.cellIndex); } rowIndex += cellInfo.rowSpan || 1; } this.table.setAttribute("width", backTableWidth - backTdWidth); this.update(); }, splitToCells:function (cell) { var me = this, cells = this.splitToRows(cell); utils.each(cells, function (cell) { me.splitToCols(cell); }) }, splitToRows:function (cell) { var cellInfo = this.getCellInfo(cell), rowIndex = cellInfo.rowIndex, colIndex = cellInfo.colIndex, results = []; // 修改Cell的rowSpan cell.rowSpan = 1; results.push(cell); // 补齐单元格 for (var i = rowIndex, endRow = rowIndex + cellInfo.rowSpan; i < endRow; i++) { if (i == rowIndex)continue; var tableRow = this.table.rows[i], tmpCell = tableRow.insertCell(colIndex - this.getPreviewMergedCellsNum(i, colIndex)); tmpCell.colSpan = cellInfo.colSpan; this.setCellContent(tmpCell); tmpCell.setAttribute('vAlign', cell.getAttribute('vAlign')); tmpCell.setAttribute('align', cell.getAttribute('align')); if (cell.style.cssText) { tmpCell.style.cssText = cell.style.cssText; } results.push(tmpCell); } this.update(); return results; }, getPreviewMergedCellsNum:function (rowIndex, colIndex) { var indexRow = this.indexTable[rowIndex], num = 0; for (var i = 0; i < colIndex;) { var colSpan = indexRow[i].colSpan, tmpRowIndex = indexRow[i].rowIndex; num += (colSpan - (tmpRowIndex == rowIndex ? 1 : 0)); i += colSpan; } return num; }, splitToCols:function (cell) { var backWidth = (cell.offsetWidth / cell.colSpan - 22).toFixed(0), cellInfo = this.getCellInfo(cell), rowIndex = cellInfo.rowIndex, colIndex = cellInfo.colIndex, results = []; // 修改Cell的rowSpan cell.colSpan = 1; cell.setAttribute("width", backWidth); results.push(cell); // 补齐单元格 for (var j = colIndex, endCol = colIndex + cellInfo.colSpan; j < endCol; j++) { if (j == colIndex)continue; var tableRow = this.table.rows[rowIndex], tmpCell = tableRow.insertCell(this.indexTable[rowIndex][j].cellIndex + 1); tmpCell.rowSpan = cellInfo.rowSpan; this.setCellContent(tmpCell); tmpCell.setAttribute('vAlign', cell.getAttribute('vAlign')); tmpCell.setAttribute('align', cell.getAttribute('align')); tmpCell.setAttribute('width', backWidth); if (cell.style.cssText) { tmpCell.style.cssText = cell.style.cssText; } //处理th的情况 if (cell.tagName == 'TH') { var th = cell.ownerDocument.createElement('th'); th.appendChild(tmpCell.firstChild); th.setAttribute('vAlign', cell.getAttribute('vAlign')); th.rowSpan = tmpCell.rowSpan; tableRow.insertBefore(th, tmpCell); domUtils.remove(tmpCell); } results.push(tmpCell); } this.update(); return results; }, isLastCell:function (cell, rowsNum, colsNum) { rowsNum = rowsNum || this.rowsNum; colsNum = colsNum || this.colsNum; var cellInfo = this.getCellInfo(cell); return ((cellInfo.rowIndex + cellInfo.rowSpan) == rowsNum) && ((cellInfo.colIndex + cellInfo.colSpan) == colsNum); }, getLastCell:function (cells) { cells = cells || this.table.getElementsByTagName("td"); var firstInfo = this.getCellInfo(cells[0]); var me = this, last = cells[0], tr = last.parentNode, cellsNum = 0, cols = 0, rows; utils.each(cells, function (cell) { if (cell.parentNode == tr)cols += cell.colSpan || 1; cellsNum += cell.rowSpan * cell.colSpan || 1; }); rows = cellsNum / cols; utils.each(cells, function (cell) { if (me.isLastCell(cell, rows, cols)) { last = cell; return false; } }); return last; }, selectRow:function (rowIndex) { var indexRow = this.indexTable[rowIndex], start = this.getCell(indexRow[0].rowIndex, indexRow[0].cellIndex), end = this.getCell(indexRow[this.colsNum - 1].rowIndex, indexRow[this.colsNum - 1].cellIndex), range = this.getCellsRange(start, end); this.setSelected(range); }, selectTable:function () { var tds = this.table.getElementsByTagName("td"), range = this.getCellsRange(tds[0], tds[tds.length - 1]); this.setSelected(range); }, setBackground:function (cells, value) { if (typeof value === "string") { utils.each(cells, function (cell) { cell.style.backgroundColor = value; }) } else if (typeof value === "object") { value = utils.extend({ repeat:true, colorList:["#ddd", "#fff"] }, value); var rowIndex = this.getCellInfo(cells[0]).rowIndex, count = 0, colors = value.colorList, getColor = function (list, index, repeat) { return list[index] ? list[index] : repeat ? list[index % list.length] : ""; }; for (var i = 0, cell; cell = cells[i++];) { var cellInfo = this.getCellInfo(cell); cell.style.backgroundColor = getColor(colors, ((rowIndex + count) == cellInfo.rowIndex) ? count : ++count, value.repeat); } } }, removeBackground:function (cells) { utils.each(cells, function (cell) { cell.style.backgroundColor = ""; }) } }; function showError(e) { } })(); // plugins/table.cmds.js /** * Created with JetBrains PhpStorm. * User: taoqili * Date: 13-2-20 * Time: 下午6:25 * To change this template use File | Settings | File Templates. */ ; (function () { var UT = UE.UETable, getTableItemsByRange = function (editor) { return UT.getTableItemsByRange(editor); }, getUETableBySelected = function (editor) { return UT.getUETableBySelected(editor) }, getDefaultValue = function (editor, table) { return UT.getDefaultValue(editor, table); }, getUETable = function (tdOrTable) { return UT.getUETable(tdOrTable); }; UE.commands['inserttable'] = { queryCommandState: function () { return getTableItemsByRange(this).table ? -1 : 0; }, execCommand: function (cmd, opt) { function createTable(opt, tdWidth) { var html = [], rowsNum = opt.numRows, colsNum = opt.numCols; for (var r = 0; r < rowsNum; r++) { html.push(''); for (var c = 0; c < colsNum; c++) { html.push('
  • ' + (browser.ie && browser.version < 11 ? domUtils.fillChar : '
    ') + '
    ' + html.join('') + '
    ' } if (!opt) { opt = utils.extend({}, { numCols: this.options.defaultCols, numRows: this.options.defaultRows, tdvalign: this.options.tdvalign }) } var me = this; var range = this.selection.getRange(), start = range.startContainer, firstParentBlock = domUtils.findParent(start, function (node) { return domUtils.isBlockElm(node); }, true) || me.body; var defaultValue = getDefaultValue(me), tableWidth = firstParentBlock.offsetWidth, tdWidth = Math.floor(tableWidth / opt.numCols - defaultValue.tdPadding * 2 - defaultValue.tdBorder); //todo其他属性 !opt.tdvalign && (opt.tdvalign = me.options.tdvalign); me.execCommand("inserthtml", createTable(opt, tdWidth)); } }; UE.commands['insertparagraphbeforetable'] = { queryCommandState: function () { return getTableItemsByRange(this).cell ? 0 : -1; }, execCommand: function () { var table = getTableItemsByRange(this).table; if (table) { var p = this.document.createElement("p"); p.innerHTML = browser.ie ? ' ' : '
    '; table.parentNode.insertBefore(p, table); this.selection.getRange().setStart(p, 0).setCursor(); } } }; UE.commands['deletetable'] = { queryCommandState: function () { var rng = this.selection.getRange(); return domUtils.findParentByTagName(rng.startContainer, 'table', true) ? 0 : -1; }, execCommand: function (cmd, table) { var rng = this.selection.getRange(); table = table || domUtils.findParentByTagName(rng.startContainer, 'table', true); if (table) { var next = table.nextSibling; if (!next) { next = domUtils.createElement(this.document, 'p', { 'innerHTML': browser.ie ? domUtils.fillChar : '
    ' }); table.parentNode.insertBefore(next, table); } domUtils.remove(table); rng = this.selection.getRange(); if (next.nodeType == 3) { rng.setStartBefore(next) } else { rng.setStart(next, 0) } rng.setCursor(false, true) this.fireEvent("tablehasdeleted") } } }; UE.commands['cellalign'] = { queryCommandState: function () { return getSelectedArr(this).length ? 0 : -1 }, execCommand: function (cmd, align) { var selectedTds = getSelectedArr(this); if (selectedTds.length) { for (var i = 0, ci; ci = selectedTds[i++];) { ci.setAttribute('align', align); } } } }; UE.commands['cellvalign'] = { queryCommandState: function () { return getSelectedArr(this).length ? 0 : -1; }, execCommand: function (cmd, valign) { var selectedTds = getSelectedArr(this); if (selectedTds.length) { for (var i = 0, ci; ci = selectedTds[i++];) { ci.setAttribute('vAlign', valign); } } } }; UE.commands['insertcaption'] = { queryCommandState: function () { var table = getTableItemsByRange(this).table; if (table) { return table.getElementsByTagName('caption').length == 0 ? 1 : -1; } return -1; }, execCommand: function () { var table = getTableItemsByRange(this).table; if (table) { var caption = this.document.createElement('caption'); caption.innerHTML = browser.ie ? domUtils.fillChar : '
    '; table.insertBefore(caption, table.firstChild); var range = this.selection.getRange(); range.setStart(caption, 0).setCursor(); } } }; UE.commands['deletecaption'] = { queryCommandState: function () { var rng = this.selection.getRange(), table = domUtils.findParentByTagName(rng.startContainer, 'table'); if (table) { return table.getElementsByTagName('caption').length == 0 ? -1 : 1; } return -1; }, execCommand: function () { var rng = this.selection.getRange(), table = domUtils.findParentByTagName(rng.startContainer, 'table'); if (table) { domUtils.remove(table.getElementsByTagName('caption')[0]); var range = this.selection.getRange(); range.setStart(table.rows[0].cells[0], 0).setCursor(); } } }; UE.commands['inserttitle'] = { queryCommandState: function () { var table = getTableItemsByRange(this).table; if (table) { var firstRow = table.rows[0]; return firstRow.cells[firstRow.cells.length-1].tagName.toLowerCase() != 'th' ? 0 : -1 } return -1; }, execCommand: function () { var table = getTableItemsByRange(this).table; if (table) { getUETable(table).insertRow(0, 'th'); } var th = table.getElementsByTagName('th')[0]; this.selection.getRange().setStart(th, 0).setCursor(false, true); } }; UE.commands['deletetitle'] = { queryCommandState: function () { var table = getTableItemsByRange(this).table; if (table) { var firstRow = table.rows[0]; return firstRow.cells[firstRow.cells.length-1].tagName.toLowerCase() == 'th' ? 0 : -1 } return -1; }, execCommand: function () { var table = getTableItemsByRange(this).table; if (table) { domUtils.remove(table.rows[0]) } var td = table.getElementsByTagName('td')[0]; this.selection.getRange().setStart(td, 0).setCursor(false, true); } }; UE.commands['inserttitlecol'] = { queryCommandState: function () { var table = getTableItemsByRange(this).table; if (table) { var lastRow = table.rows[table.rows.length-1]; return lastRow.getElementsByTagName('th').length ? -1 : 0; } return -1; }, execCommand: function (cmd) { var table = getTableItemsByRange(this).table; if (table) { getUETable(table).insertCol(0, 'th'); } resetTdWidth(table, this); var th = table.getElementsByTagName('th')[0]; this.selection.getRange().setStart(th, 0).setCursor(false, true); } }; UE.commands['deletetitlecol'] = { queryCommandState: function () { var table = getTableItemsByRange(this).table; if (table) { var lastRow = table.rows[table.rows.length-1]; return lastRow.getElementsByTagName('th').length ? 0 : -1; } return -1; }, execCommand: function () { var table = getTableItemsByRange(this).table; if (table) { for(var i = 0; i< table.rows.length; i++ ){ domUtils.remove(table.rows[i].children[0]) } } resetTdWidth(table, this); var td = table.getElementsByTagName('td')[0]; this.selection.getRange().setStart(td, 0).setCursor(false, true); } }; UE.commands["mergeright"] = { queryCommandState: function (cmd) { var tableItems = getTableItemsByRange(this), table = tableItems.table, cell = tableItems.cell; if (!table || !cell) return -1; var ut = getUETable(table); if (ut.selectedTds.length) return -1; var cellInfo = ut.getCellInfo(cell), rightColIndex = cellInfo.colIndex + cellInfo.colSpan; if (rightColIndex >= ut.colsNum) return -1; // 如果处于最右边则不能向右合并 var rightCellInfo = ut.indexTable[cellInfo.rowIndex][rightColIndex], rightCell = table.rows[rightCellInfo.rowIndex].cells[rightCellInfo.cellIndex]; if (!rightCell || cell.tagName != rightCell.tagName) return -1; // TH和TD不能相互合并 // 当且仅当两个Cell的开始列号和结束列号一致时能进行合并 return (rightCellInfo.rowIndex == cellInfo.rowIndex && rightCellInfo.rowSpan == cellInfo.rowSpan) ? 0 : -1; }, execCommand: function (cmd) { var rng = this.selection.getRange(), bk = rng.createBookmark(true); var cell = getTableItemsByRange(this).cell, ut = getUETable(cell); ut.mergeRight(cell); rng.moveToBookmark(bk).select(); } }; UE.commands["mergedown"] = { queryCommandState: function (cmd) { var tableItems = getTableItemsByRange(this), table = tableItems.table, cell = tableItems.cell; if (!table || !cell) return -1; var ut = getUETable(table); if (ut.selectedTds.length)return -1; var cellInfo = ut.getCellInfo(cell), downRowIndex = cellInfo.rowIndex + cellInfo.rowSpan; if (downRowIndex >= ut.rowsNum) return -1; // 如果处于最下边则不能向下合并 var downCellInfo = ut.indexTable[downRowIndex][cellInfo.colIndex], downCell = table.rows[downCellInfo.rowIndex].cells[downCellInfo.cellIndex]; if (!downCell || cell.tagName != downCell.tagName) return -1; // TH和TD不能相互合并 // 当且仅当两个Cell的开始列号和结束列号一致时能进行合并 return (downCellInfo.colIndex == cellInfo.colIndex && downCellInfo.colSpan == cellInfo.colSpan) ? 0 : -1; }, execCommand: function () { var rng = this.selection.getRange(), bk = rng.createBookmark(true); var cell = getTableItemsByRange(this).cell, ut = getUETable(cell); ut.mergeDown(cell); rng.moveToBookmark(bk).select(); } }; UE.commands["mergecells"] = { queryCommandState: function () { return getUETableBySelected(this) ? 0 : -1; }, execCommand: function () { var ut = getUETableBySelected(this); if (ut && ut.selectedTds.length) { var cell = ut.selectedTds[0]; ut.mergeRange(); var rng = this.selection.getRange(); if (domUtils.isEmptyBlock(cell)) { rng.setStart(cell, 0).collapse(true) } else { rng.selectNodeContents(cell) } rng.select(); } } }; UE.commands["insertrow"] = { queryCommandState: function () { var tableItems = getTableItemsByRange(this), cell = tableItems.cell; return cell && (cell.tagName == "TD" || (cell.tagName == 'TH' && tableItems.tr !== tableItems.table.rows[0])) && getUETable(tableItems.table).rowsNum < this.options.maxRowNum ? 0 : -1; }, execCommand: function () { var rng = this.selection.getRange(), bk = rng.createBookmark(true); var tableItems = getTableItemsByRange(this), cell = tableItems.cell, table = tableItems.table, ut = getUETable(table), cellInfo = ut.getCellInfo(cell); //ut.insertRow(!ut.selectedTds.length ? cellInfo.rowIndex:ut.cellsRange.beginRowIndex,''); if (!ut.selectedTds.length) { ut.insertRow(cellInfo.rowIndex, cell); } else { var range = ut.cellsRange; for (var i = 0, len = range.endRowIndex - range.beginRowIndex + 1; i < len; i++) { ut.insertRow(range.beginRowIndex, cell); } } rng.moveToBookmark(bk).select(); if (table.getAttribute("interlaced") === "enabled")this.fireEvent("interlacetable", table); } }; //后插入行 UE.commands["insertrownext"] = { queryCommandState: function () { var tableItems = getTableItemsByRange(this), cell = tableItems.cell; return cell && (cell.tagName == "TD") && getUETable(tableItems.table).rowsNum < this.options.maxRowNum ? 0 : -1; }, execCommand: function () { var rng = this.selection.getRange(), bk = rng.createBookmark(true); var tableItems = getTableItemsByRange(this), cell = tableItems.cell, table = tableItems.table, ut = getUETable(table), cellInfo = ut.getCellInfo(cell); //ut.insertRow(!ut.selectedTds.length? cellInfo.rowIndex + cellInfo.rowSpan : ut.cellsRange.endRowIndex + 1,''); if (!ut.selectedTds.length) { ut.insertRow(cellInfo.rowIndex + cellInfo.rowSpan, cell); } else { var range = ut.cellsRange; for (var i = 0, len = range.endRowIndex - range.beginRowIndex + 1; i < len; i++) { ut.insertRow(range.endRowIndex + 1, cell); } } rng.moveToBookmark(bk).select(); if (table.getAttribute("interlaced") === "enabled")this.fireEvent("interlacetable", table); } }; UE.commands["deleterow"] = { queryCommandState: function () { var tableItems = getTableItemsByRange(this); return tableItems.cell ? 0 : -1; }, execCommand: function () { var cell = getTableItemsByRange(this).cell, ut = getUETable(cell), cellsRange = ut.cellsRange, cellInfo = ut.getCellInfo(cell), preCell = ut.getVSideCell(cell), nextCell = ut.getVSideCell(cell, true), rng = this.selection.getRange(); if (utils.isEmptyObject(cellsRange)) { ut.deleteRow(cellInfo.rowIndex); } else { for (var i = cellsRange.beginRowIndex; i < cellsRange.endRowIndex + 1; i++) { ut.deleteRow(cellsRange.beginRowIndex); } } var table = ut.table; if (!table.getElementsByTagName('td').length) { var nextSibling = table.nextSibling; domUtils.remove(table); if (nextSibling) { rng.setStart(nextSibling, 0).setCursor(false, true); } } else { if (cellInfo.rowSpan == 1 || cellInfo.rowSpan == cellsRange.endRowIndex - cellsRange.beginRowIndex + 1) { if (nextCell || preCell) rng.selectNodeContents(nextCell || preCell).setCursor(false, true); } else { var newCell = ut.getCell(cellInfo.rowIndex, ut.indexTable[cellInfo.rowIndex][cellInfo.colIndex].cellIndex); if (newCell) rng.selectNodeContents(newCell).setCursor(false, true); } } if (table.getAttribute("interlaced") === "enabled")this.fireEvent("interlacetable", table); } }; UE.commands["insertcol"] = { queryCommandState: function (cmd) { var tableItems = getTableItemsByRange(this), cell = tableItems.cell; return cell && (cell.tagName == "TD" || (cell.tagName == 'TH' && cell !== tableItems.tr.cells[0])) && getUETable(tableItems.table).colsNum < this.options.maxColNum ? 0 : -1; }, execCommand: function (cmd) { var rng = this.selection.getRange(), bk = rng.createBookmark(true); if (this.queryCommandState(cmd) == -1)return; var cell = getTableItemsByRange(this).cell, ut = getUETable(cell), cellInfo = ut.getCellInfo(cell); //ut.insertCol(!ut.selectedTds.length ? cellInfo.colIndex:ut.cellsRange.beginColIndex); if (!ut.selectedTds.length) { ut.insertCol(cellInfo.colIndex, cell); } else { var range = ut.cellsRange; for (var i = 0, len = range.endColIndex - range.beginColIndex + 1; i < len; i++) { ut.insertCol(range.beginColIndex, cell); } } rng.moveToBookmark(bk).select(true); } }; UE.commands["insertcolnext"] = { queryCommandState: function () { var tableItems = getTableItemsByRange(this), cell = tableItems.cell; return cell && getUETable(tableItems.table).colsNum < this.options.maxColNum ? 0 : -1; }, execCommand: function () { var rng = this.selection.getRange(), bk = rng.createBookmark(true); var cell = getTableItemsByRange(this).cell, ut = getUETable(cell), cellInfo = ut.getCellInfo(cell); //ut.insertCol(!ut.selectedTds.length ? cellInfo.colIndex + cellInfo.colSpan:ut.cellsRange.endColIndex +1); if (!ut.selectedTds.length) { ut.insertCol(cellInfo.colIndex + cellInfo.colSpan, cell); } else { var range = ut.cellsRange; for (var i = 0, len = range.endColIndex - range.beginColIndex + 1; i < len; i++) { ut.insertCol(range.endColIndex + 1, cell); } } rng.moveToBookmark(bk).select(); } }; UE.commands["deletecol"] = { queryCommandState: function () { var tableItems = getTableItemsByRange(this); return tableItems.cell ? 0 : -1; }, execCommand: function () { var cell = getTableItemsByRange(this).cell, ut = getUETable(cell), range = ut.cellsRange, cellInfo = ut.getCellInfo(cell), preCell = ut.getHSideCell(cell), nextCell = ut.getHSideCell(cell, true); if (utils.isEmptyObject(range)) { ut.deleteCol(cellInfo.colIndex); } else { for (var i = range.beginColIndex; i < range.endColIndex + 1; i++) { ut.deleteCol(range.beginColIndex); } } var table = ut.table, rng = this.selection.getRange(); if (!table.getElementsByTagName('td').length) { var nextSibling = table.nextSibling; domUtils.remove(table); if (nextSibling) { rng.setStart(nextSibling, 0).setCursor(false, true); } } else { if (domUtils.inDoc(cell, this.document)) { rng.setStart(cell, 0).setCursor(false, true); } else { if (nextCell && domUtils.inDoc(nextCell, this.document)) { rng.selectNodeContents(nextCell).setCursor(false, true); } else { if (preCell && domUtils.inDoc(preCell, this.document)) { rng.selectNodeContents(preCell).setCursor(true, true); } } } } } }; UE.commands["splittocells"] = { queryCommandState: function () { var tableItems = getTableItemsByRange(this), cell = tableItems.cell; if (!cell) return -1; var ut = getUETable(tableItems.table); if (ut.selectedTds.length > 0) return -1; return cell && (cell.colSpan > 1 || cell.rowSpan > 1) ? 0 : -1; }, execCommand: function () { var rng = this.selection.getRange(), bk = rng.createBookmark(true); var cell = getTableItemsByRange(this).cell, ut = getUETable(cell); ut.splitToCells(cell); rng.moveToBookmark(bk).select(); } }; UE.commands["splittorows"] = { queryCommandState: function () { var tableItems = getTableItemsByRange(this), cell = tableItems.cell; if (!cell) return -1; var ut = getUETable(tableItems.table); if (ut.selectedTds.length > 0) return -1; return cell && cell.rowSpan > 1 ? 0 : -1; }, execCommand: function () { var rng = this.selection.getRange(), bk = rng.createBookmark(true); var cell = getTableItemsByRange(this).cell, ut = getUETable(cell); ut.splitToRows(cell); rng.moveToBookmark(bk).select(); } }; UE.commands["splittocols"] = { queryCommandState: function () { var tableItems = getTableItemsByRange(this), cell = tableItems.cell; if (!cell) return -1; var ut = getUETable(tableItems.table); if (ut.selectedTds.length > 0) return -1; return cell && cell.colSpan > 1 ? 0 : -1; }, execCommand: function () { var rng = this.selection.getRange(), bk = rng.createBookmark(true); var cell = getTableItemsByRange(this).cell, ut = getUETable(cell); ut.splitToCols(cell); rng.moveToBookmark(bk).select(); } }; UE.commands["adaptbytext"] = UE.commands["adaptbywindow"] = { queryCommandState: function () { return getTableItemsByRange(this).table ? 0 : -1 }, execCommand: function (cmd) { var tableItems = getTableItemsByRange(this), table = tableItems.table; if (table) { if (cmd == 'adaptbywindow') { resetTdWidth(table, this); } else { var cells = domUtils.getElementsByTagName(table, "td th"); utils.each(cells, function (cell) { cell.removeAttribute("width"); }); table.removeAttribute("width"); } } } }; //平均分配各列 UE.commands['averagedistributecol'] = { queryCommandState: function () { var ut = getUETableBySelected(this); if (!ut) return -1; return ut.isFullRow() || ut.isFullCol() ? 0 : -1; }, execCommand: function (cmd) { var me = this, ut = getUETableBySelected(me); function getAverageWidth() { var tb = ut.table, averageWidth, sumWidth = 0, colsNum = 0, tbAttr = getDefaultValue(me, tb); if (ut.isFullRow()) { sumWidth = tb.offsetWidth; colsNum = ut.colsNum; } else { var begin = ut.cellsRange.beginColIndex, end = ut.cellsRange.endColIndex, node; for (var i = begin; i <= end;) { node = ut.selectedTds[i]; sumWidth += node.offsetWidth; i += node.colSpan; colsNum += 1; } } averageWidth = Math.ceil(sumWidth / colsNum) - tbAttr.tdBorder * 2 - tbAttr.tdPadding * 2; return averageWidth; } function setAverageWidth(averageWidth) { utils.each(domUtils.getElementsByTagName(ut.table, "th"), function (node) { node.setAttribute("width", ""); }); var cells = ut.isFullRow() ? domUtils.getElementsByTagName(ut.table, "td") : ut.selectedTds; utils.each(cells, function (node) { if (node.colSpan == 1) { node.setAttribute("width", averageWidth); } }); } if (ut && ut.selectedTds.length) { setAverageWidth(getAverageWidth()); } } }; //平均分配各行 UE.commands['averagedistributerow'] = { queryCommandState: function () { var ut = getUETableBySelected(this); if (!ut) return -1; if (ut.selectedTds && /th/ig.test(ut.selectedTds[0].tagName)) return -1; return ut.isFullRow() || ut.isFullCol() ? 0 : -1; }, execCommand: function (cmd) { var me = this, ut = getUETableBySelected(me); function getAverageHeight() { var averageHeight, rowNum, sumHeight = 0, tb = ut.table, tbAttr = getDefaultValue(me, tb), tdpadding = parseInt(domUtils.getComputedStyle(tb.getElementsByTagName('td')[0], "padding-top")); if (ut.isFullCol()) { var captionArr = domUtils.getElementsByTagName(tb, "caption"), thArr = domUtils.getElementsByTagName(tb, "th"), captionHeight, thHeight; if (captionArr.length > 0) { captionHeight = captionArr[0].offsetHeight; } if (thArr.length > 0) { thHeight = thArr[0].offsetHeight; } sumHeight = tb.offsetHeight - (captionHeight || 0) - (thHeight || 0); rowNum = thArr.length == 0 ? ut.rowsNum : (ut.rowsNum - 1); } else { var begin = ut.cellsRange.beginRowIndex, end = ut.cellsRange.endRowIndex, count = 0, trs = domUtils.getElementsByTagName(tb, "tr"); for (var i = begin; i <= end; i++) { sumHeight += trs[i].offsetHeight; count += 1; } rowNum = count; } //ie8下是混杂模式 if (browser.ie && browser.version < 9) { averageHeight = Math.ceil(sumHeight / rowNum); } else { averageHeight = Math.ceil(sumHeight / rowNum) - tbAttr.tdBorder * 2 - tdpadding * 2; } return averageHeight; } function setAverageHeight(averageHeight) { var cells = ut.isFullCol() ? domUtils.getElementsByTagName(ut.table, "td") : ut.selectedTds; utils.each(cells, function (node) { if (node.rowSpan == 1) { node.setAttribute("height", averageHeight); } }); } if (ut && ut.selectedTds.length) { setAverageHeight(getAverageHeight()); } } }; //单元格对齐方式 UE.commands['cellalignment'] = { queryCommandState: function () { return getTableItemsByRange(this).table ? 0 : -1 }, execCommand: function (cmd, data) { var me = this, ut = getUETableBySelected(me); if (!ut) { var start = me.selection.getStart(), cell = start && domUtils.findParentByTagName(start, ["td", "th", "caption"], true); if (!/caption/ig.test(cell.tagName)) { domUtils.setAttributes(cell, data); } else { cell.style.textAlign = data.align; cell.style.verticalAlign = data.vAlign; } me.selection.getRange().setCursor(true); } else { utils.each(ut.selectedTds, function (cell) { domUtils.setAttributes(cell, data); }); } }, /** * 查询当前点击的单元格的对齐状态, 如果当前已经选择了多个单元格, 则会返回所有单元格经过统一协调过后的状态 * @see UE.UETable.getTableCellAlignState */ queryCommandValue: function (cmd) { var activeMenuCell = getTableItemsByRange( this).cell; if( !activeMenuCell ) { activeMenuCell = getSelectedArr(this)[0]; } if (!activeMenuCell) { return null; } else { //获取同时选中的其他单元格 var cells = UE.UETable.getUETable(activeMenuCell).selectedTds; !cells.length && ( cells = activeMenuCell ); return UE.UETable.getTableCellAlignState(cells); } } }; //表格对齐方式 UE.commands['tablealignment'] = { queryCommandState: function () { if (browser.ie && browser.version < 8) { return -1; } return getTableItemsByRange(this).table ? 0 : -1 }, execCommand: function (cmd, value) { var me = this, start = me.selection.getStart(), table = start && domUtils.findParentByTagName(start, ["table"], true); if (table) { table.setAttribute("align",value); } } }; //表格属性 UE.commands['edittable'] = { queryCommandState: function () { return getTableItemsByRange(this).table ? 0 : -1 }, execCommand: function (cmd, color) { var rng = this.selection.getRange(), table = domUtils.findParentByTagName(rng.startContainer, 'table'); if (table) { var arr = domUtils.getElementsByTagName(table, "td").concat( domUtils.getElementsByTagName(table, "th"), domUtils.getElementsByTagName(table, "caption") ); utils.each(arr, function (node) { node.style.borderColor = color; }); } } }; //单元格属性 UE.commands['edittd'] = { queryCommandState: function () { return getTableItemsByRange(this).table ? 0 : -1 }, execCommand: function (cmd, bkColor) { var me = this, ut = getUETableBySelected(me); if (!ut) { var start = me.selection.getStart(), cell = start && domUtils.findParentByTagName(start, ["td", "th", "caption"], true); if (cell) { cell.style.backgroundColor = bkColor; } } else { utils.each(ut.selectedTds, function (cell) { cell.style.backgroundColor = bkColor; }); } } }; UE.commands["settablebackground"] = { queryCommandState: function () { return getSelectedArr(this).length > 1 ? 0 : -1; }, execCommand: function (cmd, value) { var cells, ut; cells = getSelectedArr(this); ut = getUETable(cells[0]); ut.setBackground(cells, value); } }; UE.commands["cleartablebackground"] = { queryCommandState: function () { var cells = getSelectedArr(this); if (!cells.length)return -1; for (var i = 0, cell; cell = cells[i++];) { if (cell.style.backgroundColor !== "") return 0; } return -1; }, execCommand: function () { var cells = getSelectedArr(this), ut = getUETable(cells[0]); ut.removeBackground(cells); } }; UE.commands["interlacetable"] = UE.commands["uninterlacetable"] = { queryCommandState: function (cmd) { var table = getTableItemsByRange(this).table; if (!table) return -1; var interlaced = table.getAttribute("interlaced"); if (cmd == "interlacetable") { //TODO 待定 //是否需要待定,如果设置,则命令只能单次执行成功,但反射具备toggle效果;否则可以覆盖前次命令,但反射将不存在toggle效果 return (interlaced === "enabled") ? -1 : 0; } else { return (!interlaced || interlaced === "disabled") ? -1 : 0; } }, execCommand: function (cmd, classList) { var table = getTableItemsByRange(this).table; if (cmd == "interlacetable") { table.setAttribute("interlaced", "enabled"); this.fireEvent("interlacetable", table, classList); } else { table.setAttribute("interlaced", "disabled"); this.fireEvent("uninterlacetable", table); } } }; UE.commands["setbordervisible"] = { queryCommandState: function (cmd) { var table = getTableItemsByRange(this).table; if (!table) return -1; return 0; }, execCommand: function () { var table = getTableItemsByRange(this).table; utils.each(domUtils.getElementsByTagName(table,'td'),function(td){ td.style.borderWidth = '1px'; td.style.borderStyle = 'solid'; }) } }; function resetTdWidth(table, editor) { var tds = domUtils.getElementsByTagName(table,'td th'); utils.each(tds, function (td) { td.removeAttribute("width"); }); table.setAttribute('width', getTableWidth(editor, true, getDefaultValue(editor, table))); var tdsWidths = []; setTimeout(function () { utils.each(tds, function (td) { (td.colSpan == 1) && tdsWidths.push(td.offsetWidth) }) utils.each(tds, function (td,i) { (td.colSpan == 1) && td.setAttribute("width", tdsWidths[i] + ""); }) }, 0); } function getTableWidth(editor, needIEHack, defaultValue) { var body = editor.body; return body.offsetWidth - (needIEHack ? parseInt(domUtils.getComputedStyle(body, 'margin-left'), 10) * 2 : 0) - defaultValue.tableBorder * 2 - (editor.options.offsetWidth || 0); } function getSelectedArr(editor) { var cell = getTableItemsByRange(editor).cell; if (cell) { var ut = getUETable(cell); return ut.selectedTds.length ? ut.selectedTds : [cell]; } else { return []; } } })(); // plugins/table.action.js /** * Created with JetBrains PhpStorm. * User: taoqili * Date: 12-10-12 * Time: 上午10:05 * To change this template use File | Settings | File Templates. */ UE.plugins['table'] = function () { var me = this, tabTimer = null, //拖动计时器 tableDragTimer = null, //双击计时器 tableResizeTimer = null, //单元格最小宽度 cellMinWidth = 5, isInResizeBuffer = false, //单元格边框大小 cellBorderWidth = 5, //鼠标偏移距离 offsetOfTableCell = 10, //记录在有限时间内的点击状态, 共有3个取值, 0, 1, 2。 0代表未初始化, 1代表单击了1次,2代表2次 singleClickState = 0, userActionStatus = null, //双击允许的时间范围 dblclickTime = 360, UT = UE.UETable, getUETable = function (tdOrTable) { return UT.getUETable(tdOrTable); }, getUETableBySelected = function (editor) { return UT.getUETableBySelected(editor); }, getDefaultValue = function (editor, table) { return UT.getDefaultValue(editor, table); }, removeSelectedClass = function (cells) { return UT.removeSelectedClass(cells); }; function showError(e) { // throw e; } me.ready(function(){ var me = this; var orgGetText = me.selection.getText; me.selection.getText = function(){ var table = getUETableBySelected(me); if(table){ var str = ''; utils.each(table.selectedTds,function(td){ str += td[browser.ie?'innerText':'textContent']; }) return str; }else{ return orgGetText.call(me.selection) } } }) //处理拖动及框选相关方法 var startTd = null, //鼠标按下时的锚点td currentTd = null, //当前鼠标经过时的td onDrag = "", //指示当前拖动状态,其值可为"","h","v" ,分别表示未拖动状态,横向拖动状态,纵向拖动状态,用于鼠标移动过程中的判断 onBorder = false, //检测鼠标按下时是否处在单元格边缘位置 dragButton = null, dragOver = false, dragLine = null, //模拟的拖动线 dragTd = null; //发生拖动的目标td var mousedown = false, //todo 判断混乱模式 needIEHack = true; me.setOpt({ 'maxColNum':20, 'maxRowNum':100, 'defaultCols':5, 'defaultRows':5, 'tdvalign':'top', 'cursorpath':me.options.UEDITOR_HOME_URL + "themes/default/images/cursor_", 'tableDragable':false, 'classList':["ue-table-interlace-color-single","ue-table-interlace-color-double"] }); me.getUETable = getUETable; var commands = { 'deletetable':1, 'inserttable':1, 'cellvalign':1, 'insertcaption':1, 'deletecaption':1, 'inserttitle':1, 'deletetitle':1, "mergeright":1, "mergedown":1, "mergecells":1, "insertrow":1, "insertrownext":1, "deleterow":1, "insertcol":1, "insertcolnext":1, "deletecol":1, "splittocells":1, "splittorows":1, "splittocols":1, "adaptbytext":1, "adaptbywindow":1, "adaptbycustomer":1, "insertparagraph":1, "insertparagraphbeforetable":1, "averagedistributecol":1, "averagedistributerow":1 }; me.ready(function () { utils.cssRule('table', //选中的td上的样式 '.selectTdClass{background-color:#edf5fa !important}' + 'table.noBorderTable td,table.noBorderTable th,table.noBorderTable caption{border:1px dashed #ddd !important}' + //插入的表格的默认样式 'table{margin-bottom:10px;border-collapse:collapse;display:table;}' + 'td,th{padding: 5px 10px;border: 1px solid #DDD;}' + 'caption{border:1px dashed #DDD;border-bottom:0;padding:3px;text-align:center;}' + 'th{border-top:1px solid #BBB;background-color:#F7F7F7;}' + 'table tr.firstRow th{border-top-width:2px;}' + '.ue-table-interlace-color-single{ background-color: #fcfcfc; } .ue-table-interlace-color-double{ background-color: #f7faff; }' + 'td p{margin:0;padding:0;}', me.document); var tableCopyList, isFullCol, isFullRow; //注册del/backspace事件 me.addListener('keydown', function (cmd, evt) { var me = this; var keyCode = evt.keyCode || evt.which; if (keyCode == 8) { var ut = getUETableBySelected(me); if (ut && ut.selectedTds.length) { if (ut.isFullCol()) { me.execCommand('deletecol') } else if (ut.isFullRow()) { me.execCommand('deleterow') } else { me.fireEvent('delcells'); } domUtils.preventDefault(evt); } var caption = domUtils.findParentByTagName(me.selection.getStart(), 'caption', true), range = me.selection.getRange(); if (range.collapsed && caption && isEmptyBlock(caption)) { me.fireEvent('saveScene'); var table = caption.parentNode; domUtils.remove(caption); if (table) { range.setStart(table.rows[0].cells[0], 0).setCursor(false, true); } me.fireEvent('saveScene'); } } if (keyCode == 46) { ut = getUETableBySelected(me); if (ut) { me.fireEvent('saveScene'); for (var i = 0, ci; ci = ut.selectedTds[i++];) { domUtils.fillNode(me.document, ci) } me.fireEvent('saveScene'); domUtils.preventDefault(evt); } } if (keyCode == 13) { var rng = me.selection.getRange(), caption = domUtils.findParentByTagName(rng.startContainer, 'caption', true); if (caption) { var table = domUtils.findParentByTagName(caption, 'table'); if (!rng.collapsed) { rng.deleteContents(); me.fireEvent('saveScene'); } else { if (caption) { rng.setStart(table.rows[0].cells[0], 0).setCursor(false, true); } } domUtils.preventDefault(evt); return; } if (rng.collapsed) { var table = domUtils.findParentByTagName(rng.startContainer, 'table'); if (table) { var cell = table.rows[0].cells[0], start = domUtils.findParentByTagName(me.selection.getStart(), ['td', 'th'], true), preNode = table.previousSibling; if (cell === start && (!preNode || preNode.nodeType == 1 && preNode.tagName == 'TABLE' ) && domUtils.isStartInblock(rng)) { var first = domUtils.findParent(me.selection.getStart(), function(n){return domUtils.isBlockElm(n)}, true); if(first && ( /t(h|d)/i.test(first.tagName) || first === start.firstChild )){ me.execCommand('insertparagraphbeforetable'); domUtils.preventDefault(evt); } } } } } if ((evt.ctrlKey || evt.metaKey) && evt.keyCode == '67') { tableCopyList = null; var ut = getUETableBySelected(me); if (ut) { var tds = ut.selectedTds; isFullCol = ut.isFullCol(); isFullRow = ut.isFullRow(); tableCopyList = [ [ut.cloneCell(tds[0],null,true)] ]; for (var i = 1, ci; ci = tds[i]; i++) { if (ci.parentNode !== tds[i - 1].parentNode) { tableCopyList.push([ut.cloneCell(ci,null,true)]); } else { tableCopyList[tableCopyList.length - 1].push(ut.cloneCell(ci,null,true)); } } } } }); me.addListener("tablehasdeleted",function(){ toggleDraggableState(this, false, "", null); if (dragButton)domUtils.remove(dragButton); }); me.addListener('beforepaste', function (cmd, html) { var me = this; var rng = me.selection.getRange(); if (domUtils.findParentByTagName(rng.startContainer, 'caption', true)) { var div = me.document.createElement("div"); div.innerHTML = html.html; //trace:3729 html.html = div[browser.ie9below ? 'innerText' : 'textContent']; return; } var table = getUETableBySelected(me); if (tableCopyList) { me.fireEvent('saveScene'); var rng = me.selection.getRange(); var td = domUtils.findParentByTagName(rng.startContainer, ['td', 'th'], true), tmpNode, preNode; if (td) { var ut = getUETable(td); if (isFullRow) { var rowIndex = ut.getCellInfo(td).rowIndex; if (td.tagName == 'TH') { rowIndex++; } for (var i = 0, ci; ci = tableCopyList[i++];) { var tr = ut.insertRow(rowIndex++, "td"); for (var j = 0, cj; cj = ci[j]; j++) { var cell = tr.cells[j]; if (!cell) { cell = tr.insertCell(j) } cell.innerHTML = cj.innerHTML; cj.getAttribute('width') && cell.setAttribute('width', cj.getAttribute('width')); cj.getAttribute('vAlign') && cell.setAttribute('vAlign', cj.getAttribute('vAlign')); cj.getAttribute('align') && cell.setAttribute('align', cj.getAttribute('align')); cj.style.cssText && (cell.style.cssText = cj.style.cssText) } for (var j = 0, cj; cj = tr.cells[j]; j++) { if (!ci[j]) break; cj.innerHTML = ci[j].innerHTML; ci[j].getAttribute('width') && cj.setAttribute('width', ci[j].getAttribute('width')); ci[j].getAttribute('vAlign') && cj.setAttribute('vAlign', ci[j].getAttribute('vAlign')); ci[j].getAttribute('align') && cj.setAttribute('align', ci[j].getAttribute('align')); ci[j].style.cssText && (cj.style.cssText = ci[j].style.cssText) } } } else { if (isFullCol) { cellInfo = ut.getCellInfo(td); var maxColNum = 0; for (var j = 0, ci = tableCopyList[0], cj; cj = ci[j++];) { maxColNum += cj.colSpan || 1; } me.__hasEnterExecCommand = true; for (i = 0; i < maxColNum; i++) { me.execCommand('insertcol'); } me.__hasEnterExecCommand = false; td = ut.table.rows[0].cells[cellInfo.cellIndex]; if (td.tagName == 'TH') { td = ut.table.rows[1].cells[cellInfo.cellIndex]; } } for (var i = 0, ci; ci = tableCopyList[i++];) { tmpNode = td; for (var j = 0, cj; cj = ci[j++];) { if (td) { td.innerHTML = cj.innerHTML; //todo 定制处理 cj.getAttribute('width') && td.setAttribute('width', cj.getAttribute('width')); cj.getAttribute('vAlign') && td.setAttribute('vAlign', cj.getAttribute('vAlign')); cj.getAttribute('align') && td.setAttribute('align', cj.getAttribute('align')); cj.style.cssText && (td.style.cssText = cj.style.cssText); preNode = td; td = td.nextSibling; } else { var cloneTd = cj.cloneNode(true); domUtils.removeAttributes(cloneTd, ['class', 'rowSpan', 'colSpan']); preNode.parentNode.appendChild(cloneTd) } } td = ut.getNextCell(tmpNode, true, true); if (!tableCopyList[i]) break; if (!td) { var cellInfo = ut.getCellInfo(tmpNode); ut.table.insertRow(ut.table.rows.length); ut.update(); td = ut.getVSideCell(tmpNode, true); } } } ut.update(); } else { table = me.document.createElement('table'); for (var i = 0, ci; ci = tableCopyList[i++];) { var tr = table.insertRow(table.rows.length); for (var j = 0, cj; cj = ci[j++];) { cloneTd = UT.cloneCell(cj,null,true); domUtils.removeAttributes(cloneTd, ['class']); tr.appendChild(cloneTd) } if (j == 2 && cloneTd.rowSpan > 1) { cloneTd.rowSpan = 1; } } var defaultValue = getDefaultValue(me), width = me.body.offsetWidth - (needIEHack ? parseInt(domUtils.getComputedStyle(me.body, 'margin-left'), 10) * 2 : 0) - defaultValue.tableBorder * 2 - (me.options.offsetWidth || 0); me.execCommand('insertHTML', '' + table.innerHTML.replace(/>\s*<').replace(/\bth\b/gi, "td") + '
    ') } me.fireEvent('contentchange'); me.fireEvent('saveScene'); html.html = ''; return true; } else { var div = me.document.createElement("div"), tables; div.innerHTML = html.html; tables = div.getElementsByTagName("table"); if (domUtils.findParentByTagName(me.selection.getStart(), 'table')) { utils.each(tables, function (t) { domUtils.remove(t) }); if (domUtils.findParentByTagName(me.selection.getStart(), 'caption', true)) { div.innerHTML = div[browser.ie ? 'innerText' : 'textContent']; } } else { utils.each(tables, function (table) { removeStyleSize(table, true); domUtils.removeAttributes(table, ['style', 'border']); utils.each(domUtils.getElementsByTagName(table, "td"), function (td) { if (isEmptyBlock(td)) { domUtils.fillNode(me.document, td); } removeStyleSize(td, true); // domUtils.removeAttributes(td, ['style']) }); }); } html.html = div.innerHTML; } }); me.addListener('afterpaste', function () { utils.each(domUtils.getElementsByTagName(me.body, "table"), function (table) { if (table.offsetWidth > me.body.offsetWidth) { var defaultValue = getDefaultValue(me, table); table.style.width = me.body.offsetWidth - (needIEHack ? parseInt(domUtils.getComputedStyle(me.body, 'margin-left'), 10) * 2 : 0) - defaultValue.tableBorder * 2 - (me.options.offsetWidth || 0) + 'px' } }) }); me.addListener('blur', function () { tableCopyList = null; }); var timer; me.addListener('keydown', function () { clearTimeout(timer); timer = setTimeout(function () { var rng = me.selection.getRange(), cell = domUtils.findParentByTagName(rng.startContainer, ['th', 'td'], true); if (cell) { var table = cell.parentNode.parentNode.parentNode; if (table.offsetWidth > table.getAttribute("width")) { cell.style.wordBreak = "break-all"; } } }, 100); }); me.addListener("selectionchange", function () { toggleDraggableState(me, false, "", null); }); //内容变化时触发索引更新 //todo 可否考虑标记检测,如果不涉及表格的变化就不进行索引重建和更新 me.addListener("contentchange", function () { var me = this; //尽可能排除一些不需要更新的状况 hideDragLine(me); if (getUETableBySelected(me))return; var rng = me.selection.getRange(); var start = rng.startContainer; start = domUtils.findParentByTagName(start, ['td', 'th'], true); utils.each(domUtils.getElementsByTagName(me.document, 'table'), function (table) { if (me.fireEvent("excludetable", table) === true) return; table.ueTable = new UT(table); //trace:3742 // utils.each(domUtils.getElementsByTagName(me.document, 'td'), function (td) { // // if (domUtils.isEmptyBlock(td) && td !== start) { // domUtils.fillNode(me.document, td); // if (browser.ie && browser.version == 6) { // td.innerHTML = ' ' // } // } // }); // utils.each(domUtils.getElementsByTagName(me.document, 'th'), function (th) { // if (domUtils.isEmptyBlock(th) && th !== start) { // domUtils.fillNode(me.document, th); // if (browser.ie && browser.version == 6) { // th.innerHTML = ' ' // } // } // }); table.onmouseover = function () { me.fireEvent('tablemouseover', table); }; table.onmousemove = function () { me.fireEvent('tablemousemove', table); me.options.tableDragable && toggleDragButton(true, this, me); utils.defer(function(){ me.fireEvent('contentchange',50) },true) }; table.onmouseout = function () { me.fireEvent('tablemouseout', table); toggleDraggableState(me, false, "", null); hideDragLine(me); }; table.onclick = function (evt) { evt = me.window.event || evt; var target = getParentTdOrTh(evt.target || evt.srcElement); if (!target)return; var ut = getUETable(target), table = ut.table, cellInfo = ut.getCellInfo(target), cellsRange, rng = me.selection.getRange(); // if ("topLeft" == inPosition(table, mouseCoords(evt))) { // cellsRange = ut.getCellsRange(ut.table.rows[0].cells[0], ut.getLastCell()); // ut.setSelected(cellsRange); // return; // } // if ("bottomRight" == inPosition(table, mouseCoords(evt))) { // // return; // } if (inTableSide(table, target, evt, true)) { var endTdCol = ut.getCell(ut.indexTable[ut.rowsNum - 1][cellInfo.colIndex].rowIndex, ut.indexTable[ut.rowsNum - 1][cellInfo.colIndex].cellIndex); if (evt.shiftKey && ut.selectedTds.length) { if (ut.selectedTds[0] !== endTdCol) { cellsRange = ut.getCellsRange(ut.selectedTds[0], endTdCol); ut.setSelected(cellsRange); } else { rng && rng.selectNodeContents(endTdCol).select(); } } else { if (target !== endTdCol) { cellsRange = ut.getCellsRange(target, endTdCol); ut.setSelected(cellsRange); } else { rng && rng.selectNodeContents(endTdCol).select(); } } return; } if (inTableSide(table, target, evt)) { var endTdRow = ut.getCell(ut.indexTable[cellInfo.rowIndex][ut.colsNum - 1].rowIndex, ut.indexTable[cellInfo.rowIndex][ut.colsNum - 1].cellIndex); if (evt.shiftKey && ut.selectedTds.length) { if (ut.selectedTds[0] !== endTdRow) { cellsRange = ut.getCellsRange(ut.selectedTds[0], endTdRow); ut.setSelected(cellsRange); } else { rng && rng.selectNodeContents(endTdRow).select(); } } else { if (target !== endTdRow) { cellsRange = ut.getCellsRange(target, endTdRow); ut.setSelected(cellsRange); } else { rng && rng.selectNodeContents(endTdRow).select(); } } } }; }); switchBorderColor(me, true); }); domUtils.on(me.document, "mousemove", mouseMoveEvent); domUtils.on(me.document, "mouseout", function (evt) { var target = evt.target || evt.srcElement; if (target.tagName == "TABLE") { toggleDraggableState(me, false, "", null); } }); /** * 表格隔行变色 */ me.addListener("interlacetable",function(type,table,classList){ if(!table) return; var me = this, rows = table.rows, len = rows.length, getClass = function(list,index,repeat){ return list[index] ? list[index] : repeat ? list[index % list.length]: ""; }; for(var i = 0;i 1 ? currentRowIndex : ua.getCellInfo(cell).rowIndex; var nextCell = ua.getTabNextCell(cell, currentRowIndex); if (nextCell) { if (isEmptyBlock(nextCell)) { range.setStart(nextCell, 0).setCursor(false, true) } else { range.selectNodeContents(nextCell).select() } } else { me.fireEvent('saveScene'); me.__hasEnterExecCommand = true; this.execCommand('insertrownext'); me.__hasEnterExecCommand = false; range = this.selection.getRange(); range.setStart(table.rows[table.rows.length - 1].cells[0], 0).setCursor(); me.fireEvent('saveScene'); } } return true; } }); browser.ie && me.addListener('selectionchange', function () { toggleDraggableState(this, false, "", null); }); me.addListener("keydown", function (type, evt) { var me = this; //处理在表格的最后一个输入tab产生新的表格 var keyCode = evt.keyCode || evt.which; if (keyCode == 8 || keyCode == 46) { return; } var notCtrlKey = !evt.ctrlKey && !evt.metaKey && !evt.shiftKey && !evt.altKey; notCtrlKey && removeSelectedClass(domUtils.getElementsByTagName(me.body, "td")); var ut = getUETableBySelected(me); if (!ut) return; notCtrlKey && ut.clearSelected(); }); me.addListener("beforegetcontent", function () { switchBorderColor(this, false); browser.ie && utils.each(this.document.getElementsByTagName('caption'), function (ci) { if (domUtils.isEmptyNode(ci)) { ci.innerHTML = ' ' } }); }); me.addListener("aftergetcontent", function () { switchBorderColor(this, true); }); me.addListener("getAllHtml", function () { removeSelectedClass(me.document.getElementsByTagName("td")); }); //修正全屏状态下插入的表格宽度在非全屏状态下撑开编辑器的情况 me.addListener("fullscreenchanged", function (type, fullscreen) { if (!fullscreen) { var ratio = this.body.offsetWidth / document.body.offsetWidth, tables = domUtils.getElementsByTagName(this.body, "table"); utils.each(tables, function (table) { if (table.offsetWidth < me.body.offsetWidth) return false; var tds = domUtils.getElementsByTagName(table, "td"), backWidths = []; utils.each(tds, function (td) { backWidths.push(td.offsetWidth); }); for (var i = 0, td; td = tds[i]; i++) { td.setAttribute("width", Math.floor(backWidths[i] * ratio)); } table.setAttribute("width", Math.floor(getTableWidth(me, needIEHack, getDefaultValue(me)))) }); } }); //重写execCommand命令,用于处理框选时的处理 var oldExecCommand = me.execCommand; me.execCommand = function (cmd, datatat) { var me = this, args = arguments; cmd = cmd.toLowerCase(); var ut = getUETableBySelected(me), tds, range = new dom.Range(me.document), cmdFun = me.commands[cmd] || UE.commands[cmd], result; if (!cmdFun) return; if (ut && !commands[cmd] && !cmdFun.notNeedUndo && !me.__hasEnterExecCommand) { me.__hasEnterExecCommand = true; me.fireEvent("beforeexeccommand", cmd); tds = ut.selectedTds; var lastState = -2, lastValue = -2, value, state; for (var i = 0, td; td = tds[i]; i++) { if (isEmptyBlock(td)) { range.setStart(td, 0).setCursor(false, true) } else { range.selectNode(td).select(true); } state = me.queryCommandState(cmd); value = me.queryCommandValue(cmd); if (state != -1) { if (lastState !== state || lastValue !== value) { me._ignoreContentChange = true; result = oldExecCommand.apply(me, arguments); me._ignoreContentChange = false; } lastState = me.queryCommandState(cmd); lastValue = me.queryCommandValue(cmd); if (domUtils.isEmptyBlock(td)) { domUtils.fillNode(me.document, td) } } } range.setStart(tds[0], 0).shrinkBoundary(true).setCursor(false, true); me.fireEvent('contentchange'); me.fireEvent("afterexeccommand", cmd); me.__hasEnterExecCommand = false; me._selectionChange(); } else { result = oldExecCommand.apply(me, arguments); } return result; }; }); /** * 删除obj的宽高style,改成属性宽高 * @param obj * @param replaceToProperty */ function removeStyleSize(obj, replaceToProperty) { removeStyle(obj, "width", true); removeStyle(obj, "height", true); } function removeStyle(obj, styleName, replaceToProperty) { if (obj.style[styleName]) { replaceToProperty && obj.setAttribute(styleName, parseInt(obj.style[styleName], 10)); obj.style[styleName] = ""; } } function getParentTdOrTh(ele) { if (ele.tagName == "TD" || ele.tagName == "TH") return ele; var td; if (td = domUtils.findParentByTagName(ele, "td", true) || domUtils.findParentByTagName(ele, "th", true)) return td; return null; } function isEmptyBlock(node) { var reg = new RegExp(domUtils.fillChar, 'g'); if (node[browser.ie ? 'innerText' : 'textContent'].replace(/^\s*$/, '').replace(reg, '').length > 0) { return 0; } for (var n in dtd.$isNotEmpty) { if (node.getElementsByTagName(n).length) { return 0; } } return 1; } function mouseCoords(evt) { if (evt.pageX || evt.pageY) { return { x:evt.pageX, y:evt.pageY }; } return { x:evt.clientX + me.document.body.scrollLeft - me.document.body.clientLeft, y:evt.clientY + me.document.body.scrollTop - me.document.body.clientTop }; } function mouseMoveEvent(evt) { if( isEditorDisabled() ) { return; } try { //普通状态下鼠标移动 var target = getParentTdOrTh(evt.target || evt.srcElement), pos; //区分用户的行为是拖动还是双击 if( isInResizeBuffer ) { me.body.style.webkitUserSelect = 'none'; if( Math.abs( userActionStatus.x - evt.clientX ) > offsetOfTableCell || Math.abs( userActionStatus.y - evt.clientY ) > offsetOfTableCell ) { clearTableDragTimer(); isInResizeBuffer = false; singleClickState = 0; //drag action tableBorderDrag(evt); } } //修改单元格大小时的鼠标移动 if (onDrag && dragTd) { singleClickState = 0; me.body.style.webkitUserSelect = 'none'; me.selection.getNative()[browser.ie9below ? 'empty' : 'removeAllRanges'](); pos = mouseCoords(evt); toggleDraggableState(me, true, onDrag, pos, target); if (onDrag == "h") { dragLine.style.left = getPermissionX(dragTd, evt) + "px"; } else if (onDrag == "v") { dragLine.style.top = getPermissionY(dragTd, evt) + "px"; } return; } //当鼠标处于table上时,修改移动过程中的光标状态 if (target) { //针对使用table作为容器的组件不触发拖拽效果 if (me.fireEvent('excludetable', target) === true) return; pos = mouseCoords(evt); var state = getRelation(target, pos), table = domUtils.findParentByTagName(target, "table", true); if (inTableSide(table, target, evt, true)) { if (me.fireEvent("excludetable", table) === true) return; me.body.style.cursor = "url(" + me.options.cursorpath + "h.png),pointer"; } else if (inTableSide(table, target, evt)) { if (me.fireEvent("excludetable", table) === true) return; me.body.style.cursor = "url(" + me.options.cursorpath + "v.png),pointer"; } else { me.body.style.cursor = "text"; var curCell = target; if (/\d/.test(state)) { state = state.replace(/\d/, ''); target = getUETable(target).getPreviewCell(target, state == "v"); } //位于第一行的顶部或者第一列的左边时不可拖动 toggleDraggableState(me, target ? !!state : false, target ? state : '', pos, target); } } else { toggleDragButton(false, table, me); } } catch (e) { showError(e); } } var dragButtonTimer; function toggleDragButton(show, table, editor) { if (!show) { if (dragOver)return; dragButtonTimer = setTimeout(function () { !dragOver && dragButton && dragButton.parentNode && dragButton.parentNode.removeChild(dragButton); }, 2000); } else { createDragButton(table, editor); } } function createDragButton(table, editor) { var pos = domUtils.getXY(table), doc = table.ownerDocument; if (dragButton && dragButton.parentNode)return dragButton; dragButton = doc.createElement("div"); dragButton.contentEditable = false; dragButton.innerHTML = ""; dragButton.style.cssText = "width:15px;height:15px;background-image:url(" + editor.options.UEDITOR_HOME_URL + "dialogs/table/dragicon.png);position: absolute;cursor:move;top:" + (pos.y - 15) + "px;left:" + (pos.x) + "px;"; domUtils.unSelectable(dragButton); dragButton.onmouseover = function (evt) { dragOver = true; }; dragButton.onmouseout = function (evt) { dragOver = false; }; domUtils.on(dragButton, 'click', function (type, evt) { doClick(evt, this); }); domUtils.on(dragButton, 'dblclick', function (type, evt) { doDblClick(evt); }); domUtils.on(dragButton, 'dragstart', function (type, evt) { domUtils.preventDefault(evt); }); var timer; function doClick(evt, button) { // 部分浏览器下需要清理 clearTimeout(timer); timer = setTimeout(function () { editor.fireEvent("tableClicked", table, button); }, 300); } function doDblClick(evt) { clearTimeout(timer); var ut = getUETable(table), start = table.rows[0].cells[0], end = ut.getLastCell(), range = ut.getCellsRange(start, end); editor.selection.getRange().setStart(start, 0).setCursor(false, true); ut.setSelected(range); } doc.body.appendChild(dragButton); } // function inPosition(table, pos) { // var tablePos = domUtils.getXY(table), // width = table.offsetWidth, // height = table.offsetHeight; // if (pos.x - tablePos.x < 5 && pos.y - tablePos.y < 5) { // return "topLeft"; // } else if (tablePos.x + width - pos.x < 5 && tablePos.y + height - pos.y < 5) { // return "bottomRight"; // } // } function inTableSide(table, cell, evt, top) { var pos = mouseCoords(evt), state = getRelation(cell, pos); if (top) { var caption = table.getElementsByTagName("caption")[0], capHeight = caption ? caption.offsetHeight : 0; return (state == "v1") && ((pos.y - domUtils.getXY(table).y - capHeight) < 8); } else { return (state == "h1") && ((pos.x - domUtils.getXY(table).x) < 8); } } /** * 获取拖动时允许的X轴坐标 * @param dragTd * @param evt */ function getPermissionX(dragTd, evt) { var ut = getUETable(dragTd); if (ut) { var preTd = ut.getSameEndPosCells(dragTd, "x")[0], nextTd = ut.getSameStartPosXCells(dragTd)[0], mouseX = mouseCoords(evt).x, left = (preTd ? domUtils.getXY(preTd).x : domUtils.getXY(ut.table).x) + 20 , right = nextTd ? domUtils.getXY(nextTd).x + nextTd.offsetWidth - 20 : (me.body.offsetWidth + 5 || parseInt(domUtils.getComputedStyle(me.body, "width"), 10)); left += cellMinWidth; right -= cellMinWidth; return mouseX < left ? left : mouseX > right ? right : mouseX; } } /** * 获取拖动时允许的Y轴坐标 */ function getPermissionY(dragTd, evt) { try { var top = domUtils.getXY(dragTd).y, mousePosY = mouseCoords(evt).y; return mousePosY < top ? top : mousePosY; } catch (e) { showError(e); } } /** * 移动状态切换 */ function toggleDraggableState(editor, draggable, dir, mousePos, cell) { try { editor.body.style.cursor = dir == "h" ? "col-resize" : dir == "v" ? "row-resize" : "text"; if (browser.ie) { if (dir && !mousedown && !getUETableBySelected(editor)) { getDragLine(editor, editor.document); showDragLineAt(dir, cell); } else { hideDragLine(editor) } } onBorder = draggable; } catch (e) { showError(e); } } /** * 获取与UETable相关的resize line * @param uetable UETable对象 */ function getResizeLineByUETable() { var lineId = '_UETableResizeLine', line = this.document.getElementById( lineId ); if( !line ) { line = this.document.createElement("div"); line.id = lineId; line.contnetEditable = false; line.setAttribute("unselectable", "on"); var styles = { width: 2*cellBorderWidth + 1 + 'px', position: 'absolute', 'z-index': 100000, cursor: 'col-resize', background: 'red', display: 'none' }; //切换状态 line.onmouseout = function(){ this.style.display = 'none'; }; utils.extend( line.style, styles ); this.document.body.appendChild( line ); } return line; } /** * 更新resize-line */ function updateResizeLine( cell, uetable ) { var line = getResizeLineByUETable.call( this ), table = uetable.table, styles = { top: domUtils.getXY( table ).y + 'px', left: domUtils.getXY( cell).x + cell.offsetWidth - cellBorderWidth + 'px', display: 'block', height: table.offsetHeight + 'px' }; utils.extend( line.style, styles ); } /** * 显示resize-line */ function showResizeLine( cell ) { var uetable = getUETable( cell ); updateResizeLine.call( this, cell, uetable ); } /** * 获取鼠标与当前单元格的相对位置 * @param ele * @param mousePos */ function getRelation(ele, mousePos) { var elePos = domUtils.getXY(ele); if( !elePos ) { return ''; } if (elePos.x + ele.offsetWidth - mousePos.x < cellBorderWidth) { return "h"; } if (mousePos.x - elePos.x < cellBorderWidth) { return 'h1' } if (elePos.y + ele.offsetHeight - mousePos.y < cellBorderWidth) { return "v"; } if (mousePos.y - elePos.y < cellBorderWidth) { return 'v1' } return ''; } function mouseDownEvent(type, evt) { if( isEditorDisabled() ) { return ; } userActionStatus = { x: evt.clientX, y: evt.clientY }; //右键菜单单独处理 if (evt.button == 2) { var ut = getUETableBySelected(me), flag = false; if (ut) { var td = getTargetTd(me, evt); utils.each(ut.selectedTds, function (ti) { if (ti === td) { flag = true; } }); if (!flag) { removeSelectedClass(domUtils.getElementsByTagName(me.body, "th td")); ut.clearSelected() } else { td = ut.selectedTds[0]; setTimeout(function () { me.selection.getRange().setStart(td, 0).setCursor(false, true); }, 0); } } } else { tableClickHander( evt ); } } //清除表格的计时器 function clearTableTimer() { tabTimer && clearTimeout( tabTimer ); tabTimer = null; } //双击收缩 function tableDbclickHandler(evt) { singleClickState = 0; evt = evt || me.window.event; var target = getParentTdOrTh(evt.target || evt.srcElement); if (target) { var h; if (h = getRelation(target, mouseCoords(evt))) { hideDragLine( me ); if (h == 'h1') { h = 'h'; if (inTableSide(domUtils.findParentByTagName(target, "table"), target, evt)) { me.execCommand('adaptbywindow'); } else { target = getUETable(target).getPreviewCell(target); if (target) { var rng = me.selection.getRange(); rng.selectNodeContents(target).setCursor(true, true) } } } if (h == 'h') { var ut = getUETable(target), table = ut.table, cells = getCellsByMoveBorder( target, table, true ); cells = extractArray( cells, 'left' ); ut.width = ut.offsetWidth; var oldWidth = [], newWidth = []; utils.each( cells, function( cell ){ oldWidth.push( cell.offsetWidth ); } ); utils.each( cells, function( cell ){ cell.removeAttribute("width"); } ); window.setTimeout( function(){ //是否允许改变 var changeable = true; utils.each( cells, function( cell, index ){ var width = cell.offsetWidth; if( width > oldWidth[index] ) { changeable = false; return false; } newWidth.push( width ); } ); var change = changeable ? newWidth : oldWidth; utils.each( cells, function( cell, index ){ cell.width = change[index] - getTabcellSpace(); } ); }, 0 ); // minWidth -= cellMinWidth; // // table.removeAttribute("width"); // utils.each(cells, function (cell) { // cell.style.width = ""; // cell.width -= minWidth; // }); } } } } function tableClickHander( evt ) { removeSelectedClass(domUtils.getElementsByTagName(me.body, "td th")); //trace:3113 //选中单元格,点击table外部,不会清掉table上挂的ueTable,会引起getUETableBySelected方法返回值 utils.each(me.document.getElementsByTagName('table'), function (t) { t.ueTable = null; }); startTd = getTargetTd(me, evt); if( !startTd ) return; var table = domUtils.findParentByTagName(startTd, "table", true); ut = getUETable(table); ut && ut.clearSelected(); //判断当前鼠标状态 if (!onBorder) { me.document.body.style.webkitUserSelect = ''; mousedown = true; me.addListener('mouseover', mouseOverEvent); } else { //边框上的动作处理 borderActionHandler( evt ); } } //处理表格边框上的动作, 这里做延时处理,避免两种动作互相影响 function borderActionHandler( evt ) { if ( browser.ie ) { evt = reconstruct(evt ); } clearTableDragTimer(); //是否正在等待resize的缓冲中 isInResizeBuffer = true; tableDragTimer = setTimeout(function(){ tableBorderDrag( evt ); }, dblclickTime); } function extractArray( originArr, key ) { var result = [], tmp = null; for( var i = 0, len = originArr.length; i 0 && singleClickState--; }, dblclickTime ); if( singleClickState === 2 ) { singleClickState = 0; tableDbclickHandler(evt); return; } } if (evt.button == 2)return; var me = this; //清除表格上原生跨选问题 var range = me.selection.getRange(), start = domUtils.findParentByTagName(range.startContainer, 'table', true), end = domUtils.findParentByTagName(range.endContainer, 'table', true); if (start || end) { if (start === end) { start = domUtils.findParentByTagName(range.startContainer, ['td', 'th', 'caption'], true); end = domUtils.findParentByTagName(range.endContainer, ['td', 'th', 'caption'], true); if (start !== end) { me.selection.clearRange() } } else { me.selection.clearRange() } } mousedown = false; me.document.body.style.webkitUserSelect = ''; //拖拽状态下的mouseUP if ( onDrag && dragTd ) { me.selection.getNative()[browser.ie9below ? 'empty' : 'removeAllRanges'](); singleClickState = 0; dragLine = me.document.getElementById('ue_tableDragLine'); // trace 3973 if (dragLine) { var dragTdPos = domUtils.getXY(dragTd), dragLinePos = domUtils.getXY(dragLine); switch (onDrag) { case "h": changeColWidth(dragTd, dragLinePos.x - dragTdPos.x); break; case "v": changeRowHeight(dragTd, dragLinePos.y - dragTdPos.y - dragTd.offsetHeight); break; default: } onDrag = ""; dragTd = null; hideDragLine(me); me.fireEvent('saveScene'); return; } } //正常状态下的mouseup if (!startTd) { var target = domUtils.findParentByTagName(evt.target || evt.srcElement, "td", true); if (!target) target = domUtils.findParentByTagName(evt.target || evt.srcElement, "th", true); if (target && (target.tagName == "TD" || target.tagName == "TH")) { if (me.fireEvent("excludetable", target) === true) return; range = new dom.Range(me.document); range.setStart(target, 0).setCursor(false, true); } } else { var ut = getUETable(startTd), cell = ut ? ut.selectedTds[0] : null; if (cell) { range = new dom.Range(me.document); if (domUtils.isEmptyBlock(cell)) { range.setStart(cell, 0).setCursor(false, true); } else { range.selectNodeContents(cell).shrinkBoundary().setCursor(false, true); } } else { range = me.selection.getRange().shrinkBoundary(); if (!range.collapsed) { var start = domUtils.findParentByTagName(range.startContainer, ['td', 'th'], true), end = domUtils.findParentByTagName(range.endContainer, ['td', 'th'], true); //在table里边的不能清除 if (start && !end || !start && end || start && end && start !== end) { range.setCursor(false, true); } } } startTd = null; me.removeListener('mouseover', mouseOverEvent); } me._selectionChange(250, evt); } function mouseOverEvent(type, evt) { if( isEditorDisabled() ) { return; } var me = this, tar = evt.target || evt.srcElement; currentTd = domUtils.findParentByTagName(tar, "td", true) || domUtils.findParentByTagName(tar, "th", true); //需要判断两个TD是否位于同一个表格内 if (startTd && currentTd && ((startTd.tagName == "TD" && currentTd.tagName == "TD") || (startTd.tagName == "TH" && currentTd.tagName == "TH")) && domUtils.findParentByTagName(startTd, 'table') == domUtils.findParentByTagName(currentTd, 'table')) { var ut = getUETable(currentTd); if (startTd != currentTd) { me.document.body.style.webkitUserSelect = 'none'; me.selection.getNative()[browser.ie9below ? 'empty' : 'removeAllRanges'](); var range = ut.getCellsRange(startTd, currentTd); ut.setSelected(range); } else { me.document.body.style.webkitUserSelect = ''; ut.clearSelected(); } } evt.preventDefault ? evt.preventDefault() : (evt.returnValue = false); } function setCellHeight(cell, height, backHeight) { var lineHight = parseInt(domUtils.getComputedStyle(cell, "line-height"), 10), tmpHeight = backHeight + height; height = tmpHeight < lineHight ? lineHight : tmpHeight; if (cell.style.height) cell.style.height = ""; cell.rowSpan == 1 ? cell.setAttribute("height", height) : (cell.removeAttribute && cell.removeAttribute("height")); } function getWidth(cell) { if (!cell)return 0; return parseInt(domUtils.getComputedStyle(cell, "width"), 10); } function changeColWidth(cell, changeValue) { var ut = getUETable(cell); if (ut) { //根据当前移动的边框获取相关的单元格 var table = ut.table, cells = getCellsByMoveBorder( cell, table ); table.style.width = ""; table.removeAttribute("width"); //修正改变量 changeValue = correctChangeValue( changeValue, cell, cells ); if (cell.nextSibling) { var i=0; utils.each( cells, function( cellGroup ){ cellGroup.left.width = (+cellGroup.left.width)+changeValue; cellGroup.right && ( cellGroup.right.width = (+cellGroup.right.width)-changeValue ); } ); } else { utils.each( cells, function( cellGroup ){ cellGroup.left.width -= -changeValue; } ); } } } function isEditorDisabled() { return me.body.contentEditable === "false"; } function changeRowHeight(td, changeValue) { if (Math.abs(changeValue) < 10) return; var ut = getUETable(td); if (ut) { var cells = ut.getSameEndPosCells(td, "y"), //备份需要连带变化的td的原始高度,否则后期无法获取正确的值 backHeight = cells[0] ? cells[0].offsetHeight : 0; for (var i = 0, cell; cell = cells[i++];) { setCellHeight(cell, changeValue, backHeight); } } } /** * 获取调整单元格大小的相关单元格 * @isContainMergeCell 返回的结果中是否包含发生合并后的单元格 */ function getCellsByMoveBorder( cell, table, isContainMergeCell ) { if( !table ) { table = domUtils.findParentByTagName( cell, 'table' ); } if( !table ) { return null; } //获取到该单元格所在行的序列号 var index = domUtils.getNodeIndex( cell ), temp = cell, rows = table.rows, colIndex = 0; while( temp ) { //获取到当前单元格在未发生单元格合并时的序列 if( temp.nodeType === 1 ) { colIndex += (temp.colSpan || 1); } temp = temp.previousSibling; } temp = null; //记录想关的单元格 var borderCells = []; utils.each(rows, function( tabRow ){ var cells = tabRow.cells, currIndex = 0; utils.each( cells, function( tabCell ){ currIndex += (tabCell.colSpan || 1); if( currIndex === colIndex ) { borderCells.push({ left: tabCell, right: tabCell.nextSibling || null }); return false; } else if( currIndex > colIndex ) { if( isContainMergeCell ) { borderCells.push({ left: tabCell }); } return false; } } ); }); return borderCells; } /** * 通过给定的单元格集合获取最小的单元格width */ function getMinWidthByTableCells( cells ) { var minWidth = Number.MAX_VALUE; for( var i = 0, curCell; curCell = cells[ i ] ; i++ ) { minWidth = Math.min( minWidth, curCell.width || getTableCellWidth( curCell ) ); } return minWidth; } function correctChangeValue( changeValue, relatedCell, cells ) { //为单元格的paading预留空间 changeValue -= getTabcellSpace(); if( changeValue < 0 ) { return 0; } changeValue -= getTableCellWidth( relatedCell ); //确定方向 var direction = changeValue < 0 ? 'left':'right'; changeValue = Math.abs(changeValue); //只关心非最后一个单元格就可以 utils.each( cells, function( cellGroup ){ var curCell = cellGroup[direction]; //为单元格保留最小空间 if( curCell ) { changeValue = Math.min( changeValue, getTableCellWidth( curCell )-cellMinWidth ); } } ); //修正越界 changeValue = changeValue < 0 ? 0 : changeValue; return direction === 'left' ? -changeValue : changeValue; } function getTableCellWidth( cell ) { var width = 0, //偏移纠正量 offset = 0, width = cell.offsetWidth - getTabcellSpace(); //最后一个节点纠正一下 if( !cell.nextSibling ) { width -= getTableCellOffset( cell ); } width = width < 0 ? 0 : width; try { cell.width = width; } catch(e) { } return width; } /** * 获取单元格所在表格的最末单元格的偏移量 */ function getTableCellOffset( cell ) { tab = domUtils.findParentByTagName( cell, "table", false); if( tab.offsetVal === undefined ) { var prev = cell.previousSibling; if( prev ) { //最后一个单元格和前一个单元格的width diff结果 如果恰好为一个border width, 则条件成立 tab.offsetVal = cell.offsetWidth - prev.offsetWidth === UT.borderWidth ? UT.borderWidth : 0; } else { tab.offsetVal = 0; } } return tab.offsetVal; } function getTabcellSpace() { if( UT.tabcellSpace === undefined ) { var cell = null, tab = me.document.createElement("table"), tbody = me.document.createElement("tbody"), trow = me.document.createElement("tr"), tabcell = me.document.createElement("td"), mirror = null; tabcell.style.cssText = 'border: 0;'; tabcell.width = 1; trow.appendChild( tabcell ); trow.appendChild( mirror = tabcell.cloneNode( false ) ); tbody.appendChild( trow ); tab.appendChild( tbody ); tab.style.cssText = "visibility: hidden;"; me.body.appendChild( tab ); UT.paddingSpace = tabcell.offsetWidth - 1; var tmpTabWidth = tab.offsetWidth; tabcell.style.cssText = ''; mirror.style.cssText = ''; UT.borderWidth = ( tab.offsetWidth - tmpTabWidth ) / 3; UT.tabcellSpace = UT.paddingSpace + UT.borderWidth; me.body.removeChild( tab ); } getTabcellSpace = function(){ return UT.tabcellSpace; }; return UT.tabcellSpace; } function getDragLine(editor, doc) { if (mousedown)return; dragLine = editor.document.createElement("div"); domUtils.setAttributes(dragLine, { id:"ue_tableDragLine", unselectable:'on', contenteditable:false, 'onresizestart':'return false', 'ondragstart':'return false', 'onselectstart':'return false', style:"background-color:blue;position:absolute;padding:0;margin:0;background-image:none;border:0px none;opacity:0;filter:alpha(opacity=0)" }); editor.body.appendChild(dragLine); } function hideDragLine(editor) { if (mousedown)return; var line; while (line = editor.document.getElementById('ue_tableDragLine')) { domUtils.remove(line) } } /** * 依据state(v|h)在cell位置显示横线 * @param state * @param cell */ function showDragLineAt(state, cell) { if (!cell) return; var table = domUtils.findParentByTagName(cell, "table"), caption = table.getElementsByTagName('caption'), width = table.offsetWidth, height = table.offsetHeight - (caption.length > 0 ? caption[0].offsetHeight : 0), tablePos = domUtils.getXY(table), cellPos = domUtils.getXY(cell), css; switch (state) { case "h": css = 'height:' + height + 'px;top:' + (tablePos.y + (caption.length > 0 ? caption[0].offsetHeight : 0)) + 'px;left:' + (cellPos.x + cell.offsetWidth); dragLine.style.cssText = css + 'px;position: absolute;display:block;background-color:blue;width:1px;border:0; color:blue;opacity:.3;filter:alpha(opacity=30)'; break; case "v": css = 'width:' + width + 'px;left:' + tablePos.x + 'px;top:' + (cellPos.y + cell.offsetHeight ); //必须加上border:0和color:blue,否则低版ie不支持背景色显示 dragLine.style.cssText = css + 'px;overflow:hidden;position: absolute;display:block;background-color:blue;height:1px;border:0;color:blue;opacity:.2;filter:alpha(opacity=20)'; break; default: } } /** * 当表格边框颜色为白色时设置为虚线,true为添加虚线 * @param editor * @param flag */ function switchBorderColor(editor, flag) { var tableArr = domUtils.getElementsByTagName(editor.body, "table"), color; for (var i = 0, node; node = tableArr[i++];) { var td = domUtils.getElementsByTagName(node, "td"); if (td[0]) { if (flag) { color = (td[0].style.borderColor).replace(/\s/g, ""); if (/(#ffffff)|(rgb\(255,255,255\))/ig.test(color)) domUtils.addClass(node, "noBorderTable") } else { domUtils.removeClasses(node, "noBorderTable") } } } } function getTableWidth(editor, needIEHack, defaultValue) { var body = editor.body; return body.offsetWidth - (needIEHack ? parseInt(domUtils.getComputedStyle(body, 'margin-left'), 10) * 2 : 0) - defaultValue.tableBorder * 2 - (editor.options.offsetWidth || 0); } /** * 获取当前拖动的单元格 */ function getTargetTd(editor, evt) { var target = domUtils.findParentByTagName(evt.target || evt.srcElement, ["td", "th"], true), dir = null; if( !target ) { return null; } dir = getRelation( target, mouseCoords( evt ) ); //如果有前一个节点, 需要做一个修正, 否则可能会得到一个错误的td if( !target ) { return null; } if( dir === 'h1' && target.previousSibling ) { var position = domUtils.getXY( target), cellWidth = target.offsetWidth; if( Math.abs( position.x + cellWidth - evt.clientX ) > cellWidth / 3 ) { target = target.previousSibling; } } else if( dir === 'v1' && target.parentNode.previousSibling ) { var position = domUtils.getXY( target), cellHeight = target.offsetHeight; if( Math.abs( position.y + cellHeight - evt.clientY ) > cellHeight / 3 ) { target = target.parentNode.previousSibling.firstChild; } } //排除了非td内部以及用于代码高亮部分的td return target && !(editor.fireEvent("excludetable", target) === true) ? target : null; } }; // plugins/table.sort.js /** * Created with JetBrains PhpStorm. * User: Jinqn * Date: 13-10-12 * Time: 上午10:20 * To change this template use File | Settings | File Templates. */ UE.UETable.prototype.sortTable = function (sortByCellIndex, compareFn) { var table = this.table, rows = table.rows, trArray = [], flag = rows[0].cells[0].tagName === "TH", lastRowIndex = 0; if(this.selectedTds.length){ var range = this.cellsRange, len = range.endRowIndex + 1; for (var i = range.beginRowIndex; i < len; i++) { trArray[i] = rows[i]; } trArray.splice(0,range.beginRowIndex); lastRowIndex = (range.endRowIndex +1) === this.rowsNum ? 0 : range.endRowIndex +1; }else{ for (var i = 0,len = rows.length; i < len; i++) { trArray[i] = rows[i]; } } var Fn = { 'reversecurrent': function(td1,td2){ return 1; }, 'orderbyasc': function(td1,td2){ var value1 = td1.innerText||td1.textContent, value2 = td2.innerText||td2.textContent; return value1.localeCompare(value2); }, 'reversebyasc': function(td1,td2){ var value1 = td1.innerHTML, value2 = td2.innerHTML; return value2.localeCompare(value1); }, 'orderbynum': function(td1,td2){ var value1 = td1[browser.ie ? 'innerText':'textContent'].match(/\d+/), value2 = td2[browser.ie ? 'innerText':'textContent'].match(/\d+/); if(value1) value1 = +value1[0]; if(value2) value2 = +value2[0]; return (value1||0) - (value2||0); }, 'reversebynum': function(td1,td2){ var value1 = td1[browser.ie ? 'innerText':'textContent'].match(/\d+/), value2 = td2[browser.ie ? 'innerText':'textContent'].match(/\d+/); if(value1) value1 = +value1[0]; if(value2) value2 = +value2[0]; return (value2||0) - (value1||0); } }; //对表格设置排序的标记data-sort-type table.setAttribute('data-sort-type', compareFn && typeof compareFn === "string" && Fn[compareFn] ? compareFn:''); //th不参与排序 flag && trArray.splice(0, 1); trArray = utils.sort(trArray,function (tr1, tr2) { var result; if (compareFn && typeof compareFn === "function") { result = compareFn.call(this, tr1.cells[sortByCellIndex], tr2.cells[sortByCellIndex]); } else if (compareFn && typeof compareFn === "number") { result = 1; } else if (compareFn && typeof compareFn === "string" && Fn[compareFn]) { result = Fn[compareFn].call(this, tr1.cells[sortByCellIndex], tr2.cells[sortByCellIndex]); } else { result = Fn['orderbyasc'].call(this, tr1.cells[sortByCellIndex], tr2.cells[sortByCellIndex]); } return result; }); var fragment = table.ownerDocument.createDocumentFragment(); for (var j = 0, len = trArray.length; j < len; j++) { fragment.appendChild(trArray[j]); } var tbody = table.getElementsByTagName("tbody")[0]; if(!lastRowIndex){ tbody.appendChild(fragment); }else{ tbody.insertBefore(fragment,rows[lastRowIndex- range.endRowIndex + range.beginRowIndex - 1]) } }; UE.plugins['tablesort'] = function () { var me = this, UT = UE.UETable, getUETable = function (tdOrTable) { return UT.getUETable(tdOrTable); }, getTableItemsByRange = function (editor) { return UT.getTableItemsByRange(editor); }; me.ready(function () { //添加表格可排序的样式 utils.cssRule('tablesort', 'table.sortEnabled tr.firstRow th,table.sortEnabled tr.firstRow td{padding-right:20px;background-repeat: no-repeat;background-position: center right;' + ' background-image:url(' + me.options.themePath + me.options.theme + '/images/sortable.png);}', me.document); //做单元格合并操作时,清除可排序标识 me.addListener("afterexeccommand", function (type, cmd) { if( cmd == 'mergeright' || cmd == 'mergedown' || cmd == 'mergecells') { this.execCommand('disablesort'); } }); }); //表格排序 UE.commands['sorttable'] = { queryCommandState: function () { var me = this, tableItems = getTableItemsByRange(me); if (!tableItems.cell) return -1; var table = tableItems.table, cells = table.getElementsByTagName("td"); for (var i = 0, cell; cell = cells[i++];) { if (cell.rowSpan != 1 || cell.colSpan != 1) return -1; } return 0; }, execCommand: function (cmd, fn) { var me = this, range = me.selection.getRange(), bk = range.createBookmark(true), tableItems = getTableItemsByRange(me), cell = tableItems.cell, ut = getUETable(tableItems.table), cellInfo = ut.getCellInfo(cell); ut.sortTable(cellInfo.cellIndex, fn); range.moveToBookmark(bk); try{ range.select(); }catch(e){} } }; //设置表格可排序,清除表格可排序 UE.commands["enablesort"] = UE.commands["disablesort"] = { queryCommandState: function (cmd) { var table = getTableItemsByRange(this).table; if(table && cmd=='enablesort') { var cells = domUtils.getElementsByTagName(table, 'th td'); for(var i = 0; i1 || cells[i].getAttribute('rowspan')>1) return -1; } } return !table ? -1: cmd=='enablesort' ^ table.getAttribute('data-sort')!='sortEnabled' ? -1:0; }, execCommand: function (cmd) { var table = getTableItemsByRange(this).table; table.setAttribute("data-sort", cmd == "enablesort" ? "sortEnabled" : "sortDisabled"); cmd == "enablesort" ? domUtils.addClass(table,"sortEnabled"):domUtils.removeClasses(table,"sortEnabled"); } }; }; // plugins/contextmenu.js ///import core ///commands 右键菜单 ///commandsName ContextMenu ///commandsTitle 右键菜单 /** * 右键菜单 * @function * @name baidu.editor.plugins.contextmenu * @author zhanyi */ UE.plugins['contextmenu'] = function () { var me = this; me.setOpt('enableContextMenu',true); if(me.getOpt('enableContextMenu') === false){ return; } var lang = me.getLang( "contextMenu" ), menu, items = me.options.contextMenu || [ {label:lang['selectall'], cmdName:'selectall'}, { label:lang.cleardoc, cmdName:'cleardoc', exec:function () { if ( confirm( lang.confirmclear ) ) { this.execCommand( 'cleardoc' ); } } }, '-', { label:lang.unlink, cmdName:'unlink' }, '-', { group:lang.paragraph, icon:'justifyjustify', subMenu:[ { label:lang.justifyleft, cmdName:'justify', value:'left' }, { label:lang.justifyright, cmdName:'justify', value:'right' }, { label:lang.justifycenter, cmdName:'justify', value:'center' }, { label:lang.justifyjustify, cmdName:'justify', value:'justify' } ] }, '-', { group:lang.table, icon:'table', subMenu:[ { label:lang.inserttable, cmdName:'inserttable' }, { label:lang.deletetable, cmdName:'deletetable' }, '-', { label:lang.deleterow, cmdName:'deleterow' }, { label:lang.deletecol, cmdName:'deletecol' }, { label:lang.insertcol, cmdName:'insertcol' }, { label:lang.insertcolnext, cmdName:'insertcolnext' }, { label:lang.insertrow, cmdName:'insertrow' }, { label:lang.insertrownext, cmdName:'insertrownext' }, '-', { label:lang.insertcaption, cmdName:'insertcaption' }, { label:lang.deletecaption, cmdName:'deletecaption' }, { label:lang.inserttitle, cmdName:'inserttitle' }, { label:lang.deletetitle, cmdName:'deletetitle' }, { label:lang.inserttitlecol, cmdName:'inserttitlecol' }, { label:lang.deletetitlecol, cmdName:'deletetitlecol' }, '-', { label:lang.mergecells, cmdName:'mergecells' }, { label:lang.mergeright, cmdName:'mergeright' }, { label:lang.mergedown, cmdName:'mergedown' }, '-', { label:lang.splittorows, cmdName:'splittorows' }, { label:lang.splittocols, cmdName:'splittocols' }, { label:lang.splittocells, cmdName:'splittocells' }, '-', { label:lang.averageDiseRow, cmdName:'averagedistributerow' }, { label:lang.averageDisCol, cmdName:'averagedistributecol' }, '-', { label:lang.edittd, cmdName:'edittd', exec:function () { if ( UE.ui['edittd'] ) { new UE.ui['edittd']( this ); } this.getDialog('edittd').open(); } }, { label:lang.edittable, cmdName:'edittable', exec:function () { if ( UE.ui['edittable'] ) { new UE.ui['edittable']( this ); } this.getDialog('edittable').open(); } }, { label:lang.setbordervisible, cmdName:'setbordervisible' } ] }, { group:lang.tablesort, icon:'tablesort', subMenu:[ { label:lang.enablesort, cmdName:'enablesort' }, { label:lang.disablesort, cmdName:'disablesort' }, '-', { label:lang.reversecurrent, cmdName:'sorttable', value:'reversecurrent' }, { label:lang.orderbyasc, cmdName:'sorttable', value:'orderbyasc' }, { label:lang.reversebyasc, cmdName:'sorttable', value:'reversebyasc' }, { label:lang.orderbynum, cmdName:'sorttable', value:'orderbynum' }, { label:lang.reversebynum, cmdName:'sorttable', value:'reversebynum' } ] }, { group:lang.borderbk, icon:'borderBack', subMenu:[ { label:lang.setcolor, cmdName:"interlacetable", exec:function(){ this.execCommand("interlacetable"); } }, { label:lang.unsetcolor, cmdName:"uninterlacetable", exec:function(){ this.execCommand("uninterlacetable"); } }, { label:lang.setbackground, cmdName:"settablebackground", exec:function(){ this.execCommand("settablebackground",{repeat:true,colorList:["#bbb","#ccc"]}); } }, { label:lang.unsetbackground, cmdName:"cleartablebackground", exec:function(){ this.execCommand("cleartablebackground"); } }, { label:lang.redandblue, cmdName:"settablebackground", exec:function(){ this.execCommand("settablebackground",{repeat:true,colorList:["red","blue"]}); } }, { label:lang.threecolorgradient, cmdName:"settablebackground", exec:function(){ this.execCommand("settablebackground",{repeat:true,colorList:["#aaa","#bbb","#ccc"]}); } } ] }, { group:lang.aligntd, icon:'aligntd', subMenu:[ { cmdName:'cellalignment', value:{align:'left',vAlign:'top'} }, { cmdName:'cellalignment', value:{align:'center',vAlign:'top'} }, { cmdName:'cellalignment', value:{align:'right',vAlign:'top'} }, { cmdName:'cellalignment', value:{align:'left',vAlign:'middle'} }, { cmdName:'cellalignment', value:{align:'center',vAlign:'middle'} }, { cmdName:'cellalignment', value:{align:'right',vAlign:'middle'} }, { cmdName:'cellalignment', value:{align:'left',vAlign:'bottom'} }, { cmdName:'cellalignment', value:{align:'center',vAlign:'bottom'} }, { cmdName:'cellalignment', value:{align:'right',vAlign:'bottom'} } ] }, { group:lang.aligntable, icon:'aligntable', subMenu:[ { cmdName:'tablealignment', className: 'left', label:lang.tableleft, value:"left" }, { cmdName:'tablealignment', className: 'center', label:lang.tablecenter, value:"center" }, { cmdName:'tablealignment', className: 'right', label:lang.tableright, value:"right" } ] }, '-', { label:lang.insertparagraphbefore, cmdName:'insertparagraph', value:true }, { label:lang.insertparagraphafter, cmdName:'insertparagraph' }, { label:lang['copy'], cmdName:'copy' }, { label:lang['paste'], cmdName:'paste' } ]; if ( !items.length ) { return; } var uiUtils = UE.ui.uiUtils; me.addListener( 'contextmenu', function ( type, evt ) { var offset = uiUtils.getViewportOffsetByEvent( evt ); me.fireEvent( 'beforeselectionchange' ); if ( menu ) { menu.destroy(); } for ( var i = 0, ti, contextItems = []; ti = items[i]; i++ ) { var last; (function ( item ) { if ( item == '-' ) { if ( (last = contextItems[contextItems.length - 1 ] ) && last !== '-' ) { contextItems.push( '-' ); } } else if ( item.hasOwnProperty( "group" ) ) { for ( var j = 0, cj, subMenu = []; cj = item.subMenu[j]; j++ ) { (function ( subItem ) { if ( subItem == '-' ) { if ( (last = subMenu[subMenu.length - 1 ] ) && last !== '-' ) { subMenu.push( '-' ); }else{ subMenu.splice(subMenu.length-1); } } else { if ( (me.commands[subItem.cmdName] || UE.commands[subItem.cmdName] || subItem.query) && (subItem.query ? subItem.query() : me.queryCommandState( subItem.cmdName )) > -1 ) { subMenu.push( { 'label':subItem.label || me.getLang( "contextMenu." + subItem.cmdName + (subItem.value || '') )||"", 'className':'edui-for-' +subItem.cmdName + ( subItem.className ? ( ' edui-for-' + subItem.cmdName + '-' + subItem.className ) : '' ), onclick:subItem.exec ? function () { subItem.exec.call( me ); } : function () { me.execCommand( subItem.cmdName, subItem.value ); } } ); } } })( cj ); } if ( subMenu.length ) { function getLabel(){ switch (item.icon){ case "table": return me.getLang( "contextMenu.table" ); case "justifyjustify": return me.getLang( "contextMenu.paragraph" ); case "aligntd": return me.getLang("contextMenu.aligntd"); case "aligntable": return me.getLang("contextMenu.aligntable"); case "tablesort": return lang.tablesort; case "borderBack": return lang.borderbk; default : return ''; } } contextItems.push( { //todo 修正成自动获取方式 'label':getLabel(), className:'edui-for-' + item.icon, 'subMenu':{ items:subMenu, editor:me } } ); } } else { //有可能commmand没有加载右键不能出来,或者没有command也想能展示出来添加query方法 if ( (me.commands[item.cmdName] || UE.commands[item.cmdName] || item.query) && (item.query ? item.query.call(me) : me.queryCommandState( item.cmdName )) > -1 ) { contextItems.push( { 'label':item.label || me.getLang( "contextMenu." + item.cmdName ), className:'edui-for-' + (item.icon ? item.icon : item.cmdName + (item.value || '')), onclick:item.exec ? function () { item.exec.call( me ); } : function () { me.execCommand( item.cmdName, item.value ); } } ); } } })( ti ); } if ( contextItems[contextItems.length - 1] == '-' ) { contextItems.pop(); } menu = new UE.ui.Menu( { items:contextItems, className:"edui-contextmenu", editor:me } ); menu.render(); menu.showAt( offset ); me.fireEvent("aftershowcontextmenu",menu); domUtils.preventDefault( evt ); if ( browser.ie ) { var ieRange; try { ieRange = me.selection.getNative().createRange(); } catch ( e ) { return; } if ( ieRange.item ) { var range = new dom.Range( me.document ); range.selectNode( ieRange.item( 0 ) ).select( true, true ); } } }); // 添加复制的flash按钮 me.addListener('aftershowcontextmenu', function(type, menu) { if (me.zeroclipboard) { var items = menu.items; for (var key in items) { if (items[key].className == 'edui-for-copy') { me.zeroclipboard.clip(items[key].getDom()); } } } }); }; // plugins/shortcutmenu.js ///import core ///commands 弹出菜单 // commandsName popupmenu ///commandsTitle 弹出菜单 /** * 弹出菜单 * @function * @name baidu.editor.plugins.popupmenu * @author xuheng */ UE.plugins['shortcutmenu'] = function () { var me = this, menu, items = me.options.shortcutMenu || []; if (!items.length) { return; } me.addListener ('contextmenu mouseup' , function (type , e) { var me = this, customEvt = { type : type , target : e.target || e.srcElement , screenX : e.screenX , screenY : e.screenY , clientX : e.clientX , clientY : e.clientY }; setTimeout (function () { var rng = me.selection.getRange (); if (rng.collapsed === false || type == "contextmenu") { if (!menu) { menu = new baidu.editor.ui.ShortCutMenu ({ editor : me , items : items , theme : me.options.theme , className : 'edui-shortcutmenu' }); menu.render (); me.fireEvent ("afterrendershortcutmenu" , menu); } menu.show (customEvt , !!UE.plugins['contextmenu']); } }); if (type == 'contextmenu') { domUtils.preventDefault (e); if (browser.ie9below) { var ieRange; try { ieRange = me.selection.getNative().createRange(); } catch (e) { return; } if (ieRange.item) { var range = new dom.Range (me.document); range.selectNode (ieRange.item (0)).select (true , true); } } } }); me.addListener ('keydown' , function (type) { if (type == "keydown") { menu && !menu.isHidden && menu.hide (); } }); }; // plugins/basestyle.js /** * B、I、sub、super命令支持 * @file * @since 1.2.6.1 */ UE.plugins['basestyle'] = function(){ /** * 字体加粗 * @command bold * @param { String } cmd 命令字符串 * @remind 对已加粗的文本内容执行该命令, 将取消加粗 * @method execCommand * @example * ```javascript * //editor是编辑器实例 * //对当前选中的文本内容执行加粗操作 * //第一次执行, 文本内容加粗 * editor.execCommand( 'bold' ); * * //第二次执行, 文本内容取消加粗 * editor.execCommand( 'bold' ); * ``` */ /** * 字体倾斜 * @command italic * @method execCommand * @param { String } cmd 命令字符串 * @remind 对已倾斜的文本内容执行该命令, 将取消倾斜 * @example * ```javascript * //editor是编辑器实例 * //对当前选中的文本内容执行斜体操作 * //第一次操作, 文本内容将变成斜体 * editor.execCommand( 'italic' ); * * //再次对同一文本内容执行, 则文本内容将恢复正常 * editor.execCommand( 'italic' ); * ``` */ /** * 下标文本,与“superscript”命令互斥 * @command subscript * @method execCommand * @remind 把选中的文本内容切换成下标文本, 如果当前选中的文本已经是下标, 则该操作会把文本内容还原成正常文本 * @param { String } cmd 命令字符串 * @example * ```javascript * //editor是编辑器实例 * //对当前选中的文本内容执行下标操作 * //第一次操作, 文本内容将变成下标文本 * editor.execCommand( 'subscript' ); * * //再次对同一文本内容执行, 则文本内容将恢复正常 * editor.execCommand( 'subscript' ); * ``` */ /** * 上标文本,与“subscript”命令互斥 * @command superscript * @method execCommand * @remind 把选中的文本内容切换成上标文本, 如果当前选中的文本已经是上标, 则该操作会把文本内容还原成正常文本 * @param { String } cmd 命令字符串 * @example * ```javascript * //editor是编辑器实例 * //对当前选中的文本内容执行上标操作 * //第一次操作, 文本内容将变成上标文本 * editor.execCommand( 'superscript' ); * * //再次对同一文本内容执行, 则文本内容将恢复正常 * editor.execCommand( 'superscript' ); * ``` */ var basestyles = { 'bold':['strong','b'], 'italic':['em','i'], 'subscript':['sub'], 'superscript':['sup'] }, getObj = function(editor,tagNames){ return domUtils.filterNodeList(editor.selection.getStartElementPath(),tagNames); }, me = this; //添加快捷键 me.addshortcutkey({ "Bold" : "ctrl+66",//^B "Italic" : "ctrl+73", //^I "Underline" : "ctrl+85"//^U }); me.addInputRule(function(root){ utils.each(root.getNodesByTagName('b i'),function(node){ switch (node.tagName){ case 'b': node.tagName = 'strong'; break; case 'i': node.tagName = 'em'; } }); }); for ( var style in basestyles ) { (function( cmd, tagNames ) { me.commands[cmd] = { execCommand : function( cmdName ) { var range = me.selection.getRange(),obj = getObj(this,tagNames); if ( range.collapsed ) { if ( obj ) { var tmpText = me.document.createTextNode(''); range.insertNode( tmpText ).removeInlineStyle( tagNames ); range.setStartBefore(tmpText); domUtils.remove(tmpText); } else { var tmpNode = range.document.createElement( tagNames[0] ); if(cmdName == 'superscript' || cmdName == 'subscript'){ tmpText = me.document.createTextNode(''); range.insertNode(tmpText) .removeInlineStyle(['sub','sup']) .setStartBefore(tmpText) .collapse(true); } range.insertNode( tmpNode ).setStart( tmpNode, 0 ); } range.collapse( true ); } else { if(cmdName == 'superscript' || cmdName == 'subscript'){ if(!obj || obj.tagName.toLowerCase() != cmdName){ range.removeInlineStyle(['sub','sup']); } } obj ? range.removeInlineStyle( tagNames ) : range.applyInlineStyle( tagNames[0] ); } range.select(); }, queryCommandState : function() { return getObj(this,tagNames) ? 1 : 0; } }; })( style, basestyles[style] ); } }; // plugins/elementpath.js /** * 选取路径命令 * @file */ UE.plugins['elementpath'] = function(){ var currentLevel, tagNames, me = this; me.setOpt('elementPathEnabled',true); if(!me.options.elementPathEnabled){ return; } me.commands['elementpath'] = { execCommand : function( cmdName, level ) { var start = tagNames[level], range = me.selection.getRange(); currentLevel = level*1; range.selectNode(start).select(); }, queryCommandValue : function() { //产生一个副本,不能修改原来的startElementPath; var parents = [].concat(this.selection.getStartElementPath()).reverse(), names = []; tagNames = parents; for(var i=0,ci;ci=parents[i];i++){ if(ci.nodeType == 3) { continue; } var name = ci.tagName.toLowerCase(); if(name == 'img' && ci.getAttribute('anchorname')){ name = 'anchor'; } names[i] = name; if(currentLevel == i){ currentLevel = -1; break; } } return names; } }; }; // plugins/formatmatch.js /** * 格式刷,只格式inline的 * @file * @since 1.2.6.1 */ /** * 格式刷 * @command formatmatch * @method execCommand * @remind 该操作不能复制段落格式 * @param { String } cmd 命令字符串 * @example * ```javascript * //editor是编辑器实例 * //获取格式刷 * editor.execCommand( 'formatmatch' ); * ``` */ UE.plugins['formatmatch'] = function(){ var me = this, list = [],img, flag = 0; me.addListener('reset',function(){ list = []; flag = 0; }); function addList(type,evt){ if(browser.webkit){ var target = evt.target.tagName == 'IMG' ? evt.target : null; } function addFormat(range){ if(text){ range.selectNode(text); } return range.applyInlineStyle(list[list.length-1].tagName,null,list); } me.undoManger && me.undoManger.save(); var range = me.selection.getRange(), imgT = target || range.getClosedNode(); if(img && imgT && imgT.tagName == 'IMG'){ //trace:964 imgT.style.cssText += ';float:' + (img.style.cssFloat || img.style.styleFloat ||'none') + ';display:' + (img.style.display||'inline'); img = null; }else{ if(!img){ var collapsed = range.collapsed; if(collapsed){ var text = me.document.createTextNode('match'); range.insertNode(text).select(); } me.__hasEnterExecCommand = true; //不能把block上的属性干掉 //trace:1553 var removeFormatAttributes = me.options.removeFormatAttributes; me.options.removeFormatAttributes = ''; me.execCommand('removeformat'); me.options.removeFormatAttributes = removeFormatAttributes; me.__hasEnterExecCommand = false; //trace:969 range = me.selection.getRange(); if(list.length){ addFormat(range); } if(text){ range.setStartBefore(text).collapse(true); } range.select(); text && domUtils.remove(text); } } me.undoManger && me.undoManger.save(); me.removeListener('mouseup',addList); flag = 0; } me.commands['formatmatch'] = { execCommand : function( cmdName ) { if(flag){ flag = 0; list = []; me.removeListener('mouseup',addList); return; } var range = me.selection.getRange(); img = range.getClosedNode(); if(!img || img.tagName != 'IMG'){ range.collapse(true).shrinkBoundary(); var start = range.startContainer; list = domUtils.findParents(start,true,function(node){ return !domUtils.isBlockElm(node) && node.nodeType == 1; }); //a不能加入格式刷, 并且克隆节点 for(var i=0,ci;ci=list[i];i++){ if(ci.tagName == 'A'){ list.splice(i,1); break; } } } me.addListener('mouseup',addList); flag = 1; }, queryCommandState : function() { return flag; }, notNeedUndo : 1 }; }; // plugins/searchreplace.js ///import core ///commands 查找替换 ///commandsName SearchReplace ///commandsTitle 查询替换 ///commandsDialog dialogs\searchreplace /** * @description 查找替换 * @author zhanyi */ UE.plugin.register('searchreplace',function(){ var me = this; var _blockElm = {'table':1,'tbody':1,'tr':1,'ol':1,'ul':1}; function findTextInString(textContent,opt,currentIndex){ var str = opt.searchStr; if(opt.dir == -1){ textContent = textContent.split('').reverse().join(''); str = str.split('').reverse().join(''); currentIndex = textContent.length - currentIndex; } var reg = new RegExp(str,'g' + (opt.casesensitive ? '' : 'i')),match; while(match = reg.exec(textContent)){ if(match.index >= currentIndex){ return opt.dir == -1 ? textContent.length - match.index - opt.searchStr.length : match.index; } } return -1 } function findTextBlockElm(node,currentIndex,opt){ var textContent,index,methodName = opt.all || opt.dir == 1 ? 'getNextDomNode' : 'getPreDomNode'; if(domUtils.isBody(node)){ node = node.firstChild; } var first = 1; while(node){ textContent = node.nodeType == 3 ? node.nodeValue : node[browser.ie ? 'innerText' : 'textContent']; index = findTextInString(textContent,opt,currentIndex ); first = 0; if(index!=-1){ return { 'node':node, 'index':index } } node = domUtils[methodName](node); while(node && _blockElm[node.nodeName.toLowerCase()]){ node = domUtils[methodName](node,true); } if(node){ currentIndex = opt.dir == -1 ? (node.nodeType == 3 ? node.nodeValue : node[browser.ie ? 'innerText' : 'textContent']).length : 0; } } } function findNTextInBlockElm(node,index,str){ var currentIndex = 0, currentNode = node.firstChild, currentNodeLength = 0, result; while(currentNode){ if(currentNode.nodeType == 3){ currentNodeLength = currentNode.nodeValue.replace(/(^[\t\r\n]+)|([\t\r\n]+$)/,'').length; currentIndex += currentNodeLength; if(currentIndex >= index){ return { 'node':currentNode, 'index': currentNodeLength - (currentIndex - index) } } }else if(!dtd.$empty[currentNode.tagName]){ currentNodeLength = currentNode[browser.ie ? 'innerText' : 'textContent'].replace(/(^[\t\r\n]+)|([\t\r\n]+$)/,'').length currentIndex += currentNodeLength; if(currentIndex >= index){ result = findNTextInBlockElm(currentNode,currentNodeLength - (currentIndex - index),str); if(result){ return result; } } } currentNode = domUtils.getNextDomNode(currentNode); } } function searchReplace(me,opt){ var rng = me.selection.getRange(), startBlockNode, searchStr = opt.searchStr, span = me.document.createElement('span'); span.innerHTML = '$$ueditor_searchreplace_key$$'; rng.shrinkBoundary(true); //判断是不是第一次选中 if(!rng.collapsed){ rng.select(); var rngText = me.selection.getText(); if(new RegExp('^' + opt.searchStr + '$',(opt.casesensitive ? '' : 'i')).test(rngText)){ if(opt.replaceStr != undefined){ replaceText(rng,opt.replaceStr); rng.select(); return true; }else{ rng.collapse(opt.dir == -1) } } } rng.insertNode(span); rng.enlargeToBlockElm(true); startBlockNode = rng.startContainer; var currentIndex = startBlockNode[browser.ie ? 'innerText' : 'textContent'].indexOf('$$ueditor_searchreplace_key$$'); rng.setStartBefore(span); domUtils.remove(span); var result = findTextBlockElm(startBlockNode,currentIndex,opt); if(result){ var rngStart = findNTextInBlockElm(result.node,result.index,searchStr); var rngEnd = findNTextInBlockElm(result.node,result.index + searchStr.length,searchStr); rng.setStart(rngStart.node,rngStart.index).setEnd(rngEnd.node,rngEnd.index); if(opt.replaceStr !== undefined){ replaceText(rng,opt.replaceStr) } rng.select(); return true; }else{ rng.setCursor() } } function replaceText(rng,str){ str = me.document.createTextNode(str); rng.deleteContents().insertNode(str); } return { commands:{ 'searchreplace':{ execCommand:function(cmdName,opt){ utils.extend(opt,{ all : false, casesensitive : false, dir : 1 },true); var num = 0; if(opt.all){ var rng = me.selection.getRange(), first = me.body.firstChild; if(first && first.nodeType == 1){ rng.setStart(first,0); rng.shrinkBoundary(true); }else if(first.nodeType == 3){ rng.setStartBefore(first) } rng.collapse(true).select(true); if(opt.replaceStr !== undefined){ me.fireEvent('saveScene'); } while(searchReplace(this,opt)){ num++; } if(num){ me.fireEvent('saveScene'); } }else{ if(opt.replaceStr !== undefined){ me.fireEvent('saveScene'); } if(searchReplace(this,opt)){ num++ } if(num){ me.fireEvent('saveScene'); } } return num; }, notNeedUndo:1 } } } }); // plugins/customstyle.js /** * 自定义样式 * @file * @since 1.2.6.1 */ /** * 根据config配置文件里“customstyle”选项的值对匹配的标签执行样式替换。 * @command customstyle * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand( 'customstyle' ); * ``` */ UE.plugins['customstyle'] = function() { var me = this; me.setOpt({ 'customstyle':[ {tag:'h1',name:'tc', style:'font-size:32px;font-weight:bold;border-bottom:#ccc 2px solid;padding:0 4px 0 0;text-align:center;margin:0 0 20px 0;'}, {tag:'h1',name:'tl', style:'font-size:32px;font-weight:bold;border-bottom:#ccc 2px solid;padding:0 4px 0 0;text-align:left;margin:0 0 10px 0;'}, {tag:'span',name:'im', style:'font-size:16px;font-style:italic;font-weight:bold;line-height:18px;'}, {tag:'span',name:'hi', style:'font-size:16px;font-style:italic;font-weight:bold;color:rgb(51, 153, 204);line-height:18px;'} ]}); me.commands['customstyle'] = { execCommand : function(cmdName, obj) { var me = this, tagName = obj.tag, node = domUtils.findParent(me.selection.getStart(), function(node) { return node.getAttribute('label'); }, true), range,bk,tmpObj = {}; for (var p in obj) { if(obj[p]!==undefined) tmpObj[p] = obj[p]; } delete tmpObj.tag; if (node && node.getAttribute('label') == obj.label) { range = this.selection.getRange(); bk = range.createBookmark(); if (range.collapsed) { //trace:1732 删掉自定义标签,要有p来回填站位 if(dtd.$block[node.tagName]){ var fillNode = me.document.createElement('p'); domUtils.moveChild(node, fillNode); node.parentNode.insertBefore(fillNode, node); domUtils.remove(node); }else{ domUtils.remove(node,true); } } else { var common = domUtils.getCommonAncestor(bk.start, bk.end), nodes = domUtils.getElementsByTagName(common, tagName); if(new RegExp(tagName,'i').test(common.tagName)){ nodes.push(common); } for (var i = 0,ni; ni = nodes[i++];) { if (ni.getAttribute('label') == obj.label) { var ps = domUtils.getPosition(ni, bk.start),pe = domUtils.getPosition(ni, bk.end); if ((ps & domUtils.POSITION_FOLLOWING || ps & domUtils.POSITION_CONTAINS) && (pe & domUtils.POSITION_PRECEDING || pe & domUtils.POSITION_CONTAINS) ) if (dtd.$block[tagName]) { var fillNode = me.document.createElement('p'); domUtils.moveChild(ni, fillNode); ni.parentNode.insertBefore(fillNode, ni); } domUtils.remove(ni, true); } } node = domUtils.findParent(common, function(node) { return node.getAttribute('label') == obj.label; }, true); if (node) { domUtils.remove(node, true); } } range.moveToBookmark(bk).select(); } else { if (dtd.$block[tagName]) { this.execCommand('paragraph', tagName, tmpObj,'customstyle'); range = me.selection.getRange(); if (!range.collapsed) { range.collapse(); node = domUtils.findParent(me.selection.getStart(), function(node) { return node.getAttribute('label') == obj.label; }, true); var pNode = me.document.createElement('p'); domUtils.insertAfter(node, pNode); domUtils.fillNode(me.document, pNode); range.setStart(pNode, 0).setCursor(); } } else { range = me.selection.getRange(); if (range.collapsed) { node = me.document.createElement(tagName); domUtils.setAttributes(node, tmpObj); range.insertNode(node).setStart(node, 0).setCursor(); return; } bk = range.createBookmark(); range.applyInlineStyle(tagName, tmpObj).moveToBookmark(bk).select(); } } }, queryCommandValue : function() { var parent = domUtils.filterNodeList( this.selection.getStartElementPath(), function(node){return node.getAttribute('label')} ); return parent ? parent.getAttribute('label') : ''; } }; //当去掉customstyle是,如果是块元素,用p代替 me.addListener('keyup', function(type, evt) { var keyCode = evt.keyCode || evt.which; if (keyCode == 32 || keyCode == 13) { var range = me.selection.getRange(); if (range.collapsed) { var node = domUtils.findParent(me.selection.getStart(), function(node) { return node.getAttribute('label'); }, true); if (node && dtd.$block[node.tagName] && domUtils.isEmptyNode(node)) { var p = me.document.createElement('p'); domUtils.insertAfter(node, p); domUtils.fillNode(me.document, p); domUtils.remove(node); range.setStart(p, 0).setCursor(); } } } }); }; // plugins/catchremoteimage.js ///import core ///commands 远程图片抓取 ///commandsName catchRemoteImage,catchremoteimageenable ///commandsTitle 远程图片抓取 /** * 远程图片抓取,当开启本插件时所有不符合本地域名的图片都将被抓取成为本地服务器上的图片 */ UE.plugins['catchremoteimage'] = function () { var me = this, ajax = UE.ajax; /* 设置默认值 */ if (me.options.catchRemoteImageEnable === false) return; me.setOpt({ catchRemoteImageEnable: false }); me.addListener("afterpaste", function () { me.fireEvent("catchRemoteImage"); }); me.addListener("catchRemoteImage", function () { var catcherLocalDomain = me.getOpt('catcherLocalDomain'), catcherActionUrl = me.getActionUrl(me.getOpt('catcherActionName')), catcherUrlPrefix = me.getOpt('catcherUrlPrefix'), catcherFieldName = me.getOpt('catcherFieldName'); var remoteImages = [], imgs = domUtils.getElementsByTagName(me.document, "img"), test = function (src, urls) { if (src.indexOf(location.host) != -1 || /(^\.)|(^\/)/.test(src)) { return true; } if (urls) { for (var j = 0, url; url = urls[j++];) { if (src.indexOf(url) !== -1) { return true; } } } return false; }; for (var i = 0, ci; ci = imgs[i++];) { if (ci.getAttribute("word_img")) { continue; } var src = ci.getAttribute("_src") || ci.src || ""; if (/^(https?|ftp):/i.test(src) && !test(src, catcherLocalDomain)) { remoteImages.push(src); } } if (remoteImages.length) { catchremoteimage(remoteImages, { //成功抓取 success: function (r) { try { var info = r.state !== undefined ? r:eval("(" + r.responseText + ")"); } catch (e) { return; } /* 获取源路径和新路径 */ var i, j, ci, cj, oldSrc, newSrc, list = info.list; for (i = 0; ci = imgs[i++];) { oldSrc = ci.getAttribute("_src") || ci.src || ""; for (j = 0; cj = list[j++];) { if (oldSrc == cj.source && cj.state == "SUCCESS") { //抓取失败时不做替换处理 newSrc = catcherUrlPrefix + cj.url; domUtils.setAttributes(ci, { "src": newSrc, "_src": newSrc }); break; } } } me.fireEvent('catchremotesuccess') }, //回调失败,本次请求超时 error: function () { me.fireEvent("catchremoteerror"); } }); } function catchremoteimage(imgs, callbacks) { var params = utils.serializeParam(me.queryCommandValue('serverparam')) || '', url = utils.formatUrl(catcherActionUrl + (catcherActionUrl.indexOf('?') == -1 ? '?':'&') + params), isJsonp = utils.isCrossDomainUrl(url), opt = { 'method': 'POST', 'dataType': isJsonp ? 'jsonp':'', 'timeout': 60000, //单位:毫秒,回调请求超时设置。目标用户如果网速不是很快的话此处建议设置一个较大的数值 'onsuccess': callbacks["success"], 'onerror': callbacks["error"] }; opt[catcherFieldName] = imgs; ajax.request(url, opt); } }); }; // plugins/snapscreen.js /** * 截屏插件,为UEditor提供插入支持 * @file * @since 1.4.2 */ UE.plugin.register('snapscreen', function (){ var me = this; var snapplugin; function getLocation(url){ var search, a = document.createElement('a'), params = utils.serializeParam(me.queryCommandValue('serverparam')) || ''; a.href = url; if (browser.ie) { a.href = a.href; } search = a.search; if (params) { search = search + (search.indexOf('?') == -1 ? '?':'&')+ params; search = search.replace(/[&]+/ig, '&'); } return { 'port': a.port, 'hostname': a.hostname, 'path': a.pathname + search || + a.hash } } return { commands:{ /** * 字体背景颜色 * @command snapscreen * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand('snapscreen'); * ``` */ 'snapscreen':{ execCommand:function (cmd) { var url, local, res; var lang = me.getLang("snapScreen_plugin"); if(!snapplugin){ var container = me.container; var doc = me.container.ownerDocument || me.container.document; snapplugin = doc.createElement("object"); try{snapplugin.type = "application/x-pluginbaidusnap";}catch(e){ return; } snapplugin.style.cssText = "position:absolute;left:-9999px;width:0;height:0;"; snapplugin.setAttribute("width","0"); snapplugin.setAttribute("height","0"); container.appendChild(snapplugin); } function onSuccess(rs){ try{ rs = eval("("+ rs +")"); if(rs.state == 'SUCCESS'){ var opt = me.options; me.execCommand('insertimage', { src: opt.snapscreenUrlPrefix + rs.url, _src: opt.snapscreenUrlPrefix + rs.url, alt: rs.title || '', floatStyle: opt.snapscreenImgAlign }); } else { alert(rs.state); } }catch(e){ alert(lang.callBackErrorMsg); } } url = me.getActionUrl(me.getOpt('snapscreenActionName')); local = getLocation(url); setTimeout(function () { try{ res =snapplugin.saveSnapshot(local.hostname, local.path, local.port); }catch(e){ me.ui._dialogs['snapscreenDialog'].open(); return; } onSuccess(res); }, 50); }, queryCommandState: function(){ return (navigator.userAgent.indexOf("Windows",0) != -1) ? 0:-1; } } } } }); // plugins/insertparagraph.js /** * 插入段落 * @file * @since 1.2.6.1 */ /** * 插入段落 * @command insertparagraph * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * //editor是编辑器实例 * editor.execCommand( 'insertparagraph' ); * ``` */ UE.commands['insertparagraph'] = { execCommand : function( cmdName,front) { var me = this, range = me.selection.getRange(), start = range.startContainer,tmpNode; while(start ){ if(domUtils.isBody(start)){ break; } tmpNode = start; start = start.parentNode; } if(tmpNode){ var p = me.document.createElement('p'); if(front){ tmpNode.parentNode.insertBefore(p,tmpNode) }else{ tmpNode.parentNode.insertBefore(p,tmpNode.nextSibling) } domUtils.fillNode(me.document,p); range.setStart(p,0).setCursor(false,true); } } }; // plugins/webapp.js /** * 百度应用 * @file * @since 1.2.6.1 */ /** * 插入百度应用 * @command webapp * @method execCommand * @remind 需要百度APPKey * @remind 百度应用主页: http://app.baidu.com/ * @param { Object } appOptions 应用所需的参数项, 支持的key有: title=>应用标题, width=>应用容器宽度, * height=>应用容器高度,logo=>应用logo,url=>应用地址 * @example * ```javascript * //editor是编辑器实例 * //在编辑器里插入一个“植物大战僵尸”的APP * editor.execCommand( 'webapp' , { * title: '植物大战僵尸', * width: 560, * height: 465, * logo: '应用展示的图片', * url: '百度应用的地址' * } ); * ``` */ //UE.plugins['webapp'] = function () { // var me = this; // function createInsertStr( obj, toIframe, addParagraph ) { // return !toIframe ? // (addParagraph ? '

    ' : '') + '' + // (addParagraph ? '

    ' : '') // : // ''; // } // // function switchImgAndIframe( img2frame ) { // var tmpdiv, // nodes = domUtils.getElementsByTagName( me.document, !img2frame ? "iframe" : "img" ); // for ( var i = 0, node; node = nodes[i++]; ) { // if ( node.className != "edui-faked-webapp" ){ // continue; // } // tmpdiv = me.document.createElement( "div" ); // tmpdiv.innerHTML = createInsertStr( img2frame ? {url:node.getAttribute( "_url" ), width:node.width, height:node.height,title:node.title,logo:node.style.backgroundImage.replace("url(","").replace(")","")} : {url:node.getAttribute( "src", 2 ),title:node.title, width:node.width, height:node.height,logo:node.getAttribute("logo_url")}, img2frame ? true : false,false ); // node.parentNode.replaceChild( tmpdiv.firstChild, node ); // } // } // // me.addListener( "beforegetcontent", function () { // switchImgAndIframe( true ); // } ); // me.addListener( 'aftersetcontent', function () { // switchImgAndIframe( false ); // } ); // me.addListener( 'aftergetcontent', function ( cmdName ) { // if ( cmdName == 'aftergetcontent' && me.queryCommandState( 'source' ) ){ // return; // } // switchImgAndIframe( false ); // } ); // // me.commands['webapp'] = { // execCommand:function ( cmd, obj ) { // me.execCommand( "inserthtml", createInsertStr( obj, false,true ) ); // } // }; //}; UE.plugin.register('webapp', function (){ var me = this; function createInsertStr(obj,toEmbed){ return !toEmbed ? '' : '' } return { outputRule: function(root){ utils.each(root.getNodesByTagName('img'),function(node){ var html; if(node.getAttr('class') == 'edui-faked-webapp'){ html = createInsertStr({ title:node.getAttr('title'), 'width':node.getAttr('width'), 'height':node.getAttr('height'), 'align':node.getAttr('align'), 'cssfloat':node.getStyle('float'), 'url':node.getAttr("_url"), 'logo':node.getAttr('_logo_url') },true); var embed = UE.uNode.createElement(html); node.parentNode.replaceChild(embed,node); } }) }, inputRule:function(root){ utils.each(root.getNodesByTagName('iframe'),function(node){ if(node.getAttr('class') == 'edui-faked-webapp'){ var img = UE.uNode.createElement(createInsertStr({ title:node.getAttr('title'), 'width':node.getAttr('width'), 'height':node.getAttr('height'), 'align':node.getAttr('align'), 'cssfloat':node.getStyle('float'), 'url':node.getAttr("src"), 'logo':node.getAttr('logo_url') })); node.parentNode.replaceChild(img,node); } }) }, commands:{ /** * 插入百度应用 * @command webapp * @method execCommand * @remind 需要百度APPKey * @remind 百度应用主页: http://app.baidu.com/ * @param { Object } appOptions 应用所需的参数项, 支持的key有: title=>应用标题, width=>应用容器宽度, * height=>应用容器高度,logo=>应用logo,url=>应用地址 * @example * ```javascript * //editor是编辑器实例 * //在编辑器里插入一个“植物大战僵尸”的APP * editor.execCommand( 'webapp' , { * title: '植物大战僵尸', * width: 560, * height: 465, * logo: '应用展示的图片', * url: '百度应用的地址' * } ); * ``` */ 'webapp':{ execCommand:function (cmd, obj) { var me = this, str = createInsertStr(utils.extend(obj,{ align:'none' }), false); me.execCommand("inserthtml",str); }, queryCommandState:function () { var me = this, img = me.selection.getRange().getClosedNode(), flag = img && (img.className == "edui-faked-webapp"); return flag ? 1 : 0; } } } } }); // plugins/template.js ///import core ///import plugins\inserthtml.js ///import plugins\cleardoc.js ///commands 模板 ///commandsName template ///commandsTitle 模板 ///commandsDialog dialogs\template UE.plugins['template'] = function () { UE.commands['template'] = { execCommand:function (cmd, obj) { obj.html && this.execCommand("inserthtml", obj.html); } }; this.addListener("click", function (type, evt) { var el = evt.target || evt.srcElement, range = this.selection.getRange(); var tnode = domUtils.findParent(el, function (node) { if (node.className && domUtils.hasClass(node, "ue_t")) { return node; } }, true); tnode && range.selectNode(tnode).shrinkBoundary().select(); }); this.addListener("keydown", function (type, evt) { var range = this.selection.getRange(); if (!range.collapsed) { if (!evt.ctrlKey && !evt.metaKey && !evt.shiftKey && !evt.altKey) { var tnode = domUtils.findParent(range.startContainer, function (node) { if (node.className && domUtils.hasClass(node, "ue_t")) { return node; } }, true); if (tnode) { domUtils.removeClasses(tnode, ["ue_t"]); } } } }); }; // plugins/music.js /** * 插入音乐命令 * @file */ UE.plugin.register('music', function (){ var me = this; function creatInsertStr(url,width,height,align,cssfloat,toEmbed){ return !toEmbed ? '' : ''; } return { outputRule: function(root){ utils.each(root.getNodesByTagName('img'),function(node){ var html; if(node.getAttr('class') == 'edui-faked-music'){ var cssfloat = node.getStyle('float'); var align = node.getAttr('align'); html = creatInsertStr(node.getAttr("_url"), node.getAttr('width'), node.getAttr('height'), align, cssfloat, true); var embed = UE.uNode.createElement(html); node.parentNode.replaceChild(embed,node); } }) }, inputRule:function(root){ utils.each(root.getNodesByTagName('embed'),function(node){ if(node.getAttr('class') == 'edui-faked-music'){ var cssfloat = node.getStyle('float'); var align = node.getAttr('align'); html = creatInsertStr(node.getAttr("src"), node.getAttr('width'), node.getAttr('height'), align, cssfloat,false); var img = UE.uNode.createElement(html); node.parentNode.replaceChild(img,node); } }) }, commands:{ /** * 插入音乐 * @command music * @method execCommand * @param { Object } musicOptions 插入音乐的参数项, 支持的key有: url=>音乐地址; * width=>音乐容器宽度;height=>音乐容器高度;align=>音乐文件的对齐方式, 可选值有: left, center, right, none * @example * ```javascript * //editor是编辑器实例 * //在编辑器里插入一个“植物大战僵尸”的APP * editor.execCommand( 'music' , { * width: 400, * height: 95, * align: "center", * url: "音乐地址" * } ); * ``` */ 'music':{ execCommand:function (cmd, musicObj) { var me = this, str = creatInsertStr(musicObj.url, musicObj.width || 400, musicObj.height || 95, "none", false); me.execCommand("inserthtml",str); }, queryCommandState:function () { var me = this, img = me.selection.getRange().getClosedNode(), flag = img && (img.className == "edui-faked-music"); return flag ? 1 : 0; } } } } }); // plugins/autoupload.js /** * @description * 1.拖放文件到编辑区域,自动上传并插入到选区 * 2.插入粘贴板的图片,自动上传并插入到选区 * @author Jinqn * @date 2013-10-14 */ UE.plugin.register('autoupload', function (){ function sendAndInsertFile(file, editor) { var me = editor; //模拟数据 var fieldName, urlPrefix, maxSize, allowFiles, actionUrl, loadingHtml, errorHandler, successHandler, filetype = /image\/\w+/i.test(file.type) ? 'image':'file', loadingId = 'loading_' + (+new Date()).toString(36); fieldName = me.getOpt(filetype + 'FieldName'); urlPrefix = me.getOpt(filetype + 'UrlPrefix'); maxSize = me.getOpt(filetype + 'MaxSize'); allowFiles = me.getOpt(filetype + 'AllowFiles'); actionUrl = me.getActionUrl(me.getOpt(filetype + 'ActionName')); errorHandler = function(title) { var loader = me.document.getElementById(loadingId); loader && domUtils.remove(loader); me.fireEvent('showmessage', { 'id': loadingId, 'content': title, 'type': 'error', 'timeout': 4000 }); }; if (filetype == 'image') { loadingHtml = ''; successHandler = function(data) { var link = urlPrefix + data.url, loader = me.document.getElementById(loadingId); if (loader) { loader.setAttribute('src', link); loader.setAttribute('_src', link); loader.setAttribute('title', data.title || ''); loader.setAttribute('alt', data.original || ''); loader.removeAttribute('id'); domUtils.removeClasses(loader, 'loadingclass'); } }; } else { loadingHtml = '

    ' + '' + '

    '; successHandler = function(data) { var link = urlPrefix + data.url, loader = me.document.getElementById(loadingId); var rng = me.selection.getRange(), bk = rng.createBookmark(); rng.selectNode(loader).select(); me.execCommand('insertfile', {'url': link}); rng.moveToBookmark(bk).select(); }; } /* 插入loading的占位符 */ me.execCommand('inserthtml', loadingHtml); /* 判断后端配置是否没有加载成功 */ if (!me.getOpt(filetype + 'ActionName')) { errorHandler(me.getLang('autoupload.errorLoadConfig')); return; } /* 判断文件大小是否超出限制 */ if(file.size > maxSize) { errorHandler(me.getLang('autoupload.exceedSizeError')); return; } /* 判断文件格式是否超出允许 */ var fileext = file.name ? file.name.substr(file.name.lastIndexOf('.')):''; if ((fileext && filetype != 'image') || (allowFiles && (allowFiles.join('') + '.').indexOf(fileext.toLowerCase() + '.') == -1)) { errorHandler(me.getLang('autoupload.exceedTypeError')); return; } /* 创建Ajax并提交 */ var xhr = new XMLHttpRequest(), fd = new FormData(), params = utils.serializeParam(me.queryCommandValue('serverparam')) || '', url = utils.formatUrl(actionUrl + (actionUrl.indexOf('?') == -1 ? '?':'&') + params); fd.append(fieldName, file, file.name || ('blob.' + file.type.substr('image/'.length))); fd.append('type', 'ajax'); xhr.open("post", url, true); xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); xhr.addEventListener('load', function (e) { try{ var json = (new Function("return " + utils.trim(e.target.response)))(); if (json.state == 'SUCCESS' && json.url) { successHandler(json); } else { errorHandler(json.state); } }catch(er){ errorHandler(me.getLang('autoupload.loadError')); } }); xhr.send(fd); } function getPasteImage(e){ return e.clipboardData && e.clipboardData.items && e.clipboardData.items.length == 1 && /^image\//.test(e.clipboardData.items[0].type) ? e.clipboardData.items:null; } function getDropImage(e){ return e.dataTransfer && e.dataTransfer.files ? e.dataTransfer.files:null; } return { outputRule: function(root){ utils.each(root.getNodesByTagName('img'),function(n){ if (/\b(loaderrorclass)|(bloaderrorclass)\b/.test(n.getAttr('class'))) { n.parentNode.removeChild(n); } }); utils.each(root.getNodesByTagName('p'),function(n){ if (/\bloadpara\b/.test(n.getAttr('class'))) { n.parentNode.removeChild(n); } }); }, bindEvents:{ //插入粘贴板的图片,拖放插入图片 'ready':function(e){ var me = this; if(window.FormData && window.FileReader) { domUtils.on(me.body, 'paste drop', function(e){ var hasImg = false, items; //获取粘贴板文件列表或者拖放文件列表 items = e.type == 'paste' ? getPasteImage(e):getDropImage(e); if(items){ var len = items.length, file; while (len--){ file = items[len]; if(file.getAsFile) file = file.getAsFile(); if(file && file.size > 0) { sendAndInsertFile(file, me); hasImg = true; } } hasImg && e.preventDefault(); } }); //取消拖放图片时出现的文字光标位置提示 domUtils.on(me.body, 'dragover', function (e) { if(e.dataTransfer.types[0] == 'Files') { e.preventDefault(); } }); //设置loading的样式 utils.cssRule('loading', '.loadingclass{display:inline-block;cursor:default;background: url(\'' + this.options.themePath + this.options.theme +'/images/loading.gif\') no-repeat center center transparent;border:1px solid #cccccc;margin-left:1px;height: 22px;width: 22px;}\n' + '.loaderrorclass{display:inline-block;cursor:default;background: url(\'' + this.options.themePath + this.options.theme +'/images/loaderror.png\') no-repeat center center transparent;border:1px solid #cccccc;margin-right:1px;height: 22px;width: 22px;' + '}', this.document); } } } } }); // plugins/autosave.js UE.plugin.register('autosave', function (){ var me = this, //无限循环保护 lastSaveTime = new Date(), //最小保存间隔时间 MIN_TIME = 20, //auto save key saveKey = null; function save ( editor ) { var saveData; if ( new Date() - lastSaveTime < MIN_TIME ) { return; } if ( !editor.hasContents() ) { //这里不能调用命令来删除, 会造成事件死循环 saveKey && me.removePreferences( saveKey ); return; } lastSaveTime = new Date(); editor._saveFlag = null; saveData = me.body.innerHTML; if ( editor.fireEvent( "beforeautosave", { content: saveData } ) === false ) { return; } me.setPreferences( saveKey, saveData ); editor.fireEvent( "afterautosave", { content: saveData } ); } return { defaultOptions: { //默认间隔时间 saveInterval: 500, enableAutoSave: true // HaoChuan9421 }, bindEvents:{ 'ready':function(){ var _suffix = "-drafts-data", key = null; if ( me.key ) { key = me.key + _suffix; } else { key = ( me.container.parentNode.id || 'ue-common' ) + _suffix; } //页面地址+编辑器ID 保持唯一 saveKey = ( location.protocol + location.host + location.pathname ).replace( /[.:\/]/g, '_' ) + key; }, 'contentchange': function () { // HaoChuan9421 if (!me.getOpt('enableAutoSave')) { return; } if ( !saveKey ) { return; } if ( me._saveFlag ) { window.clearTimeout( me._saveFlag ); } if ( me.options.saveInterval > 0 ) { me._saveFlag = window.setTimeout( function () { save( me ); }, me.options.saveInterval ); } else { save(me); } } }, commands:{ 'clearlocaldata':{ execCommand:function (cmd, name) { if ( saveKey && me.getPreferences( saveKey ) ) { me.removePreferences( saveKey ) } }, notNeedUndo: true, ignoreContentChange:true }, 'getlocaldata':{ execCommand:function (cmd, name) { return saveKey ? me.getPreferences( saveKey ) || '' : ''; }, notNeedUndo: true, ignoreContentChange:true }, 'drafts':{ execCommand:function (cmd, name) { if ( saveKey ) { me.body.innerHTML = me.getPreferences( saveKey ) || '

    '+domUtils.fillHtml+'

    '; me.focus(true); } }, queryCommandState: function () { return saveKey ? ( me.getPreferences( saveKey ) === null ? -1 : 0 ) : -1; }, notNeedUndo: true, ignoreContentChange:true } } } }); // plugins/charts.js UE.plugin.register('charts', function (){ var me = this; return { bindEvents: { 'chartserror': function () { } }, commands:{ 'charts': { execCommand: function ( cmd, data ) { var tableNode = domUtils.findParentByTagName(this.selection.getRange().startContainer, 'table', true), flagText = [], config = {}; if ( !tableNode ) { return false; } if ( !validData( tableNode ) ) { me.fireEvent( "chartserror" ); return false; } config.title = data.title || ''; config.subTitle = data.subTitle || ''; config.xTitle = data.xTitle || ''; config.yTitle = data.yTitle || ''; config.suffix = data.suffix || ''; config.tip = data.tip || ''; //数据对齐方式 config.dataFormat = data.tableDataFormat || ''; //图表类型 config.chartType = data.chartType || 0; for ( var key in config ) { if ( !config.hasOwnProperty( key ) ) { continue; } flagText.push( key+":"+config[ key ] ); } tableNode.setAttribute( "data-chart", flagText.join( ";" ) ); domUtils.addClass( tableNode, "edui-charts-table" ); }, queryCommandState: function ( cmd, name ) { var tableNode = domUtils.findParentByTagName(this.selection.getRange().startContainer, 'table', true); return tableNode && validData( tableNode ) ? 0 : -1; } } }, inputRule:function(root){ utils.each(root.getNodesByTagName('table'),function( tableNode ){ if ( tableNode.getAttr("data-chart") !== undefined ) { tableNode.setAttr("style"); } }) }, outputRule:function(root){ utils.each(root.getNodesByTagName('table'),function( tableNode ){ if ( tableNode.getAttr("data-chart") !== undefined ) { tableNode.setAttr("style", "display: none;"); } }) } } function validData ( table ) { var firstRows = null, cellCount = 0; //行数不够 if ( table.rows.length < 2 ) { return false; } //列数不够 if ( table.rows[0].cells.length < 2 ) { return false; } //第一行所有cell必须是th firstRows = table.rows[ 0 ].cells; cellCount = firstRows.length; for ( var i = 0, cell; cell = firstRows[ i ]; i++ ) { if ( cell.tagName.toLowerCase() !== 'th' ) { return false; } } for ( var i = 1, row; row = table.rows[ i ]; i++ ) { //每行单元格数不匹配, 返回false if ( row.cells.length != cellCount ) { return false; } //第一列不是th也返回false if ( row.cells[0].tagName.toLowerCase() !== 'th' ) { return false; } for ( var j = 1, cell; cell = row.cells[ j ]; j++ ) { var value = utils.trim( ( cell.innerText || cell.textContent || '' ) ); value = value.replace( new RegExp( UE.dom.domUtils.fillChar, 'g' ), '' ).replace( /^\s+|\s+$/g, '' ); //必须是数字 if ( !/^\d*\.?\d+$/.test( value ) ) { return false; } } } return true; } }); // plugins/section.js /** * 目录大纲支持插件 * @file * @since 1.3.0 */ UE.plugin.register('section', function (){ /* 目录节点对象 */ function Section(option){ this.tag = ''; this.level = -1, this.dom = null; this.nextSection = null; this.previousSection = null; this.parentSection = null; this.startAddress = []; this.endAddress = []; this.children = []; } function getSection(option) { var section = new Section(); return utils.extend(section, option); } function getNodeFromAddress(startAddress, root) { var current = root; for(var i = 0;i < startAddress.length; i++) { if(!current.childNodes) return null; current = current.childNodes[startAddress[i]]; } return current; } var me = this; return { bindMultiEvents:{ type: 'aftersetcontent afterscencerestore', handler: function(){ me.fireEvent('updateSections'); } }, bindEvents:{ /* 初始化、拖拽、粘贴、执行setcontent之后 */ 'ready': function (){ me.fireEvent('updateSections'); domUtils.on(me.body, 'drop paste', function(){ me.fireEvent('updateSections'); }); }, /* 执行paragraph命令之后 */ 'afterexeccommand': function (type, cmd) { if(cmd == 'paragraph') { me.fireEvent('updateSections'); } }, /* 部分键盘操作,触发updateSections事件 */ 'keyup': function (type, e) { var me = this, range = me.selection.getRange(); if(range.collapsed != true) { me.fireEvent('updateSections'); } else { var keyCode = e.keyCode || e.which; if(keyCode == 13 || keyCode == 8 || keyCode == 46) { me.fireEvent('updateSections'); } } } }, commands:{ 'getsections': { execCommand: function (cmd, levels) { var levelFn = levels || ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; for (var i = 0; i < levelFn.length; i++) { if (typeof levelFn[i] == 'string') { levelFn[i] = function(fn){ return function(node){ return node.tagName == fn.toUpperCase() }; }(levelFn[i]); } else if (typeof levelFn[i] != 'function') { levelFn[i] = function (node) { return null; } } } function getSectionLevel(node) { for (var i = 0; i < levelFn.length; i++) { if (levelFn[i](node)) return i; } return -1; } var me = this, Directory = getSection({'level':-1, 'title':'root'}), previous = Directory; function traversal(node, Directory) { var level, tmpSection = null, parent, child, children = node.childNodes; for (var i = 0, len = children.length; i < len; i++) { child = children[i]; level = getSectionLevel(child); if (level >= 0) { var address = me.selection.getRange().selectNode(child).createAddress(true).startAddress, current = getSection({ 'tag': child.tagName, 'title': child.innerText || child.textContent || '', 'level': level, 'dom': child, 'startAddress': utils.clone(address, []), 'endAddress': utils.clone(address, []), 'children': [] }); previous.nextSection = current; current.previousSection = previous; parent = previous; while(level <= parent.level){ parent = parent.parentSection; } current.parentSection = parent; parent.children.push(current); tmpSection = previous = current; } else { child.nodeType === 1 && traversal(child, Directory); tmpSection && tmpSection.endAddress[tmpSection.endAddress.length - 1] ++; } } } traversal(me.body, Directory); return Directory; }, notNeedUndo: true }, 'movesection': { execCommand: function (cmd, sourceSection, targetSection, isAfter) { var me = this, targetAddress, target; if(!sourceSection || !targetSection || targetSection.level == -1) return; targetAddress = isAfter ? targetSection.endAddress:targetSection.startAddress; target = getNodeFromAddress(targetAddress, me.body); /* 判断目标地址是否被源章节包含 */ if(!targetAddress || !target || isContainsAddress(sourceSection.startAddress, sourceSection.endAddress, targetAddress)) return; var startNode = getNodeFromAddress(sourceSection.startAddress, me.body), endNode = getNodeFromAddress(sourceSection.endAddress, me.body), current, nextNode; if(isAfter) { current = endNode; while ( current && !(domUtils.getPosition( startNode, current ) & domUtils.POSITION_FOLLOWING) ) { nextNode = current.previousSibling; domUtils.insertAfter(target, current); if(current == startNode) break; current = nextNode; } } else { current = startNode; while ( current && !(domUtils.getPosition( current, endNode ) & domUtils.POSITION_FOLLOWING) ) { nextNode = current.nextSibling; target.parentNode.insertBefore(current, target); if(current == endNode) break; current = nextNode; } } me.fireEvent('updateSections'); /* 获取地址的包含关系 */ function isContainsAddress(startAddress, endAddress, addressTarget){ var isAfterStartAddress = false, isBeforeEndAddress = false; for(var i = 0; i< startAddress.length; i++){ if(i >= addressTarget.length) break; if(addressTarget[i] > startAddress[i]) { isAfterStartAddress = true; break; } else if(addressTarget[i] < startAddress[i]) { break; } } for(var i = 0; i< endAddress.length; i++){ if(i >= addressTarget.length) break; if(addressTarget[i] < startAddress[i]) { isBeforeEndAddress = true; break; } else if(addressTarget[i] > startAddress[i]) { break; } } return isAfterStartAddress && isBeforeEndAddress; } } }, 'deletesection': { execCommand: function (cmd, section, keepChildren) { var me = this; if(!section) return; function getNodeFromAddress(startAddress) { var current = me.body; for(var i = 0;i < startAddress.length; i++) { if(!current.childNodes) return null; current = current.childNodes[startAddress[i]]; } return current; } var startNode = getNodeFromAddress(section.startAddress), endNode = getNodeFromAddress(section.endAddress), current = startNode, nextNode; if(!keepChildren) { while ( current && domUtils.inDoc(endNode, me.document) && !(domUtils.getPosition( current, endNode ) & domUtils.POSITION_FOLLOWING) ) { nextNode = current.nextSibling; domUtils.remove(current); current = nextNode; } } else { domUtils.remove(current); } me.fireEvent('updateSections'); } }, 'selectsection': { execCommand: function (cmd, section) { if(!section && !section.dom) return false; var me = this, range = me.selection.getRange(), address = { 'startAddress':utils.clone(section.startAddress, []), 'endAddress':utils.clone(section.endAddress, []) }; address.endAddress[address.endAddress.length - 1]++; range.moveToAddress(address).select().scrollToView(); return true; }, notNeedUndo: true }, 'scrolltosection': { execCommand: function (cmd, section) { if(!section && !section.dom) return false; var me = this, range = me.selection.getRange(), address = { 'startAddress':section.startAddress, 'endAddress':section.endAddress }; address.endAddress[address.endAddress.length - 1]++; range.moveToAddress(address).scrollToView(); return true; }, notNeedUndo: true } } } }); // plugins/simpleupload.js /** * @description * 简单上传:点击按钮,直接选择文件上传。 * 原 UEditor 作者使用了 form 表单 + iframe 的方式上传 * 但由于同源策略的限制,父页面无法访问跨域的 iframe 内容 * 导致无法获取接口返回的数据,使得单图上传无法在跨域的情况下使用 * 这里改为普通的XHR上传,兼容到IE10+ * @author HaoChuan9421 * @date 2018-12-20 */ UE.plugin.register('simpleupload', function() { var me = this, containerBtn, timestrap = (+new Date()).toString(36); function initUploadBtn() { var w = containerBtn.offsetWidth || 20, h = containerBtn.offsetHeight || 20, btnStyle = 'display:block;width:' + w + 'px;height:' + h + 'px;overflow:hidden;border:0;margin:0;padding:0;position:absolute;top:0;left:0;filter:alpha(opacity=0);-moz-opacity:0;-khtml-opacity: 0;opacity: 0;cursor:pointer;'; var form = document.createElement('form'); var input = document.createElement('input'); form.id = 'edui_form_' + timestrap; form.enctype = 'multipart/form-data'; form.style = btnStyle; input.id = 'edui_input_' + timestrap; input.type = 'file' input.accept = 'image/*'; input.name = me.options.imageFieldName; input.style = btnStyle; form.appendChild(input); containerBtn.appendChild(form); input.addEventListener('change', function(event) { if (!input.value) return; var loadingId = 'loading_' + (+new Date()).toString(36); var imageActionUrl = me.getActionUrl(me.getOpt('imageActionName')); var params = utils.serializeParam(me.queryCommandValue('serverparam')) || ''; var action = utils.formatUrl(imageActionUrl + (imageActionUrl.indexOf('?') == -1 ? '?' : '&') + params); var allowFiles = me.getOpt('imageAllowFiles'); me.focus(); me.execCommand('inserthtml', ''); function showErrorLoader(title) { if (loadingId) { var loader = me.document.getElementById(loadingId); loader && domUtils.remove(loader); me.fireEvent('showmessage', { 'id': loadingId, 'content': title, 'type': 'error', 'timeout': 4000 }); } } /* 判断后端配置是否没有加载成功 */ if (!me.getOpt('imageActionName')) { showErrorLoader(me.getLang('autoupload.errorLoadConfig')); return; } // 判断文件格式是否错误 var filename = input.value, fileext = filename ? filename.substr(filename.lastIndexOf('.')) : ''; if (!fileext || (allowFiles && (allowFiles.join('') + '.').indexOf(fileext.toLowerCase() + '.') == -1)) { showErrorLoader(me.getLang('simpleupload.exceedTypeError')); return; } var xhr = new XMLHttpRequest() xhr.open('post', action, true) if (me.options.headers && Object.prototype.toString.apply(me.options.headers) === "[object Object]") { for (var key in me.options.headers) { xhr.setRequestHeader(key, me.options.headers[key]) } } xhr.onload = function() { if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) { var res = JSON.parse(xhr.responseText) var link = me.options.imageUrlPrefix + res.url; if (res.state == 'SUCCESS' && res.url) { loader = me.document.getElementById(loadingId); loader.setAttribute('src', link); loader.setAttribute('_src', link); loader.setAttribute('title', res.title || ''); loader.setAttribute('alt', res.original || ''); loader.removeAttribute('id'); domUtils.removeClasses(loader, 'loadingclass'); me.fireEvent("contentchange"); } else { showErrorLoader(res.state); } } else { showErrorLoader(me.getLang('simpleupload.loadError')); } }; xhr.onerror = function() { showErrorLoader(me.getLang('simpleupload.loadError')); }; xhr.send(new FormData(form)); form.reset(); }) } return { bindEvents: { 'ready': function() { //设置loading的样式 utils.cssRule('loading', '.loadingclass{display:inline-block;cursor:default;background: url(\'' + this.options.themePath + this.options.theme + '/images/loading.gif\') no-repeat center center transparent;border:1px solid #cccccc;margin-right:1px;height: 22px;width: 22px;}\n' + '.loaderrorclass{display:inline-block;cursor:default;background: url(\'' + this.options.themePath + this.options.theme + '/images/loaderror.png\') no-repeat center center transparent;border:1px solid #cccccc;margin-right:1px;height: 22px;width: 22px;' + '}', this.document); }, /* 初始化简单上传按钮 */ 'simpleuploadbtnready': function(type, container) { containerBtn = container; me.afterConfigReady(initUploadBtn); } }, outputRule: function(root) { utils.each(root.getNodesByTagName('img'), function(n) { if (/\b(loaderrorclass)|(bloaderrorclass)\b/.test(n.getAttr('class'))) { n.parentNode.removeChild(n); } }); } } }); // plugins/serverparam.js /** * 服务器提交的额外参数列表设置插件 * @file * @since 1.2.6.1 */ UE.plugin.register('serverparam', function (){ var me = this, serverParam = {}; return { commands:{ /** * 修改服务器提交的额外参数列表,清除所有项 * @command serverparam * @method execCommand * @param { String } cmd 命令字符串 * @example * ```javascript * editor.execCommand('serverparam'); * editor.queryCommandValue('serverparam'); //返回空 * ``` */ /** * 修改服务器提交的额外参数列表,删除指定项 * @command serverparam * @method execCommand * @param { String } cmd 命令字符串 * @param { String } key 要清除的属性 * @example * ```javascript * editor.execCommand('serverparam', 'name'); //删除属性name * ``` */ /** * 修改服务器提交的额外参数列表,使用键值添加项 * @command serverparam * @method execCommand * @param { String } cmd 命令字符串 * @param { String } key 要添加的属性 * @param { String } value 要添加属性的值 * @example * ```javascript * editor.execCommand('serverparam', 'name', 'hello'); * editor.queryCommandValue('serverparam'); //返回对象 {'name': 'hello'} * ``` */ /** * 修改服务器提交的额外参数列表,传入键值对对象添加多项 * @command serverparam * @method execCommand * @param { String } cmd 命令字符串 * @param { Object } key 传入的键值对对象 * @example * ```javascript * editor.execCommand('serverparam', {'name': 'hello'}); * editor.queryCommandValue('serverparam'); //返回对象 {'name': 'hello'} * ``` */ /** * 修改服务器提交的额外参数列表,使用自定义函数添加多项 * @command serverparam * @method execCommand * @param { String } cmd 命令字符串 * @param { Function } key 自定义获取参数的函数 * @example * ```javascript * editor.execCommand('serverparam', function(editor){ * return {'key': 'value'}; * }); * editor.queryCommandValue('serverparam'); //返回对象 {'key': 'value'} * ``` */ /** * 获取服务器提交的额外参数列表 * @command serverparam * @method queryCommandValue * @param { String } cmd 命令字符串 * @example * ```javascript * editor.queryCommandValue( 'serverparam' ); //返回对象 {'key': 'value'} * ``` */ 'serverparam':{ execCommand:function (cmd, key, value) { if (key === undefined || key === null) { //不传参数,清空列表 serverParam = {}; } else if (utils.isString(key)) { //传入键值 if(value === undefined || value === null) { delete serverParam[key]; } else { serverParam[key] = value; } } else if (utils.isObject(key)) { //传入对象,覆盖列表项 utils.extend(serverParam, key, true); } else if (utils.isFunction(key)){ //传入函数,添加列表项 utils.extend(serverParam, key(), true); } }, queryCommandValue: function(){ return serverParam || {}; } } } } }); // plugins/insertfile.js /** * 插入附件 */ UE.plugin.register('insertfile', function (){ var me = this; function getFileIcon(url){ var ext = url.substr(url.lastIndexOf('.') + 1).toLowerCase(), maps = { "rar":"icon_rar.gif", "zip":"icon_rar.gif", "tar":"icon_rar.gif", "gz":"icon_rar.gif", "bz2":"icon_rar.gif", "doc":"icon_doc.gif", "docx":"icon_doc.gif", "pdf":"icon_pdf.gif", "mp3":"icon_mp3.gif", "xls":"icon_xls.gif", "chm":"icon_chm.gif", "ppt":"icon_ppt.gif", "pptx":"icon_ppt.gif", "avi":"icon_mv.gif", "rmvb":"icon_mv.gif", "wmv":"icon_mv.gif", "flv":"icon_mv.gif", "swf":"icon_mv.gif", "rm":"icon_mv.gif", "exe":"icon_exe.gif", "psd":"icon_psd.gif", "txt":"icon_txt.gif", "jpg":"icon_jpg.gif", "png":"icon_jpg.gif", "jpeg":"icon_jpg.gif", "gif":"icon_jpg.gif", "ico":"icon_jpg.gif", "bmp":"icon_jpg.gif" }; return maps[ext] ? maps[ext]:maps['txt']; } return { commands:{ 'insertfile': { execCommand: function (command, filelist){ filelist = utils.isArray(filelist) ? filelist : [filelist]; var i, item, icon, title, html = '', URL = me.getOpt('UEDITOR_HOME_URL'), iconDir = URL + (URL.substr(URL.length - 1) == '/' ? '':'/') + 'dialogs/attachment/fileTypeImages/'; for (i = 0; i < filelist.length; i++) { item = filelist[i]; icon = iconDir + getFileIcon(item.url); title = item.title || item.url.substr(item.url.lastIndexOf('/') + 1); html += '

    ' + '' + '' + title + '' + '

    '; } me.execCommand('insertHtml', html); } } } } }); // plugins/xssFilter.js /** * @file xssFilter.js * @desc xss过滤器 * @author robbenmu */ UE.plugins.xssFilter = function() { var config = UEDITOR_CONFIG; var whitList = config.whitList; function filter(node) { var tagName = node.tagName; var attrs = node.attrs; if (!whitList.hasOwnProperty(tagName)) { node.parentNode.removeChild(node); return false; } UE.utils.each(attrs, function (val, key) { if (whitList[tagName].indexOf(key) === -1) { node.setAttr(key); } }); } // 添加inserthtml\paste等操作用的过滤规则 if (whitList && config.xssFilterRules) { this.options.filterRules = function () { var result = {}; UE.utils.each(whitList, function(val, key) { result[key] = function (node) { return filter(node); }; }); return result; }(); } var tagList = []; UE.utils.each(whitList, function (val, key) { tagList.push(key); }); // 添加input过滤规则 // if (whitList && config.inputXssFilter) { this.addInputRule(function (root) { root.traversal(function(node) { if (node.type !== 'element') { return false; } filter(node); }); }); } // 添加output过滤规则 // if (whitList && config.outputXssFilter) { this.addOutputRule(function (root) { root.traversal(function(node) { if (node.type !== 'element') { return false; } filter(node); }); }); } }; // ui/ui.js var baidu = baidu || {}; baidu.editor = baidu.editor || {}; UE.ui = baidu.editor.ui = {}; // ui/uiutils.js (function (){ var browser = baidu.editor.browser, domUtils = baidu.editor.dom.domUtils; var magic = '$EDITORUI'; var root = window[magic] = {}; var uidMagic = 'ID' + magic; var uidCount = 0; var uiUtils = baidu.editor.ui.uiUtils = { uid: function (obj){ return (obj ? obj[uidMagic] || (obj[uidMagic] = ++ uidCount) : ++ uidCount); }, hook: function ( fn, callback ) { var dg; if (fn && fn._callbacks) { dg = fn; } else { dg = function (){ var q; if (fn) { q = fn.apply(this, arguments); } var callbacks = dg._callbacks; var k = callbacks.length; while (k --) { var r = callbacks[k].apply(this, arguments); if (q === undefined) { q = r; } } return q; }; dg._callbacks = []; } dg._callbacks.push(callback); return dg; }, createElementByHtml: function (html){ var el = document.createElement('div'); el.innerHTML = html; el = el.firstChild; el.parentNode.removeChild(el); return el; }, getViewportElement: function (){ return (browser.ie && browser.quirks) ? document.body : document.documentElement; }, getClientRect: function (element){ var bcr; //trace IE6下在控制编辑器显隐时可能会报错,catch一下 try{ bcr = element.getBoundingClientRect(); }catch(e){ bcr={left:0,top:0,height:0,width:0} } var rect = { left: Math.round(bcr.left), top: Math.round(bcr.top), height: Math.round(bcr.bottom - bcr.top), width: Math.round(bcr.right - bcr.left) }; var doc; while ((doc = element.ownerDocument) !== document && (element = domUtils.getWindow(doc).frameElement)) { bcr = element.getBoundingClientRect(); rect.left += bcr.left; rect.top += bcr.top; } rect.bottom = rect.top + rect.height; rect.right = rect.left + rect.width; return rect; }, getViewportRect: function (){ var viewportEl = uiUtils.getViewportElement(); var width = (window.innerWidth || viewportEl.clientWidth) | 0; var height = (window.innerHeight ||viewportEl.clientHeight) | 0; return { left: 0, top: 0, height: height, width: width, bottom: height, right: width }; }, setViewportOffset: function (element, offset){ var rect; var fixedLayer = uiUtils.getFixedLayer(); if (element.parentNode === fixedLayer) { element.style.left = offset.left + 'px'; element.style.top = offset.top + 'px'; } else { domUtils.setViewportOffset(element, offset); } }, getEventOffset: function (evt){ var el = evt.target || evt.srcElement; var rect = uiUtils.getClientRect(el); var offset = uiUtils.getViewportOffsetByEvent(evt); return { left: offset.left - rect.left, top: offset.top - rect.top }; }, getViewportOffsetByEvent: function (evt){ var el = evt.target || evt.srcElement; var frameEl = domUtils.getWindow(el).frameElement; var offset = { left: evt.clientX, top: evt.clientY }; if (frameEl && el.ownerDocument !== document) { var rect = uiUtils.getClientRect(frameEl); offset.left += rect.left; offset.top += rect.top; } return offset; }, setGlobal: function (id, obj){ root[id] = obj; return magic + '["' + id + '"]'; }, unsetGlobal: function (id){ delete root[id]; }, copyAttributes: function (tgt, src){ var attributes = src.attributes; var k = attributes.length; while (k --) { var attrNode = attributes[k]; if ( attrNode.nodeName != 'style' && attrNode.nodeName != 'class' && (!browser.ie || attrNode.specified) ) { tgt.setAttribute(attrNode.nodeName, attrNode.nodeValue); } } if (src.className) { domUtils.addClass(tgt,src.className); } if (src.style.cssText) { tgt.style.cssText += ';' + src.style.cssText; } }, removeStyle: function (el, styleName){ if (el.style.removeProperty) { el.style.removeProperty(styleName); } else if (el.style.removeAttribute) { el.style.removeAttribute(styleName); } else throw ''; }, contains: function (elA, elB){ return elA && elB && (elA === elB ? false : ( elA.contains ? elA.contains(elB) : elA.compareDocumentPosition(elB) & 16 )); }, startDrag: function (evt, callbacks,doc){ var doc = doc || document; var startX = evt.clientX; var startY = evt.clientY; function handleMouseMove(evt){ var x = evt.clientX - startX; var y = evt.clientY - startY; callbacks.ondragmove(x, y,evt); if (evt.stopPropagation) { evt.stopPropagation(); } else { evt.cancelBubble = true; } } if (doc.addEventListener) { function handleMouseUp(evt){ doc.removeEventListener('mousemove', handleMouseMove, true); doc.removeEventListener('mouseup', handleMouseUp, true); window.removeEventListener('mouseup', handleMouseUp, true); callbacks.ondragstop(); } doc.addEventListener('mousemove', handleMouseMove, true); doc.addEventListener('mouseup', handleMouseUp, true); window.addEventListener('mouseup', handleMouseUp, true); evt.preventDefault(); } else { var elm = evt.srcElement; elm.setCapture(); function releaseCaptrue(){ elm.releaseCapture(); elm.detachEvent('onmousemove', handleMouseMove); elm.detachEvent('onmouseup', releaseCaptrue); elm.detachEvent('onlosecaptrue', releaseCaptrue); callbacks.ondragstop(); } elm.attachEvent('onmousemove', handleMouseMove); elm.attachEvent('onmouseup', releaseCaptrue); elm.attachEvent('onlosecaptrue', releaseCaptrue); evt.returnValue = false; } callbacks.ondragstart(); }, getFixedLayer: function (){ var layer = document.getElementById('edui_fixedlayer'); if (layer == null) { layer = document.createElement('div'); layer.id = 'edui_fixedlayer'; document.body.appendChild(layer); if (browser.ie && browser.version <= 8) { layer.style.position = 'absolute'; bindFixedLayer(); setTimeout(updateFixedOffset); } else { layer.style.position = 'fixed'; } layer.style.left = '0'; layer.style.top = '0'; layer.style.width = '0'; layer.style.height = '0'; } return layer; }, makeUnselectable: function (element){ if (browser.opera || (browser.ie && browser.version < 9)) { element.unselectable = 'on'; if (element.hasChildNodes()) { for (var i=0; i
    '; } }; utils.inherits(Separator, UIBase); })(); // ui/mask.js ///import core ///import uicore (function (){ var utils = baidu.editor.utils, domUtils = baidu.editor.dom.domUtils, UIBase = baidu.editor.ui.UIBase, uiUtils = baidu.editor.ui.uiUtils; var Mask = baidu.editor.ui.Mask = function (options){ this.initOptions(options); this.initUIBase(); }; Mask.prototype = { getHtmlTpl: function (){ return '
    '; }, postRender: function (){ var me = this; domUtils.on(window, 'resize', function (){ setTimeout(function (){ if (!me.isHidden()) { me._fill(); } }); }); }, show: function (zIndex){ this._fill(); this.getDom().style.display = ''; this.getDom().style.zIndex = zIndex; }, hide: function (){ this.getDom().style.display = 'none'; this.getDom().style.zIndex = ''; }, isHidden: function (){ return this.getDom().style.display == 'none'; }, _onMouseDown: function (){ return false; }, _onClick: function (e, target){ this.fireEvent('click', e, target); }, _fill: function (){ var el = this.getDom(); var vpRect = uiUtils.getViewportRect(); el.style.width = vpRect.width + 'px'; el.style.height = vpRect.height + 'px'; } }; utils.inherits(Mask, UIBase); })(); // ui/popup.js ///import core ///import uicore (function () { var utils = baidu.editor.utils, uiUtils = baidu.editor.ui.uiUtils, domUtils = baidu.editor.dom.domUtils, UIBase = baidu.editor.ui.UIBase, Popup = baidu.editor.ui.Popup = function (options){ this.initOptions(options); this.initPopup(); }; var allPopups = []; function closeAllPopup( evt,el ){ for ( var i = 0; i < allPopups.length; i++ ) { var pop = allPopups[i]; if (!pop.isHidden()) { if (pop.queryAutoHide(el) !== false) { if(evt&&/scroll/ig.test(evt.type)&&pop.className=="edui-wordpastepop") return; pop.hide(); } } } if(allPopups.length) pop.editor.fireEvent("afterhidepop"); } Popup.postHide = closeAllPopup; var ANCHOR_CLASSES = ['edui-anchor-topleft','edui-anchor-topright', 'edui-anchor-bottomleft','edui-anchor-bottomright']; Popup.prototype = { SHADOW_RADIUS: 5, content: null, _hidden: false, autoRender: true, canSideLeft: true, canSideUp: true, initPopup: function (){ this.initUIBase(); allPopups.push( this ); }, getHtmlTpl: function (){ return '
    ' + '
    ' + ' ' + '
    ' + '
    ' + this.getContentHtmlTpl() + '
    ' + '
    ' + '
    '; }, getContentHtmlTpl: function (){ if(this.content){ if (typeof this.content == 'string') { return this.content; } return this.content.renderHtml(); }else{ return '' } }, _UIBase_postRender: UIBase.prototype.postRender, postRender: function (){ if (this.content instanceof UIBase) { this.content.postRender(); } //捕获鼠标滚轮 if( this.captureWheel && !this.captured ) { this.captured = true; var winHeight = ( document.documentElement.clientHeight || document.body.clientHeight ) - 80, _height = this.getDom().offsetHeight, _top = uiUtils.getClientRect( this.combox.getDom() ).top, content = this.getDom('content'), ifr = this.getDom('body').getElementsByTagName('iframe'), me = this; ifr.length && ( ifr = ifr[0] ); while( _top + _height > winHeight ) { _height -= 30; } content.style.height = _height + 'px'; //同步更改iframe高度 ifr && ( ifr.style.height = _height + 'px' ); //阻止在combox上的鼠标滚轮事件, 防止用户的正常操作被误解 if( window.XMLHttpRequest ) { domUtils.on( content, ( 'onmousewheel' in document.body ) ? 'mousewheel' :'DOMMouseScroll' , function(e){ if(e.preventDefault) { e.preventDefault(); } else { e.returnValue = false; } if( e.wheelDelta ) { content.scrollTop -= ( e.wheelDelta / 120 )*60; } else { content.scrollTop -= ( e.detail / -3 )*60; } }); } else { //ie6 domUtils.on( this.getDom(), 'mousewheel' , function(e){ e.returnValue = false; me.getDom('content').scrollTop -= ( e.wheelDelta / 120 )*60; }); } } this.fireEvent('postRenderAfter'); this.hide(true); this._UIBase_postRender(); }, _doAutoRender: function (){ if (!this.getDom() && this.autoRender) { this.render(); } }, mesureSize: function (){ var box = this.getDom('content'); return uiUtils.getClientRect(box); }, fitSize: function (){ if( this.captureWheel && this.sized ) { return this.__size; } this.sized = true; var popBodyEl = this.getDom('body'); popBodyEl.style.width = ''; popBodyEl.style.height = ''; var size = this.mesureSize(); if( this.captureWheel ) { popBodyEl.style.width = -(-20 -size.width) + 'px'; var height = parseInt( this.getDom('content').style.height, 10 ); !window.isNaN( height ) && ( size.height = height ); } else { popBodyEl.style.width = size.width + 'px'; } popBodyEl.style.height = size.height + 'px'; this.__size = size; this.captureWheel && (this.getDom('content').style.overflow = 'auto'); return size; }, showAnchor: function ( element, hoz ){ this.showAnchorRect( uiUtils.getClientRect( element ), hoz ); }, showAnchorRect: function ( rect, hoz, adj ){ this._doAutoRender(); var vpRect = uiUtils.getViewportRect(); this.getDom().style.visibility = 'hidden'; this._show(); var popSize = this.fitSize(); var sideLeft, sideUp, left, top; if (hoz) { sideLeft = this.canSideLeft && (rect.right + popSize.width > vpRect.right && rect.left > popSize.width); sideUp = this.canSideUp && (rect.top + popSize.height > vpRect.bottom && rect.bottom > popSize.height); left = (sideLeft ? rect.left - popSize.width : rect.right); top = (sideUp ? rect.bottom - popSize.height : rect.top); } else { sideLeft = this.canSideLeft && (rect.right + popSize.width > vpRect.right && rect.left > popSize.width); sideUp = this.canSideUp && (rect.top + popSize.height > vpRect.bottom && rect.bottom > popSize.height); left = (sideLeft ? rect.right - popSize.width : rect.left); top = (sideUp ? rect.top - popSize.height : rect.bottom); } var popEl = this.getDom(); uiUtils.setViewportOffset(popEl, { left: left, top: top }); domUtils.removeClasses(popEl, ANCHOR_CLASSES); popEl.className += ' ' + ANCHOR_CLASSES[(sideUp ? 1 : 0) * 2 + (sideLeft ? 1 : 0)]; if(this.editor){ popEl.style.zIndex = this.editor.container.style.zIndex * 1 + 10; baidu.editor.ui.uiUtils.getFixedLayer().style.zIndex = popEl.style.zIndex - 1; } this.getDom().style.visibility = 'visible'; }, showAt: function (offset) { var left = offset.left; var top = offset.top; var rect = { left: left, top: top, right: left, bottom: top, height: 0, width: 0 }; this.showAnchorRect(rect, false, true); }, _show: function (){ if (this._hidden) { var box = this.getDom(); box.style.display = ''; this._hidden = false; // if (box.setActive) { // box.setActive(); // } this.fireEvent('show'); } }, isHidden: function (){ return this._hidden; }, show: function (){ this._doAutoRender(); this._show(); }, hide: function (notNofity){ if (!this._hidden && this.getDom()) { this.getDom().style.display = 'none'; this._hidden = true; if (!notNofity) { this.fireEvent('hide'); } } }, queryAutoHide: function (el){ return !el || !uiUtils.contains(this.getDom(), el); } }; utils.inherits(Popup, UIBase); domUtils.on( document, 'mousedown', function ( evt ) { var el = evt.target || evt.srcElement; closeAllPopup( evt,el ); } ); domUtils.on( window, 'scroll', function (evt,el) { closeAllPopup( evt,el ); } ); })(); // ui/colorpicker.js ///import core ///import uicore (function (){ var utils = baidu.editor.utils, UIBase = baidu.editor.ui.UIBase, ColorPicker = baidu.editor.ui.ColorPicker = function (options){ this.initOptions(options); this.noColorText = this.noColorText || this.editor.getLang("clearColor"); this.initUIBase(); }; ColorPicker.prototype = { getHtmlTpl: function (){ return genColorPicker(this.noColorText,this.editor); }, _onTableClick: function (evt){ var tgt = evt.target || evt.srcElement; var color = tgt.getAttribute('data-color'); if (color) { this.fireEvent('pickcolor', color); } }, _onTableOver: function (evt){ var tgt = evt.target || evt.srcElement; var color = tgt.getAttribute('data-color'); if (color) { this.getDom('preview').style.backgroundColor = color; } }, _onTableOut: function (){ this.getDom('preview').style.backgroundColor = ''; }, _onPickNoColor: function (){ this.fireEvent('picknocolor'); } }; utils.inherits(ColorPicker, UIBase); var COLORS = ( 'ffffff,000000,eeece1,1f497d,4f81bd,c0504d,9bbb59,8064a2,4bacc6,f79646,' + 'f2f2f2,7f7f7f,ddd9c3,c6d9f0,dbe5f1,f2dcdb,ebf1dd,e5e0ec,dbeef3,fdeada,' + 'd8d8d8,595959,c4bd97,8db3e2,b8cce4,e5b9b7,d7e3bc,ccc1d9,b7dde8,fbd5b5,' + 'bfbfbf,3f3f3f,938953,548dd4,95b3d7,d99694,c3d69b,b2a2c7,92cddc,fac08f,' + 'a5a5a5,262626,494429,17365d,366092,953734,76923c,5f497a,31859b,e36c09,' + '7f7f7f,0c0c0c,1d1b10,0f243e,244061,632423,4f6128,3f3151,205867,974806,' + 'c00000,ff0000,ffc000,ffff00,92d050,00b050,00b0f0,0070c0,002060,7030a0,').split(','); function genColorPicker(noColorText,editor){ var html = '
    ' + '
    ' + '
    ' + '
    '+ noColorText +'
    ' + '
    ' + '' + ''+ ''; for (var i=0; i':'')+''; } html += i<70 ? '':''; } html += '
    '+editor.getLang("themeColor")+'
    '+editor.getLang("standardColor")+'
    '; return html; } })(); // ui/tablepicker.js ///import core ///import uicore (function (){ var utils = baidu.editor.utils, uiUtils = baidu.editor.ui.uiUtils, UIBase = baidu.editor.ui.UIBase; var TablePicker = baidu.editor.ui.TablePicker = function (options){ this.initOptions(options); this.initTablePicker(); }; TablePicker.prototype = { defaultNumRows: 10, defaultNumCols: 10, maxNumRows: 20, maxNumCols: 20, numRows: 10, numCols: 10, lengthOfCellSide: 22, initTablePicker: function (){ this.initUIBase(); }, getHtmlTpl: function (){ var me = this; return '
    ' + '
    ' + '
    ' + '' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    '; }, _UIBase_render: UIBase.prototype.render, render: function (holder){ this._UIBase_render(holder); this.getDom('label').innerHTML = '0'+this.editor.getLang("t_row")+' x 0'+this.editor.getLang("t_col"); }, _track: function (numCols, numRows){ var style = this.getDom('overlay').style; var sideLen = this.lengthOfCellSide; style.width = numCols * sideLen + 'px'; style.height = numRows * sideLen + 'px'; var label = this.getDom('label'); label.innerHTML = numCols +this.editor.getLang("t_col")+' x ' + numRows + this.editor.getLang("t_row"); this.numCols = numCols; this.numRows = numRows; }, _onMouseOver: function (evt, el){ var rel = evt.relatedTarget || evt.fromElement; if (!uiUtils.contains(el, rel) && el !== rel) { this.getDom('label').innerHTML = '0'+this.editor.getLang("t_col")+' x 0'+this.editor.getLang("t_row"); this.getDom('overlay').style.visibility = ''; } }, _onMouseOut: function (evt, el){ var rel = evt.relatedTarget || evt.toElement; if (!uiUtils.contains(el, rel) && el !== rel) { this.getDom('label').innerHTML = '0'+this.editor.getLang("t_col")+' x 0'+this.editor.getLang("t_row"); this.getDom('overlay').style.visibility = 'hidden'; } }, _onMouseMove: function (evt, el){ var style = this.getDom('overlay').style; var offset = uiUtils.getEventOffset(evt); var sideLen = this.lengthOfCellSide; var numCols = Math.ceil(offset.left / sideLen); var numRows = Math.ceil(offset.top / sideLen); this._track(numCols, numRows); }, _onClick: function (){ this.fireEvent('picktable', this.numCols, this.numRows); } }; utils.inherits(TablePicker, UIBase); })(); // ui/stateful.js (function (){ var browser = baidu.editor.browser, domUtils = baidu.editor.dom.domUtils, uiUtils = baidu.editor.ui.uiUtils; var TPL_STATEFUL = 'onmousedown="$$.Stateful_onMouseDown(event, this);"' + ' onmouseup="$$.Stateful_onMouseUp(event, this);"' + ( browser.ie ? ( ' onmouseenter="$$.Stateful_onMouseEnter(event, this);"' + ' onmouseleave="$$.Stateful_onMouseLeave(event, this);"' ) : ( ' onmouseover="$$.Stateful_onMouseOver(event, this);"' + ' onmouseout="$$.Stateful_onMouseOut(event, this);"' )); baidu.editor.ui.Stateful = { alwalysHoverable: false, target:null,//目标元素和this指向dom不一样 Stateful_init: function (){ this._Stateful_dGetHtmlTpl = this.getHtmlTpl; this.getHtmlTpl = this.Stateful_getHtmlTpl; }, Stateful_getHtmlTpl: function (){ var tpl = this._Stateful_dGetHtmlTpl(); // 使用function避免$转义 return tpl.replace(/stateful/g, function (){ return TPL_STATEFUL; }); }, Stateful_onMouseEnter: function (evt, el){ this.target=el; if (!this.isDisabled() || this.alwalysHoverable) { this.addState('hover'); this.fireEvent('over'); } }, Stateful_onMouseLeave: function (evt, el){ if (!this.isDisabled() || this.alwalysHoverable) { this.removeState('hover'); this.removeState('active'); this.fireEvent('out'); } }, Stateful_onMouseOver: function (evt, el){ var rel = evt.relatedTarget; if (!uiUtils.contains(el, rel) && el !== rel) { this.Stateful_onMouseEnter(evt, el); } }, Stateful_onMouseOut: function (evt, el){ var rel = evt.relatedTarget; if (!uiUtils.contains(el, rel) && el !== rel) { this.Stateful_onMouseLeave(evt, el); } }, Stateful_onMouseDown: function (evt, el){ if (!this.isDisabled()) { this.addState('active'); } }, Stateful_onMouseUp: function (evt, el){ if (!this.isDisabled()) { this.removeState('active'); } }, Stateful_postRender: function (){ if (this.disabled && !this.hasState('disabled')) { this.addState('disabled'); } }, hasState: function (state){ return domUtils.hasClass(this.getStateDom(), 'edui-state-' + state); }, addState: function (state){ if (!this.hasState(state)) { this.getStateDom().className += ' edui-state-' + state; } }, removeState: function (state){ if (this.hasState(state)) { domUtils.removeClasses(this.getStateDom(), ['edui-state-' + state]); } }, getStateDom: function (){ return this.getDom('state'); }, isChecked: function (){ return this.hasState('checked'); }, setChecked: function (checked){ if (!this.isDisabled() && checked) { this.addState('checked'); } else { this.removeState('checked'); } }, isDisabled: function (){ return this.hasState('disabled'); }, setDisabled: function (disabled){ if (disabled) { this.removeState('hover'); this.removeState('checked'); this.removeState('active'); this.addState('disabled'); } else { this.removeState('disabled'); } } }; })(); // ui/button.js ///import core ///import uicore ///import ui/stateful.js (function (){ var utils = baidu.editor.utils, UIBase = baidu.editor.ui.UIBase, Stateful = baidu.editor.ui.Stateful, Button = baidu.editor.ui.Button = function (options){ if(options.name){ var btnName = options.name; var cssRules = options.cssRules; if(!options.className){ options.className = 'edui-for-' + btnName; } options.cssRules = '.edui-default .edui-for-'+ btnName +' .edui-icon {'+ cssRules +'}' } this.initOptions(options); this.initButton(); }; Button.prototype = { uiName: 'button', label: '', title: '', showIcon: true, showText: true, cssRules:'', initButton: function (){ this.initUIBase(); this.Stateful_init(); if(this.cssRules){ utils.cssRule('edui-customize-'+this.name+'-style',this.cssRules); } }, getHtmlTpl: function (){ return '
    ' + '
    ' + '
    ' + (this.showIcon ? '
    ' : '') + (this.showText ? '
    ' + this.label + '
    ' : '') + '
    ' + '
    ' + '
    '; }, postRender: function (){ this.Stateful_postRender(); this.setDisabled(this.disabled) }, _onMouseDown: function (e){ var target = e.target || e.srcElement, tagName = target && target.tagName && target.tagName.toLowerCase(); if (tagName == 'input' || tagName == 'object' || tagName == 'object') { return false; } }, _onClick: function (){ if (!this.isDisabled()) { this.fireEvent('click'); } }, setTitle: function(text){ var label = this.getDom('label'); label.innerHTML = text; } }; utils.inherits(Button, UIBase); utils.extend(Button.prototype, Stateful); })(); // ui/splitbutton.js ///import core ///import uicore ///import ui/stateful.js (function (){ var utils = baidu.editor.utils, uiUtils = baidu.editor.ui.uiUtils, domUtils = baidu.editor.dom.domUtils, UIBase = baidu.editor.ui.UIBase, Stateful = baidu.editor.ui.Stateful, SplitButton = baidu.editor.ui.SplitButton = function (options){ this.initOptions(options); this.initSplitButton(); }; SplitButton.prototype = { popup: null, uiName: 'splitbutton', title: '', initSplitButton: function (){ this.initUIBase(); this.Stateful_init(); var me = this; if (this.popup != null) { var popup = this.popup; this.popup = null; this.setPopup(popup); } }, _UIBase_postRender: UIBase.prototype.postRender, postRender: function (){ this.Stateful_postRender(); this._UIBase_postRender(); }, setPopup: function (popup){ if (this.popup === popup) return; if (this.popup != null) { this.popup.dispose(); } popup.addListener('show', utils.bind(this._onPopupShow, this)); popup.addListener('hide', utils.bind(this._onPopupHide, this)); popup.addListener('postrender', utils.bind(function (){ popup.getDom('body').appendChild( uiUtils.createElementByHtml('
    ') ); popup.getDom().className += ' ' + this.className; }, this)); this.popup = popup; }, _onPopupShow: function (){ this.addState('opened'); }, _onPopupHide: function (){ this.removeState('opened'); }, getHtmlTpl: function (){ return '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    '; }, showPopup: function (){ // 当popup往上弹出的时候,做特殊处理 var rect = uiUtils.getClientRect(this.getDom()); rect.top -= this.popup.SHADOW_RADIUS; rect.height += this.popup.SHADOW_RADIUS; this.popup.showAnchorRect(rect); }, _onArrowClick: function (event, el){ if (!this.isDisabled()) { this.showPopup(); } }, _onButtonClick: function (){ if (!this.isDisabled()) { this.fireEvent('buttonclick'); } } }; utils.inherits(SplitButton, UIBase); utils.extend(SplitButton.prototype, Stateful, true); })(); // ui/colorbutton.js ///import core ///import uicore ///import ui/colorpicker.js ///import ui/popup.js ///import ui/splitbutton.js (function (){ var utils = baidu.editor.utils, uiUtils = baidu.editor.ui.uiUtils, ColorPicker = baidu.editor.ui.ColorPicker, Popup = baidu.editor.ui.Popup, SplitButton = baidu.editor.ui.SplitButton, ColorButton = baidu.editor.ui.ColorButton = function (options){ this.initOptions(options); this.initColorButton(); }; ColorButton.prototype = { initColorButton: function (){ var me = this; this.popup = new Popup({ content: new ColorPicker({ noColorText: me.editor.getLang("clearColor"), editor:me.editor, onpickcolor: function (t, color){ me._onPickColor(color); }, onpicknocolor: function (t, color){ me._onPickNoColor(color); } }), editor:me.editor }); this.initSplitButton(); }, _SplitButton_postRender: SplitButton.prototype.postRender, postRender: function (){ this._SplitButton_postRender(); this.getDom('button_body').appendChild( uiUtils.createElementByHtml('
    ') ); this.getDom().className += ' edui-colorbutton'; }, setColor: function (color){ this.getDom('colorlump').style.backgroundColor = color; this.color = color; }, _onPickColor: function (color){ if (this.fireEvent('pickcolor', color) !== false) { this.setColor(color); this.popup.hide(); } }, _onPickNoColor: function (color){ if (this.fireEvent('picknocolor') !== false) { this.popup.hide(); } } }; utils.inherits(ColorButton, SplitButton); })(); // ui/tablebutton.js ///import core ///import uicore ///import ui/popup.js ///import ui/tablepicker.js ///import ui/splitbutton.js (function (){ var utils = baidu.editor.utils, Popup = baidu.editor.ui.Popup, TablePicker = baidu.editor.ui.TablePicker, SplitButton = baidu.editor.ui.SplitButton, TableButton = baidu.editor.ui.TableButton = function (options){ this.initOptions(options); this.initTableButton(); }; TableButton.prototype = { initTableButton: function (){ var me = this; this.popup = new Popup({ content: new TablePicker({ editor:me.editor, onpicktable: function (t, numCols, numRows){ me._onPickTable(numCols, numRows); } }), 'editor':me.editor }); this.initSplitButton(); }, _onPickTable: function (numCols, numRows){ if (this.fireEvent('picktable', numCols, numRows) !== false) { this.popup.hide(); } } }; utils.inherits(TableButton, SplitButton); })(); // ui/autotypesetpicker.js ///import core ///import uicore (function () { var utils = baidu.editor.utils, UIBase = baidu.editor.ui.UIBase; var AutoTypeSetPicker = baidu.editor.ui.AutoTypeSetPicker = function (options) { this.initOptions(options); this.initAutoTypeSetPicker(); }; AutoTypeSetPicker.prototype = { initAutoTypeSetPicker:function () { this.initUIBase(); }, getHtmlTpl:function () { var me = this.editor, opt = me.options.autotypeset, lang = me.getLang("autoTypeSet"); var textAlignInputName = 'textAlignValue' + me.uid, imageBlockInputName = 'imageBlockLineValue' + me.uid, symbolConverInputName = 'symbolConverValue' + me.uid; return '
    ' + '
    ' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
    ' + lang.mergeLine + '' + lang.delLine + '
    ' + lang.removeFormat + '' + lang.indent + '
    ' + lang.alignment + '' + '' + me.getLang("justifyleft") + '' + me.getLang("justifycenter") + '' + me.getLang("justifyright") + '
    ' + lang.imageFloat + '' + '' + me.getLang("default") + '' + me.getLang("justifyleft") + '' + me.getLang("justifycenter") + '' + me.getLang("justifyright") + '
    ' + lang.removeFontsize + '' + lang.removeFontFamily + '
    ' + lang.removeHtml + '
    ' + lang.pasteFilter + '
    ' + lang.symbol + '' + '' + lang.bdc2sb + '' + lang.tobdc + '' + '
    ' + '
    ' + '
    '; }, _UIBase_render:UIBase.prototype.render }; utils.inherits(AutoTypeSetPicker, UIBase); })(); // ui/autotypesetbutton.js ///import core ///import uicore ///import ui/popup.js ///import ui/autotypesetpicker.js ///import ui/splitbutton.js (function (){ var utils = baidu.editor.utils, Popup = baidu.editor.ui.Popup, AutoTypeSetPicker = baidu.editor.ui.AutoTypeSetPicker, SplitButton = baidu.editor.ui.SplitButton, AutoTypeSetButton = baidu.editor.ui.AutoTypeSetButton = function (options){ this.initOptions(options); this.initAutoTypeSetButton(); }; function getPara(me){ var opt = {}, cont = me.getDom(), editorId = me.editor.uid, inputType = null, attrName = null, ipts = domUtils.getElementsByTagName(cont,"input"); for(var i=ipts.length-1,ipt;ipt=ipts[i--];){ inputType = ipt.getAttribute("type"); if(inputType=="checkbox"){ attrName = ipt.getAttribute("name"); opt[attrName] && delete opt[attrName]; if(ipt.checked){ var attrValue = document.getElementById( attrName + "Value" + editorId ); if(attrValue){ if(/input/ig.test(attrValue.tagName)){ opt[attrName] = attrValue.value; } else { var iptChilds = attrValue.getElementsByTagName("input"); for(var j=iptChilds.length-1,iptchild;iptchild=iptChilds[j--];){ if(iptchild.checked){ opt[attrName] = iptchild.value; break; } } } } else { opt[attrName] = true; } } else { opt[attrName] = false; } } else { opt[ipt.getAttribute("value")] = ipt.checked; } } var selects = domUtils.getElementsByTagName(cont,"select"); for(var i=0,si;si=selects[i++];){ var attr = si.getAttribute('name'); opt[attr] = opt[attr] ? si.value : ''; } utils.extend(me.editor.options.autotypeset,opt); me.editor.setPreferences('autotypeset', opt); } AutoTypeSetButton.prototype = { initAutoTypeSetButton: function (){ var me = this; this.popup = new Popup({ //传入配置参数 content: new AutoTypeSetPicker({editor:me.editor}), 'editor':me.editor, hide : function(){ if (!this._hidden && this.getDom()) { getPara(this); this.getDom().style.display = 'none'; this._hidden = true; this.fireEvent('hide'); } } }); var flag = 0; this.popup.addListener('postRenderAfter',function(){ var popupUI = this; if(flag)return; var cont = this.getDom(), btn = cont.getElementsByTagName('button')[0]; btn.onclick = function(){ getPara(popupUI); me.editor.execCommand('autotypeset'); popupUI.hide() }; domUtils.on(cont, 'click', function(e) { var target = e.target || e.srcElement, editorId = me.editor.uid; if (target && target.tagName == 'INPUT') { // 点击图片浮动的checkbox,去除对应的radio if (target.name == 'imageBlockLine' || target.name == 'textAlign' || target.name == 'symbolConver') { var checked = target.checked, radioTd = document.getElementById( target.name + 'Value' + editorId), radios = radioTd.getElementsByTagName('input'), defalutSelect = { 'imageBlockLine': 'none', 'textAlign': 'left', 'symbolConver': 'tobdc' }; for (var i = 0; i < radios.length; i++) { if (checked) { if (radios[i].value == defalutSelect[target.name]) { radios[i].checked = 'checked'; } } else { radios[i].checked = false; } } } // 点击radio,选中对应的checkbox if (target.name == ('imageBlockLineValue' + editorId) || target.name == ('textAlignValue' + editorId) || target.name == 'bdc') { var checkboxs = target.parentNode.previousSibling.getElementsByTagName('input'); checkboxs && (checkboxs[0].checked = true); } getPara(popupUI); } }); flag = 1; }); this.initSplitButton(); } }; utils.inherits(AutoTypeSetButton, SplitButton); })(); // ui/cellalignpicker.js ///import core ///import uicore (function () { var utils = baidu.editor.utils, Popup = baidu.editor.ui.Popup, Stateful = baidu.editor.ui.Stateful, UIBase = baidu.editor.ui.UIBase; /** * 该参数将新增一个参数: selected, 参数类型为一个Object, 形如{ 'align': 'center', 'valign': 'top' }, 表示单元格的初始 * 对齐状态为: 竖直居上,水平居中; 其中 align的取值为:'center', 'left', 'right'; valign的取值为: 'top', 'middle', 'bottom' * @update 2013/4/2 hancong03@baidu.com */ var CellAlignPicker = baidu.editor.ui.CellAlignPicker = function (options) { this.initOptions(options); this.initSelected(); this.initCellAlignPicker(); }; CellAlignPicker.prototype = { //初始化选中状态, 该方法将根据传递进来的参数获取到应该选中的对齐方式图标的索引 initSelected: function(){ var status = { valign: { top: 0, middle: 1, bottom: 2 }, align: { left: 0, center: 1, right: 2 }, count: 3 }, result = -1; if( this.selected ) { this.selectedIndex = status.valign[ this.selected.valign ] * status.count + status.align[ this.selected.align ]; } }, initCellAlignPicker:function () { this.initUIBase(); this.Stateful_init(); }, getHtmlTpl:function () { var alignType = [ 'left', 'center', 'right' ], COUNT = 9, tempClassName = null, tempIndex = -1, tmpl = []; for( var i= 0; i'); tmpl.push( '
    ' ); tempIndex === 2 && tmpl.push(''); } return '
    ' + '
    ' + '' + tmpl.join('') + '
    ' + '
    ' + '
    '; }, getStateDom: function (){ return this.target; }, _onClick: function (evt){ var target= evt.target || evt.srcElement; if(/icon/.test(target.className)){ this.items[target.parentNode.getAttribute("index")].onclick(); Popup.postHide(evt); } }, _UIBase_render:UIBase.prototype.render }; utils.inherits(CellAlignPicker, UIBase); utils.extend(CellAlignPicker.prototype, Stateful,true); })(); // ui/pastepicker.js ///import core ///import uicore (function () { var utils = baidu.editor.utils, Stateful = baidu.editor.ui.Stateful, uiUtils = baidu.editor.ui.uiUtils, UIBase = baidu.editor.ui.UIBase; var PastePicker = baidu.editor.ui.PastePicker = function (options) { this.initOptions(options); this.initPastePicker(); }; PastePicker.prototype = { initPastePicker:function () { this.initUIBase(); this.Stateful_init(); }, getHtmlTpl:function () { return '
    ' + '
    ' + '
    ' + this.editor.getLang("pasteOpt") + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' }, getStateDom:function () { return this.target; }, format:function (param) { this.editor.ui._isTransfer = true; this.editor.fireEvent('pasteTransfer', param); }, _onClick:function (cur) { var node = domUtils.getNextDomNode(cur), screenHt = uiUtils.getViewportRect().height, subPop = uiUtils.getClientRect(node); if ((subPop.top + subPop.height) > screenHt) node.style.top = (-subPop.height - cur.offsetHeight) + "px"; else node.style.top = ""; if (/hidden/ig.test(domUtils.getComputedStyle(node, "visibility"))) { node.style.visibility = "visible"; domUtils.addClass(cur, "edui-state-opened"); } else { node.style.visibility = "hidden"; domUtils.removeClasses(cur, "edui-state-opened") } }, _UIBase_render:UIBase.prototype.render }; utils.inherits(PastePicker, UIBase); utils.extend(PastePicker.prototype, Stateful, true); })(); // ui/toolbar.js (function (){ var utils = baidu.editor.utils, uiUtils = baidu.editor.ui.uiUtils, UIBase = baidu.editor.ui.UIBase, Toolbar = baidu.editor.ui.Toolbar = function (options){ this.initOptions(options); this.initToolbar(); }; Toolbar.prototype = { items: null, initToolbar: function (){ this.items = this.items || []; this.initUIBase(); }, add: function (item,index){ if(index === undefined){ this.items.push(item); }else{ this.items.splice(index,0,item) } }, getHtmlTpl: function (){ var buff = []; for (var i=0; i' + buff.join('') + '
    ' }, postRender: function (){ var box = this.getDom(); for (var i=0; i
    '; }, postRender:function () { }, queryAutoHide:function () { return true; } }; Menu.prototype = { items:null, uiName:'menu', initMenu:function () { this.items = this.items || []; this.initPopup(); this.initItems(); }, initItems:function () { for (var i = 0; i < this.items.length; i++) { var item = this.items[i]; if (item == '-') { this.items[i] = this.getSeparator(); } else if (!(item instanceof MenuItem)) { item.editor = this.editor; item.theme = this.editor.options.theme; this.items[i] = this.createItem(item); } } }, getSeparator:function () { return menuSeparator; }, createItem:function (item) { //新增一个参数menu, 该参数存储了menuItem所对应的menu引用 item.menu = this; return new MenuItem(item); }, _Popup_getContentHtmlTpl:Popup.prototype.getContentHtmlTpl, getContentHtmlTpl:function () { if (this.items.length == 0) { return this._Popup_getContentHtmlTpl(); } var buff = []; for (var i = 0; i < this.items.length; i++) { var item = this.items[i]; buff[i] = item.renderHtml(); } return ('
    ' + buff.join('') + '
    '); }, _Popup_postRender:Popup.prototype.postRender, postRender:function () { var me = this; for (var i = 0; i < this.items.length; i++) { var item = this.items[i]; item.ownerMenu = this; item.postRender(); } domUtils.on(this.getDom(), 'mouseover', function (evt) { evt = evt || event; var rel = evt.relatedTarget || evt.fromElement; var el = me.getDom(); if (!uiUtils.contains(el, rel) && el !== rel) { me.fireEvent('over'); } }); this._Popup_postRender(); }, queryAutoHide:function (el) { if (el) { if (uiUtils.contains(this.getDom(), el)) { return false; } for (var i = 0; i < this.items.length; i++) { var item = this.items[i]; if (item.queryAutoHide(el) === false) { return false; } } } }, clearItems:function () { for (var i = 0; i < this.items.length; i++) { var item = this.items[i]; clearTimeout(item._showingTimer); clearTimeout(item._closingTimer); if (item.subMenu) { item.subMenu.destroy(); } } this.items = []; }, destroy:function () { if (this.getDom()) { domUtils.remove(this.getDom()); } this.clearItems(); }, dispose:function () { this.destroy(); } }; utils.inherits(Menu, Popup); /** * @update 2013/04/03 hancong03 新增一个参数menu, 该参数存储了menuItem所对应的menu引用 * @type {Function} */ var MenuItem = baidu.editor.ui.MenuItem = function (options) { this.initOptions(options); this.initUIBase(); this.Stateful_init(); if (this.subMenu && !(this.subMenu instanceof Menu)) { if (options.className && options.className.indexOf("aligntd") != -1) { var me = this; //获取单元格对齐初始状态 this.subMenu.selected = this.editor.queryCommandValue( 'cellalignment' ); this.subMenu = new Popup({ content:new CellAlignPicker(this.subMenu), parentMenu:me, editor:me.editor, destroy:function () { if (this.getDom()) { domUtils.remove(this.getDom()); } } }); this.subMenu.addListener("postRenderAfter", function () { domUtils.on(this.getDom(), "mouseover", function () { me.addState('opened'); }); }); } else { this.subMenu = new Menu(this.subMenu); } } }; MenuItem.prototype = { label:'', subMenu:null, ownerMenu:null, uiName:'menuitem', alwalysHoverable:true, getHtmlTpl:function () { return '
    ' + '
    ' + this.renderLabelHtml() + '
    ' + '
    '; }, postRender:function () { var me = this; this.addListener('over', function () { me.ownerMenu.fireEvent('submenuover', me); if (me.subMenu) { me.delayShowSubMenu(); } }); if (this.subMenu) { this.getDom().className += ' edui-hassubmenu'; this.subMenu.render(); this.addListener('out', function () { me.delayHideSubMenu(); }); this.subMenu.addListener('over', function () { clearTimeout(me._closingTimer); me._closingTimer = null; me.addState('opened'); }); this.ownerMenu.addListener('hide', function () { me.hideSubMenu(); }); this.ownerMenu.addListener('submenuover', function (t, subMenu) { if (subMenu !== me) { me.delayHideSubMenu(); } }); this.subMenu._bakQueryAutoHide = this.subMenu.queryAutoHide; this.subMenu.queryAutoHide = function (el) { if (el && uiUtils.contains(me.getDom(), el)) { return false; } return this._bakQueryAutoHide(el); }; } this.getDom().style.tabIndex = '-1'; uiUtils.makeUnselectable(this.getDom()); this.Stateful_postRender(); }, delayShowSubMenu:function () { var me = this; if (!me.isDisabled()) { me.addState('opened'); clearTimeout(me._showingTimer); clearTimeout(me._closingTimer); me._closingTimer = null; me._showingTimer = setTimeout(function () { me.showSubMenu(); }, 250); } }, delayHideSubMenu:function () { var me = this; if (!me.isDisabled()) { me.removeState('opened'); clearTimeout(me._showingTimer); if (!me._closingTimer) { me._closingTimer = setTimeout(function () { if (!me.hasState('opened')) { me.hideSubMenu(); } me._closingTimer = null; }, 400); } } }, renderLabelHtml:function () { return '
    ' + '
    ' + '
    ' + (this.label || '') + '
    '; }, getStateDom:function () { return this.getDom(); }, queryAutoHide:function (el) { if (this.subMenu && this.hasState('opened')) { return this.subMenu.queryAutoHide(el); } }, _onClick:function (event, this_) { if (this.hasState('disabled')) return; if (this.fireEvent('click', event, this_) !== false) { if (this.subMenu) { this.showSubMenu(); } else { Popup.postHide(event); } } }, showSubMenu:function () { var rect = uiUtils.getClientRect(this.getDom()); rect.right -= 5; rect.left += 2; rect.width -= 7; rect.top -= 4; rect.bottom += 4; rect.height += 8; this.subMenu.showAnchorRect(rect, true, true); }, hideSubMenu:function () { this.subMenu.hide(); } }; utils.inherits(MenuItem, UIBase); utils.extend(MenuItem.prototype, Stateful, true); })(); // ui/combox.js ///import core ///import uicore ///import ui/menu.js ///import ui/splitbutton.js (function (){ // todo: menu和item提成通用list var utils = baidu.editor.utils, uiUtils = baidu.editor.ui.uiUtils, Menu = baidu.editor.ui.Menu, SplitButton = baidu.editor.ui.SplitButton, Combox = baidu.editor.ui.Combox = function (options){ this.initOptions(options); this.initCombox(); }; Combox.prototype = { uiName: 'combox', onbuttonclick:function () { this.showPopup(); }, initCombox: function (){ var me = this; this.items = this.items || []; for (var i=0; i vpRect.right) { left = vpRect.right - rect.width; } var top = offset.top; if (top + rect.height > vpRect.bottom) { top = vpRect.bottom - rect.height; } el.style.left = Math.max(left, 0) + 'px'; el.style.top = Math.max(top, 0) + 'px'; }, showAtCenter: function (){ var vpRect = uiUtils.getViewportRect(); if ( !this.fullscreen ) { this.getDom().style.display = ''; var popSize = this.fitSize(); var titleHeight = this.getDom('titlebar').offsetHeight | 0; var left = vpRect.width / 2 - popSize.width / 2; var top = vpRect.height / 2 - (popSize.height - titleHeight) / 2 - titleHeight; var popEl = this.getDom(); this.safeSetOffset({ left: Math.max(left | 0, 0), top: Math.max(top | 0, 0) }); if (!domUtils.hasClass(popEl, 'edui-state-centered')) { popEl.className += ' edui-state-centered'; } } else { var dialogWrapNode = this.getDom(), contentNode = this.getDom('content'); dialogWrapNode.style.display = "block"; var wrapRect = UE.ui.uiUtils.getClientRect( dialogWrapNode ), contentRect = UE.ui.uiUtils.getClientRect( contentNode ); dialogWrapNode.style.left = "-100000px"; contentNode.style.width = ( vpRect.width - wrapRect.width + contentRect.width ) + "px"; contentNode.style.height = ( vpRect.height - wrapRect.height + contentRect.height ) + "px"; dialogWrapNode.style.width = vpRect.width + "px"; dialogWrapNode.style.height = vpRect.height + "px"; dialogWrapNode.style.left = 0; //保存环境的overflow值 this._originalContext = { html: { overflowX: document.documentElement.style.overflowX, overflowY: document.documentElement.style.overflowY }, body: { overflowX: document.body.style.overflowX, overflowY: document.body.style.overflowY } }; document.documentElement.style.overflowX = 'hidden'; document.documentElement.style.overflowY = 'hidden'; document.body.style.overflowX = 'hidden'; document.body.style.overflowY = 'hidden'; } this._show(); }, getContentHtml: function (){ var contentHtml = ''; if (typeof this.content == 'string') { contentHtml = this.content; } else if (this.iframeUrl) { contentHtml = ''; } return contentHtml; }, getHtmlTpl: function (){ var footHtml = ''; if (this.buttons) { var buff = []; for (var i=0; i' + buff.join('') + '
    ' + '
    '; } return '
    ' + '
    ' + '
    ' + '
    ' + '' + (this.title || '') + '' + '
    ' + this.closeButton.renderHtml() + '
    ' + '
    '+ ( this.autoReset ? '' : this.getContentHtml()) +'
    ' + footHtml + '
    '; }, postRender: function (){ // todo: 保持居中/记住上次关闭位置选项 if (!this.modalMask.getDom()) { this.modalMask.render(); this.modalMask.hide(); } if (!this.dragMask.getDom()) { this.dragMask.render(); this.dragMask.hide(); } var me = this; this.addListener('show', function (){ me.modalMask.show(this.getDom().style.zIndex - 2); }); this.addListener('hide', function (){ me.modalMask.hide(); }); if (this.buttons) { for (var i=0; i'; me.editor.container.style.zIndex && (this.getDom().style.zIndex = me.editor.container.style.zIndex * 1 + 1); } } // canSideUp:false, // canSideLeft:false }); this.onbuttonclick = function(){ this.showPopup(); }; this.initSplitButton(); } }; utils.inherits(MultiMenuPop, SplitButton); })(); // ui/shortcutmenu.js (function () { var UI = baidu.editor.ui, UIBase = UI.UIBase, uiUtils = UI.uiUtils, utils = baidu.editor.utils, domUtils = baidu.editor.dom.domUtils; var allMenus = [],//存储所有快捷菜单 timeID, isSubMenuShow = false;//是否有子pop显示 var ShortCutMenu = UI.ShortCutMenu = function (options) { this.initOptions (options); this.initShortCutMenu (); }; ShortCutMenu.postHide = hideAllMenu; ShortCutMenu.prototype = { isHidden : true , SPACE : 5 , initShortCutMenu : function () { this.items = this.items || []; this.initUIBase (); this.initItems (); this.initEvent (); allMenus.push (this); } , initEvent : function () { var me = this, doc = me.editor.document; domUtils.on (doc , "mousemove" , function (e) { if (me.isHidden === false) { //有pop显示就不隐藏快捷菜单 if (me.getSubMenuMark () || me.eventType == "contextmenu") return; var flag = true, el = me.getDom (), wt = el.offsetWidth, ht = el.offsetHeight, distanceX = wt / 2 + me.SPACE,//距离中心X标准 distanceY = ht / 2,//距离中心Y标准 x = Math.abs (e.screenX - me.left),//离中心距离横坐标 y = Math.abs (e.screenY - me.top);//离中心距离纵坐标 clearTimeout (timeID); timeID = setTimeout (function () { if (y > 0 && y < distanceY) { me.setOpacity (el , "1"); } else if (y > distanceY && y < distanceY + 70) { me.setOpacity (el , "0.5"); flag = false; } else if (y > distanceY + 70 && y < distanceY + 140) { me.hide (); } if (flag && x > 0 && x < distanceX) { me.setOpacity (el , "1") } else if (x > distanceX && x < distanceX + 70) { me.setOpacity (el , "0.5") } else if (x > distanceX + 70 && x < distanceX + 140) { me.hide (); } }); } }); //ie\ff下 mouseout不准 if (browser.chrome) { domUtils.on (doc , "mouseout" , function (e) { var relatedTgt = e.relatedTarget || e.toElement; if (relatedTgt == null || relatedTgt.tagName == "HTML") { me.hide (); } }); } me.editor.addListener ("afterhidepop" , function () { if (!me.isHidden) { isSubMenuShow = true; } }); } , initItems : function () { if (utils.isArray (this.items)) { for (var i = 0, len = this.items.length ; i < len ; i++) { var item = this.items[i].toLowerCase (); if (UI[item]) { this.items[i] = new UI[item] (this.editor); this.items[i].className += " edui-shortcutsubmenu "; } } } } , setOpacity : function (el , value) { if (browser.ie && browser.version < 9) { el.style.filter = "alpha(opacity = " + parseFloat (value) * 100 + ");" } else { el.style.opacity = value; } } , getSubMenuMark : function () { isSubMenuShow = false; var layerEle = uiUtils.getFixedLayer (); var list = domUtils.getElementsByTagName (layerEle , "div" , function (node) { return domUtils.hasClass (node , "edui-shortcutsubmenu edui-popup") }); for (var i = 0, node ; node = list[i++] ;) { if (node.style.display != "none") { isSubMenuShow = true; } } return isSubMenuShow; } , show : function (e , hasContextmenu) { var me = this, offset = {}, el = this.getDom (), fixedlayer = uiUtils.getFixedLayer (); function setPos (offset) { if (offset.left < 0) { offset.left = 0; } if (offset.top < 0) { offset.top = 0; } el.style.cssText = "position:absolute;left:" + offset.left + "px;top:" + offset.top + "px;"; } function setPosByCxtMenu (menu) { if (!menu.tagName) { menu = menu.getDom (); } offset.left = parseInt (menu.style.left); offset.top = parseInt (menu.style.top); offset.top -= el.offsetHeight + 15; setPos (offset); } me.eventType = e.type; el.style.cssText = "display:block;left:-9999px"; if (e.type == "contextmenu" && hasContextmenu) { var menu = domUtils.getElementsByTagName (fixedlayer , "div" , "edui-contextmenu")[0]; if (menu) { setPosByCxtMenu (menu) } else { me.editor.addListener ("aftershowcontextmenu" , function (type , menu) { setPosByCxtMenu (menu); }); } } else { offset = uiUtils.getViewportOffsetByEvent (e); offset.top -= el.offsetHeight + me.SPACE; offset.left += me.SPACE + 20; setPos (offset); me.setOpacity (el , 0.2); } me.isHidden = false; me.left = e.screenX + el.offsetWidth / 2 - me.SPACE; me.top = e.screenY - (el.offsetHeight / 2) - me.SPACE; if (me.editor) { el.style.zIndex = me.editor.container.style.zIndex * 1 + 10; fixedlayer.style.zIndex = el.style.zIndex - 1; } } , hide : function () { if (this.getDom ()) { this.getDom ().style.display = "none"; } this.isHidden = true; } , postRender : function () { if (utils.isArray (this.items)) { for (var i = 0, item ; item = this.items[i++] ;) { item.postRender (); } } } , getHtmlTpl : function () { var buff; if (utils.isArray (this.items)) { buff = []; for (var i = 0 ; i < this.items.length ; i++) { buff[i] = this.items[i].renderHtml (); } buff = buff.join (""); } else { buff = this.items; } return '
    ' + buff + '
    '; } }; utils.inherits (ShortCutMenu , UIBase); function hideAllMenu (e) { var tgt = e.target || e.srcElement, cur = domUtils.findParent (tgt , function (node) { return domUtils.hasClass (node , "edui-shortcutmenu") || domUtils.hasClass (node , "edui-popup"); } , true); if (!cur) { for (var i = 0, menu ; menu = allMenus[i++] ;) { menu.hide () } } } domUtils.on (document , 'mousedown' , function (e) { hideAllMenu (e); }); domUtils.on (window , 'scroll' , function (e) { hideAllMenu (e); }); }) (); // ui/breakline.js (function (){ var utils = baidu.editor.utils, UIBase = baidu.editor.ui.UIBase, Breakline = baidu.editor.ui.Breakline = function (options){ this.initOptions(options); this.initSeparator(); }; Breakline.prototype = { uiName: 'Breakline', initSeparator: function (){ this.initUIBase(); }, getHtmlTpl: function (){ return '
    '; } }; utils.inherits(Breakline, UIBase); })(); // ui/message.js ///import core ///import uicore (function () { var utils = baidu.editor.utils, domUtils = baidu.editor.dom.domUtils, UIBase = baidu.editor.ui.UIBase, Message = baidu.editor.ui.Message = function (options){ this.initOptions(options); this.initMessage(); }; Message.prototype = { initMessage: function (){ this.initUIBase(); }, getHtmlTpl: function (){ return '
    ' + '
    ×
    ' + '
    ' + ' ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    '; }, reset: function(opt){ var me = this; if (!opt.keepshow) { clearTimeout(this.timer); me.timer = setTimeout(function(){ me.hide(); }, opt.timeout || 4000); } opt.content !== undefined && me.setContent(opt.content); opt.type !== undefined && me.setType(opt.type); me.show(); }, postRender: function(){ var me = this, closer = this.getDom('closer'); closer && domUtils.on(closer, 'click', function(){ me.hide(); }); }, setContent: function(content){ this.getDom('content').innerHTML = content; }, setType: function(type){ type = type || 'info'; var body = this.getDom('body'); body.className = body.className.replace(/edui-message-type-[\w-]+/, 'edui-message-type-' + type); }, getContent: function(){ return this.getDom('content').innerHTML; }, getType: function(){ var arr = this.getDom('body').match(/edui-message-type-([\w-]+)/); return arr ? arr[1]:''; }, show: function (){ this.getDom().style.display = 'block'; }, hide: function (){ var dom = this.getDom(); if (dom) { dom.style.display = 'none'; dom.parentNode && dom.parentNode.removeChild(dom); } } }; utils.inherits(Message, UIBase); })(); // adapter/editorui.js //ui跟编辑器的适配層 //那个按钮弹出是dialog,是下拉筐等都是在这个js中配置 //自己写的ui也要在这里配置,放到baidu.editor.ui下边,当编辑器实例化的时候会根据ueditor.config中的toolbars找到相应的进行实例化 (function () { var utils = baidu.editor.utils; var editorui = baidu.editor.ui; var _Dialog = editorui.Dialog; editorui.buttons = {}; editorui.Dialog = function (options) { var dialog = new _options); dialog.addListener('hide', function () { if (dialog.editor) { var editor = dialog.editor; try { if (browser.gecko) { var y = editor.window.scrollY, x = editor.window.scrollX; editor.body.focus(); editor.window.scrollTo(x, y); } else { editor.focus(); } } catch (ex) { } } }); return dialog; }; var iframeUrlMap = { 'anchor':'~/dialogs/anchor/anchor.html', 'insertimage':'~/dialogs/image/image.html', 'link':'~/dialogs/link/link.html', 'spechars':'~/dialogs/spechars/spechars.html', 'searchreplace':'~/dialogs/searchreplace/searchreplace.html', 'map':'~/dialogs/map/map.html', 'gmap':'~/dialogs/gmap/gmap.html', 'insertvideo':'~/dialogs/video/video.html', 'help':'~/dialogs/help/help.html', 'preview':'~/dialogs/preview/preview.html', 'emotion':'~/dialogs/emotion/emotion.html', 'wordimage':'~/dialogs/wordimage/wordimage.html', 'attachment':'~/dialogs/attachment/attachment.html', 'insertframe':'~/dialogs/insertframe/insertframe.html', 'edittip':'~/dialogs/table/edittip.html', 'edittable':'~/dialogs/table/edittable.html', 'edittd':'~/dialogs/table/edittd.html', 'webapp':'~/dialogs/webapp/webapp.html', 'snapscreen':'~/dialogs/snapscreen/snapscreen.html', 'scrawl':'~/dialogs/scrawl/scrawl.html', 'music':'~/dialogs/music/music.html', 'template':'~/dialogs/template/template.html', 'background':'~/dialogs/background/background.html', 'charts': '~/dialogs/charts/charts.html' }; //为工具栏添加按钮,以下都是统一的按钮触发命令,所以写在一起 var btnCmds = ['undo', 'redo', 'formatmatch', 'bold', 'italic', 'underline', 'fontborder', 'touppercase', 'tolowercase', 'strikethrough', 'subscript', 'superscript', 'source', 'indent', 'outdent', 'blockquote', 'pasteplain', 'pagebreak', 'selectall', 'print','horizontal', 'removeformat', 'time', 'date', 'unlink', 'insertparagraphbeforetable', 'insertrow', 'insertcol', 'mergeright', 'mergedown', 'deleterow', 'deletecol', 'splittorows', 'splittocols', 'splittocells', 'mergecells', 'deletetable', 'drafts']; for (var i = 0, ci; ci = btnCmds[i++];) { ci = ci.toLowerCase(); editorui[ci] = function (cmd) { return function (editor) { var ui = new editorui.Button({ className:'edui-for-' + cmd, title:editor.options.labelMap[cmd] || editor.getLang("labelMap." + cmd) || '', onclick:function () { editor.execCommand(cmd); }, theme:editor.options.theme, showText:false }); editorui.buttons[cmd] = ui; editor.addListener('selectionchange', function (type, causeByUi, uiReady) { var state = editor.queryCommandState(cmd); if (state == -1) { ui.setDisabled(true); ui.setChecked(false); } else { if (!uiReady) { ui.setDisabled(false); ui.setChecked(state); } } }); return ui; }; }(ci); } //清除文档 editorui.cleardoc = function (editor) { var ui = new editorui.Button({ className:'edui-for-cleardoc', title:editor.options.labelMap.cleardoc || editor.getLang("labelMap.cleardoc") || '', theme:editor.options.theme, onclick:function () { if (confirm(editor.getLang("confirmClear"))) { editor.execCommand('cleardoc'); } } }); editorui.buttons["cleardoc"] = ui; editor.addListener('selectionchange', function () { ui.setDisabled(editor.queryCommandState('cleardoc') == -1); }); return ui; }; //排版,图片排版,文字方向 var typeset = { 'justify':['left', 'right', 'center', 'justify'], 'imagefloat':['none', 'left', 'center', 'right'], 'directionality':['ltr', 'rtl'] }; for (var p in typeset) { (function (cmd, val) { for (var i = 0, ci; ci = val[i++];) { (function (cmd2) { editorui[cmd.replace('float', '') + cmd2] = function (editor) { var ui = new editorui.Button({ className:'edui-for-' + cmd.replace('float', '') + cmd2, title:editor.options.labelMap[cmd.replace('float', '') + cmd2] || editor.getLang("labelMap." + cmd.replace('float', '') + cmd2) || '', theme:editor.options.theme, onclick:function () { editor.execCommand(cmd, cmd2); } }); editorui.buttons[cmd] = ui; editor.addListener('selectionchange', function (type, causeByUi, uiReady) { ui.setDisabled(editor.queryCommandState(cmd) == -1); ui.setChecked(editor.queryCommandValue(cmd) == cmd2 && !uiReady); }); return ui; }; })(ci) } })(p, typeset[p]) } //字体颜色和背景颜色 for (var i = 0, ci; ci = ['backcolor', 'forecolor'][i++];) { editorui[ci] = function (cmd) { return function (editor) { var ui = new editorui.ColorButton({ className:'edui-for-' + cmd, color:'default', title:editor.options.labelMap[cmd] || editor.getLang("labelMap." + cmd) || '', editor:editor, onpickcolor:function (t, color) { editor.execCommand(cmd, color); }, onpicknocolor:function () { editor.execCommand(cmd, 'default'); this.setColor('transparent'); this.color = 'default'; }, onbuttonclick:function () { editor.execCommand(cmd, this.color); } }); editorui.buttons[cmd] = ui; editor.addListener('selectionchange', function () { ui.setDisabled(editor.queryCommandState(cmd) == -1); }); return ui; }; }(ci); } var dialogBtns = { noOk:['searchreplace', 'help', 'spechars', 'webapp','preview'], ok:['attachment', 'anchor', 'link', 'insertimage', 'map', 'gmap', 'insertframe', 'wordimage', 'insertvideo', 'insertframe', 'edittip', 'edittable', 'edittd', 'scrawl', 'template', 'music', 'background', 'charts'] }; for (var p in dialogBtns) { (function (type, vals) { for (var i = 0, ci; ci = vals[i++];) { //todo opera下存在问题 if (browser.opera && ci === "searchreplace") { continue; } (function (cmd) { editorui[cmd] = function (editor, iframeUrl, title) { iframeUrl = iframeUrl || (editor.options.iframeUrlMap || {})[cmd] || iframeUrlMap[cmd]; title = editor.options.labelMap[cmd] || editor.getLang("labelMap." + cmd) || ''; var dialog; //没有iframeUrl不创建dialog if (iframeUrl) { dialog = new editorui.Dialog(utils.extend({ iframeUrl:editor.ui.mapUrl(iframeUrl), editor:editor, className:'edui-for-' + cmd, title:title, holdScroll: cmd === 'insertimage', fullscreen: /charts|preview/.test(cmd), closeDialog:editor.getLang("closeDialog") }, type == 'ok' ? { buttons:[ { className:'edui-okbutton', label:editor.getLang("ok"), editor:editor, onclick:function () { dialog.close(true); } }, { className:'edui-cancelbutton', label:editor.getLang("cancel"), editor:editor, onclick:function () { dialog.close(false); } } ] } : {})); editor.ui._dialogs[cmd + "Dialog"] = dialog; } var ui = new editorui.Button({ className:'edui-for-' + cmd, title:title, onclick:function () { if (dialog) { switch (cmd) { case "wordimage": var images = editor.execCommand("wordimage"); if (images && images.length) { dialog.render(); dialog.open(); } break; case "scrawl": if (editor.queryCommandState("scrawl") != -1) { dialog.render(); dialog.open(); } break; default: dialog.render(); dialog.open(); } } }, theme:editor.options.theme, disabled:(cmd == 'scrawl' && editor.queryCommandState("scrawl") == -1) || ( cmd == 'charts' ) }); editorui.buttons[cmd] = ui; editor.addListener('selectionchange', function () { //只存在于右键菜单而无工具栏按钮的ui不需要检测状态 var unNeedCheckState = {'edittable':1}; if (cmd in unNeedCheckState)return; var state = editor.queryCommandState(cmd); if (ui.getDom()) { ui.setDisabled(state == -1); ui.setChecked(state); } }); return ui; }; })(ci.toLowerCase()) } })(p, dialogBtns[p]); } editorui.snapscreen = function (editor, iframeUrl, title) { title = editor.options.labelMap['snapscreen'] || editor.getLang("labelMap.snapscreen") || ''; var ui = new editorui.Button({ className:'edui-for-snapscreen', title:title, onclick:function () { editor.execCommand("snapscreen"); }, theme:editor.options.theme }); editorui.buttons['snapscreen'] = ui; iframeUrl = iframeUrl || (editor.options.iframeUrlMap || {})["snapscreen"] || iframeUrlMap["snapscreen"]; if (iframeUrl) { var dialog = new editorui.Dialog({ iframeUrl:editor.ui.mapUrl(iframeUrl), editor:editor, className:'edui-for-snapscreen', title:title, buttons:[ { className:'edui-okbutton', label:editor.getLang("ok"), editor:editor, onclick:function () { dialog.close(true); } }, { className:'edui-cancelbutton', label:editor.getLang("cancel"), editor:editor, onclick:function () { dialog.close(false); } } ] }); dialog.render(); editor.ui._dialogs["snapscreenDialog"] = dialog; } editor.addListener('selectionchange', function () { ui.setDisabled(editor.queryCommandState('snapscreen') == -1); }); return ui; }; editorui.insertcode = function (editor, list, title) { list = editor.options['insertcode'] || []; title = editor.options.labelMap['insertcode'] || editor.getLang("labelMap.insertcode") || ''; // if (!list.length) return; var items = []; utils.each(list,function(key,val){ items.push({ label:key, value:val, theme:editor.options.theme, renderLabelHtml:function () { return '
    ' + (this.label || '') + '
    '; } }); }); var ui = new editorui.Combox({ editor:editor, items:items, onselect:function (t, index) { editor.execCommand('insertcode', this.items[index].value); }, onbuttonclick:function () { this.showPopup(); }, title:title, initValue:title, className:'edui-for-insertcode', indexByValue:function (value) { if (value) { for (var i = 0, ci; ci = this.items[i]; i++) { if (ci.value.indexOf(value) != -1) return i; } } return -1; } }); editorui.buttons['insertcode'] = ui; editor.addListener('selectionchange', function (type, causeByUi, uiReady) { if (!uiReady) { var state = editor.queryCommandState('insertcode'); if (state == -1) { ui.setDisabled(true); } else { ui.setDisabled(false); var value = editor.queryCommandValue('insertcode'); if(!value){ ui.setValue(title); return; } //trace:1871 ie下从源码模式切换回来时,字体会带单引号,而且会有逗号 value && (value = value.replace(/['"]/g, '').split(',')[0]); ui.setValue(value); } } }); return ui; }; editorui.fontfamily = function (editor, list, title) { list = editor.options['fontfamily'] || []; title = editor.options.labelMap['fontfamily'] || editor.getLang("labelMap.fontfamily") || ''; if (!list.length) return; for (var i = 0, ci, items = []; ci = list[i]; i++) { var langLabel = editor.getLang('fontfamily')[ci.name] || ""; (function (key, val) { items.push({ label:key, value:val, theme:editor.options.theme, renderLabelHtml:function () { return '
    ' + (this.label || '') + '
    '; } }); })(ci.label || langLabel, ci.val) } var ui = new editorui.Combox({ editor:editor, items:items, onselect:function (t, index) { editor.execCommand('FontFamily', this.items[index].value); }, onbuttonclick:function () { this.showPopup(); }, title:title, initValue:title, className:'edui-for-fontfamily', indexByValue:function (value) { if (value) { for (var i = 0, ci; ci = this.items[i]; i++) { if (ci.value.indexOf(value) != -1) return i; } } return -1; } }); editorui.buttons['fontfamily'] = ui; editor.addListener('selectionchange', function (type, causeByUi, uiReady) { if (!uiReady) { var state = editor.queryCommandState('FontFamily'); if (state == -1) { ui.setDisabled(true); } else { ui.setDisabled(false); var value = editor.queryCommandValue('FontFamily'); //trace:1871 ie下从源码模式切换回来时,字体会带单引号,而且会有逗号 value && (value = value.replace(/['"]/g, '').split(',')[0]); ui.setValue(value); } } }); return ui; }; editorui.fontsize = function (editor, list, title) { title = editor.options.labelMap['fontsize'] || editor.getLang("labelMap.fontsize") || ''; list = list || editor.options['fontsize'] || []; if (!list.length) return; var items = []; for (var i = 0; i < list.length; i++) { var size = list[i] + 'px'; items.push({ label:size, value:size, theme:editor.options.theme, renderLabelHtml:function () { return '
    ' + (this.label || '') + '
    '; } }); } var ui = new editorui.Combox({ editor:editor, items:items, title:title, initValue:title, onselect:function (t, index) { editor.execCommand('FontSize', this.items[index].value); }, onbuttonclick:function () { this.showPopup(); }, className:'edui-for-fontsize' }); editorui.buttons['fontsize'] = ui; editor.addListener('selectionchange', function (type, causeByUi, uiReady) { if (!uiReady) { var state = editor.queryCommandState('FontSize'); if (state == -1) { ui.setDisabled(true); } else { ui.setDisabled(false); ui.setValue(editor.queryCommandValue('FontSize')); } } }); return ui; }; editorui.paragraph = function (editor, list, title) { title = editor.options.labelMap['paragraph'] || editor.getLang("labelMap.paragraph") || ''; list = editor.options['paragraph'] || []; if (utils.isEmptyObject(list)) return; var items = []; for (var i in list) { items.push({ value:i, label:list[i] || editor.getLang("paragraph")[i], theme:editor.options.theme, renderLabelHtml:function () { return '
    ' + (this.label || '') + '
    '; } }) } var ui = new editorui.Combox({ editor:editor, items:items, title:title, initValue:title, className:'edui-for-paragraph', onselect:function (t, index) { editor.execCommand('Paragraph', this.items[index].value); }, onbuttonclick:function () { this.showPopup(); } }); editorui.buttons['paragraph'] = ui; editor.addListener('selectionchange', function (type, causeByUi, uiReady) { if (!uiReady) { var state = editor.queryCommandState('Paragraph'); if (state == -1) { ui.setDisabled(true); } else { ui.setDisabled(false); var value = editor.queryCommandValue('Paragraph'); var index = ui.indexByValue(value); if (index != -1) { ui.setValue(value); } else { ui.setValue(ui.initValue); } } } }); return ui; }; //自定义标题 editorui.customstyle = function (editor) { var list = editor.options['customstyle'] || [], title = editor.options.labelMap['customstyle'] || editor.getLang("labelMap.customstyle") || ''; if (!list.length)return; var langCs = editor.getLang('customstyle'); for (var i = 0, items = [], t; t = list[i++];) { (function (t) { var ck = {}; ck.label = t.label ? t.label : langCs[t.name]; ck.style = t.style; ck.className = t.className; ck.tag = t.tag; items.push({ label:ck.label, value:ck, theme:editor.options.theme, renderLabelHtml:function () { return '
    ' + '<' + ck.tag + ' ' + (ck.className ? ' class="' + ck.className + '"' : "") + (ck.style ? ' style="' + ck.style + '"' : "") + '>' + ck.label + "<\/" + ck.tag + ">" + '
    '; } }); })(t); } var ui = new editorui.Combox({ editor:editor, items:items, title:title, initValue:title, className:'edui-for-customstyle', onselect:function (t, index) { editor.execCommand('customstyle', this.items[index].value); }, onbuttonclick:function () { this.showPopup(); }, indexByValue:function (value) { for (var i = 0, ti; ti = this.items[i++];) { if (ti.label == value) { return i - 1 } } return -1; } }); editorui.buttons['customstyle'] = ui; editor.addListener('selectionchange', function (type, causeByUi, uiReady) { if (!uiReady) { var state = editor.queryCommandState('customstyle'); if (state == -1) { ui.setDisabled(true); } else { ui.setDisabled(false); var value = editor.queryCommandValue('customstyle'); var index = ui.indexByValue(value); if (index != -1) { ui.setValue(value); } else { ui.setValue(ui.initValue); } } } }); return ui; }; editorui.inserttable = function (editor, iframeUrl, title) { title = editor.options.labelMap['inserttable'] || editor.getLang("labelMap.inserttable") || ''; var ui = new editorui.TableButton({ editor:editor, title:title, className:'edui-for-inserttable', onpicktable:function (t, numCols, numRows) { editor.execCommand('InsertTable', {numRows:numRows, numCols:numCols, border:1}); }, onbuttonclick:function () { this.showPopup(); } }); editorui.buttons['inserttable'] = ui; editor.addListener('selectionchange', function () { ui.setDisabled(editor.queryCommandState('inserttable') == -1); }); return ui; }; editorui.lineheight = function (editor) { var val = editor.options.lineheight || []; if (!val.length)return; for (var i = 0, ci, items = []; ci = val[i++];) { items.push({ //todo:写死了 label:ci, value:ci, theme:editor.options.theme, onclick:function () { editor.execCommand("lineheight", this.value); } }) } var ui = new editorui.MenuButton({ editor:editor, className:'edui-for-lineheight', title:editor.options.labelMap['lineheight'] || editor.getLang("labelMap.lineheight") || '', items:items, onbuttonclick:function () { var value = editor.queryCommandValue('LineHeight') || this.value; editor.execCommand("LineHeight", value); } }); editorui.buttons['lineheight'] = ui; editor.addListener('selectionchange', function () { var state = editor.queryCommandState('LineHeight'); if (state == -1) { ui.setDisabled(true); } else { ui.setDisabled(false); var value = editor.queryCommandValue('LineHeight'); value && ui.setValue((value + '').replace(/cm/, '')); ui.setChecked(state) } }); return ui; }; var rowspacings = ['top', 'bottom']; for (var r = 0, ri; ri = rowspacings[r++];) { (function (cmd) { editorui['rowspacing' + cmd] = function (editor) { var val = editor.options['rowspacing' + cmd] || []; if (!val.length) return null; for (var i = 0, ci, items = []; ci = val[i++];) { items.push({ label:ci, value:ci, theme:editor.options.theme, onclick:function () { editor.execCommand("rowspacing", this.value, cmd); } }) } var ui = new editorui.MenuButton({ editor:editor, className:'edui-for-rowspacing' + cmd, title:editor.options.labelMap['rowspacing' + cmd] || editor.getLang("labelMap.rowspacing" + cmd) || '', items:items, onbuttonclick:function () { var value = editor.queryCommandValue('rowspacing', cmd) || this.value; editor.execCommand("rowspacing", value, cmd); } }); editorui.buttons[cmd] = ui; editor.addListener('selectionchange', function () { var state = editor.queryCommandState('rowspacing', cmd); if (state == -1) { ui.setDisabled(true); } else { ui.setDisabled(false); var value = editor.queryCommandValue('rowspacing', cmd); value && ui.setValue((value + '').replace(/%/, '')); ui.setChecked(state) } }); return ui; } })(ri) } //有序,无序列表 var lists = ['insertorderedlist', 'insertunorderedlist']; for (var l = 0, cl; cl = lists[l++];) { (function (cmd) { editorui[cmd] = function (editor) { var vals = editor.options[cmd], _onMenuClick = function () { editor.execCommand(cmd, this.value); }, items = []; for (var i in vals) { items.push({ label:vals[i] || editor.getLang()[cmd][i] || "", value:i, theme:editor.options.theme, onclick:_onMenuClick }) } var ui = new editorui.MenuButton({ editor:editor, className:'edui-for-' + cmd, title:editor.getLang("labelMap." + cmd) || '', 'items':items, onbuttonclick:function () { var value = editor.queryCommandValue(cmd) || this.value; editor.execCommand(cmd, value); } }); editorui.buttons[cmd] = ui; editor.addListener('selectionchange', function () { var state = editor.queryCommandState(cmd); if (state == -1) { ui.setDisabled(true); } else { ui.setDisabled(false); var value = editor.queryCommandValue(cmd); ui.setValue(value); ui.setChecked(state) } }); return ui; }; })(cl) } editorui.fullscreen = function (editor, title) { title = editor.options.labelMap['fullscreen'] || editor.getLang("labelMap.fullscreen") || ''; var ui = new editorui.Button({ className:'edui-for-fullscreen', title:title, theme:editor.options.theme, onclick:function () { if (editor.ui) { editor.ui.setFullScreen(!editor.ui.isFullScreen()); } this.setChecked(editor.ui.isFullScreen()); } }); editorui.buttons['fullscreen'] = ui; editor.addListener('selectionchange', function () { var state = editor.queryCommandState('fullscreen'); ui.setDisabled(state == -1); ui.setChecked(editor.ui.isFullScreen()); }); return ui; }; // 表情 editorui["emotion"] = function (editor, iframeUrl) { var cmd = "emotion"; var ui = new editorui.MultiMenuPop({ title:editor.options.labelMap[cmd] || editor.getLang("labelMap." + cmd + "") || '', editor:editor, className:'edui-for-' + cmd, iframeUrl:editor.ui.mapUrl(iframeUrl || (editor.options.iframeUrlMap || {})[cmd] || iframeUrlMap[cmd]) }); editorui.buttons[cmd] = ui; editor.addListener('selectionchange', function () { ui.setDisabled(editor.queryCommandState(cmd) == -1) }); return ui; }; editorui.autotypeset = function (editor) { var ui = new editorui.AutoTypeSetButton({ editor:editor, title:editor.options.labelMap['autotypeset'] || editor.getLang("labelMap.autotypeset") || '', className:'edui-for-autotypeset', onbuttonclick:function () { editor.execCommand('autotypeset') } }); editorui.buttons['autotypeset'] = ui; editor.addListener('selectionchange', function () { ui.setDisabled(editor.queryCommandState('autotypeset') == -1); }); return ui; }; /* 简单上传插件 */ editorui["simpleupload"] = function (editor) { var name = 'simpleupload', ui = new editorui.Button({ className:'edui-for-' + name, title:editor.options.labelMap[name] || editor.getLang("labelMap." + name) || '', onclick:function () {}, theme:editor.options.theme, showText:false }); editorui.buttons[name] = ui; editor.addListener('ready', function() { var b = ui.getDom('body'), iconSpan = b.children[0]; editor.fireEvent('simpleuploadbtnready', iconSpan); }); editor.addListener('selectionchange', function (type, causeByUi, uiReady) { var state = editor.queryCommandState(name); if (state == -1) { ui.setDisabled(true); ui.setChecked(false); } else { if (!uiReady) { ui.setDisabled(false); ui.setChecked(state); } } }); return ui; }; })(); // adapter/editor.js ///import core ///commands 全屏 ///commandsName FullScreen ///commandsTitle 全屏 (function () { var utils = baidu.editor.utils, uiUtils = baidu.editor.ui.uiUtils, UIBase = baidu.editor.ui.UIBase, domUtils = baidu.editor.dom.domUtils; var nodeStack = []; function EditorUI(options) { this.initOptions(options); this.initEditorUI(); } EditorUI.prototype = { uiName:'editor', initEditorUI:function () { this.editor.ui = this; this._dialogs = {}; this.initUIBase(); this._initToolbars(); var editor = this.editor, me = this; editor.addListener('ready', function () { //提供getDialog方法 editor.getDialog = function (name) { return editor.ui._dialogs[name + "Dialog"]; }; domUtils.on(editor.window, 'scroll', function (evt) { baidu.editor.ui.Popup.postHide(evt); }); //提供编辑器实时宽高(全屏时宽高不变化) editor.ui._actualFrameWidth = editor.options.initialFrameWidth; UE.browser.ie && UE.browser.version === 6 && editor.container.ownerDocument.execCommand("BackgroundImageCache", false, true); //display bottom-bar label based on config if (editor.options.elementPathEnabled) { editor.ui.getDom('elementpath').innerHTML = '
    ' + editor.getLang("elementPathTip") + ':
    '; } if (editor.options.wordCount) { function countFn() { setCount(editor,me); domUtils.un(editor.document, "click", arguments.callee); } domUtils.on(editor.document, "click", countFn); editor.ui.getDom('wordcount').innerHTML = editor.getLang("wordCountTip"); } editor.ui._scale(); if (editor.options.scaleEnabled) { if (editor.autoHeightEnabled) { editor.disableAutoHeight(); } me.enableScale(); } else { me.disableScale(); } if (!editor.options.elementPathEnabled && !editor.options.wordCount && !editor.options.scaleEnabled) { editor.ui.getDom('elementpath').style.display = "none"; editor.ui.getDom('wordcount').style.display = "none"; editor.ui.getDom('scale').style.display = "none"; } if (!editor.selection.isFocus())return; editor.fireEvent('selectionchange', false, true); }); editor.addListener('mousedown', function (t, evt) { var el = evt.target || evt.srcElement; baidu.editor.ui.Popup.postHide(evt, el); baidu.editor.ui.ShortCutMenu.postHide(evt); }); editor.addListener("delcells", function () { if (UE.ui['edittip']) { new UE.ui['edittip'](editor); } editor.getDialog('edittip').open(); }); var pastePop, isPaste = false, timer; editor.addListener("afterpaste", function () { if(editor.queryCommandState('pasteplain')) return; if(baidu.editor.ui.PastePicker){ pastePop = new baidu.editor.ui.Popup({ content:new baidu.editor.ui.PastePicker({editor:editor}), editor:editor, className:'edui-wordpastepop' }); pastePop.render(); } isPaste = true; }); editor.addListener("afterinserthtml", function () { clearTimeout(timer); timer = setTimeout(function () { if (pastePop && (isPaste || editor.ui._isTransfer)) { if(pastePop.isHidden()){ var span = domUtils.createElement(editor.document, 'span', { 'style':"line-height:0px;", 'innerHTML':'\ufeff' }), range = editor.selection.getRange(); range.insertNode(span); var tmp= getDomNode(span, 'firstChild', 'previousSibling'); tmp && pastePop.showAnchor(tmp.nodeType == 3 ? tmp.parentNode : tmp); domUtils.remove(span); }else{ pastePop.show(); } delete editor.ui._isTransfer; isPaste = false; } }, 200) }); editor.addListener('contextmenu', function (t, evt) { baidu.editor.ui.Popup.postHide(evt); }); editor.addListener('keydown', function (t, evt) { if (pastePop) pastePop.dispose(evt); var keyCode = evt.keyCode || evt.which; if(evt.altKey&&keyCode==90){ UE.ui.buttons['fullscreen'].onclick(); } }); editor.addListener('wordcount', function (type) { setCount(this,me); }); function setCount(editor,ui) { editor.setOpt({ wordCount:true, maximumWords:10000, wordCountMsg:editor.options.wordCountMsg || editor.getLang("wordCountMsg"), wordOverFlowMsg:editor.options.wordOverFlowMsg || editor.getLang("wordOverFlowMsg") }); var opt = editor.options, max = opt.maximumWords, msg = opt.wordCountMsg , errMsg = opt.wordOverFlowMsg, countDom = ui.getDom('wordcount'); if (!opt.wordCount) { return; } var count = editor.getContentLength(true); if (count > max) { countDom.innerHTML = errMsg; editor.fireEvent("wordcountoverflow"); } else { countDom.innerHTML = msg.replace("{#leave}", max - count).replace("{#count}", count); } } editor.addListener('selectionchange', function () { if (editor.options.elementPathEnabled) { me[(editor.queryCommandState('elementpath') == -1 ? 'dis' : 'en') + 'ableElementPath']() } if (editor.options.scaleEnabled) { me[(editor.queryCommandState('scale') == -1 ? 'dis' : 'en') + 'ableScale'](); } }); var popup = new baidu.editor.ui.Popup({ editor:editor, content:'', className:'edui-bubble', _onEditButtonClick:function () { this.hide(); editor.ui._dialogs.linkDialog.open(); }, _onImgEditButtonClick:function (name) { this.hide(); editor.ui._dialogs[name] && editor.ui._dialogs[name].open(); }, _onImgSetFloat:function (value) { this.hide(); editor.execCommand("imagefloat", value); }, _setIframeAlign:function (value) { var frame = popup.anchorEl; var newFrame = frame.cloneNode(true); switch (value) { case -2: newFrame.setAttribute("align", ""); break; case -1: newFrame.setAttribute("align", "left"); break; case 1: newFrame.setAttribute("align", "right"); break; } frame.parentNode.insertBefore(newFrame, frame); domUtils.remove(frame); popup.anchorEl = newFrame; popup.showAnchor(popup.anchorEl); }, _updateIframe:function () { var frame = editor._iframe = popup.anchorEl; if(domUtils.hasClass(frame, 'ueditor_baidumap')) { editor.selection.getRange().selectNode(frame).select(); editor.ui._dialogs.mapDialog.open(); popup.hide(); } else { editor.ui._dialogs.insertframeDialog.open(); popup.hide(); } }, _onRemoveButtonClick:function (cmdName) { editor.execCommand(cmdName); this.hide(); }, queryAutoHide:function (el) { if (el && el.ownerDocument == editor.document) { if (el.tagName.toLowerCase() == 'img' || domUtils.findParentByTagName(el, 'a', true)) { return el !== popup.anchorEl; } } return baidu.editor.ui.Popup.prototype.queryAutoHide.call(this, el); } }); popup.render(); if (editor.options.imagePopup) { editor.addListener('mouseover', function (t, evt) { evt = evt || window.event; var el = evt.target || evt.srcElement; if (editor.ui._dialogs.insertframeDialog && /iframe/ig.test(el.tagName)) { var html = popup.formatHtml( '' + editor.getLang("property") + ': ' + editor.getLang("default") + '  ' + editor.getLang("justifyleft") + '  ' + editor.getLang("justifyright") + '  ' + ' ' + editor.getLang("modify") + ''); if (html) { popup.getDom('content').innerHTML = html; popup.anchorEl = el; popup.showAnchor(popup.anchorEl); } else { popup.hide(); } } }); editor.addListener('selectionchange', function (t, causeByUi) { if (!causeByUi) return; var html = '', str = "", img = editor.selection.getRange().getClosedNode(), dialogs = editor.ui._dialogs; if (img && img.tagName == 'IMG') { var dialogName = 'insertimageDialog'; if (img.className.indexOf("edui-faked-video") != -1 || img.className.indexOf("edui-upload-video") != -1) { dialogName = "insertvideoDialog" } if (img.className.indexOf("edui-faked-webapp") != -1) { dialogName = "webappDialog" } if (img.src.indexOf("http://api.map.baidu.com") != -1) { dialogName = "mapDialog" } if (img.className.indexOf("edui-faked-music") != -1) { dialogName = "musicDialog" } if (img.src.indexOf("http://maps.google.com/maps/api/staticmap") != -1) { dialogName = "gmapDialog" } if (img.getAttribute("anchorname")) { dialogName = "anchorDialog"; html = popup.formatHtml( '' + editor.getLang("property") + ': ' + editor.getLang("modify") + '  ' + '' + editor.getLang("delete") + ''); } if (img.getAttribute("word_img")) { //todo 放到dialog去做查询 editor.word_img = [img.getAttribute("word_img")]; dialogName = "wordimageDialog" } if(domUtils.hasClass(img, 'loadingclass') || domUtils.hasClass(img, 'loaderrorclass')) { dialogName = ""; } if (!dialogs[dialogName]) { return; } str = '' + editor.getLang("property") + ': '+ '' + editor.getLang("default") + '  ' + '' + editor.getLang("justifyleft") + '  ' + '' + editor.getLang("justifyright") + '  ' + '' + editor.getLang("justifycenter") + '  '+ '' + editor.getLang("modify") + ''; !html && (html = popup.formatHtml(str)) } if (editor.ui._dialogs.linkDialog) { var link = editor.queryCommandValue('link'); var url; if (link && (url = (link.getAttribute('_href') || link.getAttribute('href', 2)))) { var txt = url; if (url.length > 30) { txt = url.substring(0, 20) + "..."; } if (html) { html += '
    ' } html += popup.formatHtml( '' + editor.getLang("anthorMsg") + ': ' + txt + '' + ' ' + editor.getLang("modify") + '' + ' ' + editor.getLang("clear") + ''); popup.showAnchor(link); } } if (html) { popup.getDom('content').innerHTML = html; popup.anchorEl = img || link; popup.showAnchor(popup.anchorEl); } else { popup.hide(); } }); } }, _initToolbars:function () { var editor = this.editor; var toolbars = this.toolbars || []; var toolbarUis = []; for (var i = 0; i < toolbars.length; i++) { var toolbar = toolbars[i]; var toolbarUi = new baidu.editor.ui.Toolbar({theme:editor.options.theme}); for (var j = 0; j < toolbar.length; j++) { var toolbarItem = toolbar[j]; var toolbarItemUi = null; if (typeof toolbarItem == 'string') { toolbarItem = toolbarItem.toLowerCase(); if (toolbarItem == '|') { toolbarItem = 'Separator'; } if(toolbarItem == '||'){ toolbarItem = 'Breakline'; } if (baidu.editor.ui[toolbarItem]) { toolbarItemUi = new baidu.editor.ui[toolbarItem](editor); } //fullscreen这里单独处理一下,放到首行去 if (toolbarItem == 'fullscreen') { if (toolbarUis && toolbarUis[0]) { toolbarUis[0].items.splice(0, 0, toolbarItemUi); } else { toolbarItemUi && toolbarUi.items.splice(0, 0, toolbarItemUi); } continue; } } else { toolbarItemUi = toolbarItem; } if (toolbarItemUi && toolbarItemUi.id) { toolbarUi.add(toolbarItemUi); } } toolbarUis[i] = toolbarUi; } //接受外部定制的UI(修复因 utils.each 无法准确的循环出对象的全部元素而导致的自定义 UI 不符合预期的 BUG by HaoChuan9421) // utils.each(UE._customizeUI,function(obj,key){ // var itemUI,index; // if(obj.id && obj.id != editor.key){ // return false; // } // itemUI = obj.execFn.call(editor,editor,key); // if(itemUI){ // index = obj.index; // if(index === undefined){ // index = toolbarUi.items.length; // } // toolbarUi.add(itemUI,index) // } // }); for(var key in UE._customizeUI){ var obj = UE._customizeUI[key] var itemUI,index; if(!obj.id || obj.id == editor.key){ itemUI = obj.execFn.call(editor,editor,key); if(itemUI){ index = obj.index; if(index === undefined){ index = toolbarUi.items.length; } toolbarUi.add(itemUI,index) } } } this.toolbars = toolbarUis; }, getHtmlTpl:function () { return '
    ' + '
    ' + (this.toolbars.length ? '
    ' + this.renderToolbarBoxHtml() + '
    ' : '') + '' + '
    ' + '
    ' + '
    ' + '
    ' + //modify wdcount by matao '
    ' + '' + '' + '' + '
    ' + '
    ' + '
    '; }, showWordImageDialog:function () { this._dialogs['wordimageDialog'].open(); }, renderToolbarBoxHtml:function () { var buff = []; for (var i = 0; i < this.toolbars.length; i++) { buff.push(this.toolbars[i].renderHtml()); } return buff.join(''); }, setFullScreen:function (fullscreen) { var editor = this.editor, container = editor.container.parentNode.parentNode; if (this._fullscreen != fullscreen) { this._fullscreen = fullscreen; this.editor.fireEvent('beforefullscreenchange', fullscreen); if (baidu.editor.browser.gecko) { var bk = editor.selection.getRange().createBookmark(); } if (fullscreen) { while (container.tagName != "BODY") { var position = baidu.editor.dom.domUtils.getComputedStyle(container, "position"); nodeStack.push(position); container.style.position = "static"; container = container.parentNode; } this._bakHtmlOverflow = document.documentElement.style.overflow; this._bakBodyOverflow = document.body.style.overflow; this._bakAutoHeight = this.editor.autoHeightEnabled; this._bakScrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop); this._bakEditorContaninerWidth = editor.iframe.parentNode.offsetWidth; if (this._bakAutoHeight) { //当全屏时不能执行自动长高 editor.autoHeightEnabled = false; this.editor.disableAutoHeight(); } document.documentElement.style.overflow = 'hidden'; //修复,滚动条不收起的问题 window.scrollTo(0,window.scrollY); this._bakCssText = this.getDom().style.cssText; this._bakCssText1 = this.getDom('iframeholder').style.cssText; editor.iframe.parentNode.style.width = ''; this._updateFullScreen(); } else { while (container.tagName != "BODY") { container.style.position = nodeStack.shift(); container = container.parentNode; } this.getDom().style.cssText = this._bakCssText; this.getDom('iframeholder').style.cssText = this._bakCssText1; if (this._bakAutoHeight) { editor.autoHeightEnabled = true; this.editor.enableAutoHeight(); } document.documentElement.style.overflow = this._bakHtmlOverflow; document.body.style.overflow = this._bakBodyOverflow; editor.iframe.parentNode.style.width = this._bakEditorContaninerWidth + 'px'; window.scrollTo(0, this._bakScrollTop); } if (browser.gecko && editor.body.contentEditable === 'true') { var input = document.createElement('input'); document.body.appendChild(input); editor.body.contentEditable = false; setTimeout(function () { input.focus(); setTimeout(function () { editor.body.contentEditable = true; editor.fireEvent('fullscreenchanged', fullscreen); editor.selection.getRange().moveToBookmark(bk).select(true); baidu.editor.dom.domUtils.remove(input); fullscreen && window.scroll(0, 0); }, 0) }, 0) } if(editor.body.contentEditable === 'true'){ this.editor.fireEvent('fullscreenchanged', fullscreen); this.triggerLayout(); } } }, _updateFullScreen:function () { if (this._fullscreen) { var vpRect = uiUtils.getViewportRect(); this.getDom().style.cssText = 'border:0;position:absolute;left:0;top:' + (this.editor.options.topOffset || 0) + 'px;width:' + vpRect.width + 'px;height:' + vpRect.height + 'px;z-index:' + (this.getDom().style.zIndex * 1 + 100); uiUtils.setViewportOffset(this.getDom(), { left:0, top:this.editor.options.topOffset || 0 }); this.editor.setHeight(vpRect.height - this.getDom('toolbarbox').offsetHeight - this.getDom('bottombar').offsetHeight - (this.editor.options.topOffset || 0),true); //不手动调一下,会导致全屏失效 if(browser.gecko){ try{ window.onresize(); }catch(e){ } } } }, _updateElementPath:function () { var bottom = this.getDom('elementpath'), list; if (this.elementPathEnabled && (list = this.editor.queryCommandValue('elementpath'))) { var buff = []; for (var i = 0, ci; ci = list[i]; i++) { buff[i] = this.formatHtml('' + ci + ''); } bottom.innerHTML = '
    ' + this.editor.getLang("elementPathTip") + ': ' + buff.join(' > ') + '
    '; } else { bottom.style.display = 'none' } }, disableElementPath:function () { var bottom = this.getDom('elementpath'); bottom.innerHTML = ''; bottom.style.display = 'none'; this.elementPathEnabled = false; }, enableElementPath:function () { var bottom = this.getDom('elementpath'); bottom.style.display = ''; this.elementPathEnabled = true; this._updateElementPath(); }, _scale:function () { var doc = document, editor = this.editor, editorHolder = editor.container, editorDocument = editor.document, toolbarBox = this.getDom("toolbarbox"), bottombar = this.getDom("bottombar"), scale = this.getDom("scale"), scalelayer = this.getDom("scalelayer"); var isMouseMove = false, position = null, minEditorHeight = 0, minEditorWidth = editor.options.minFrameWidth, pageX = 0, pageY = 0, scaleWidth = 0, scaleHeight = 0; function down() { position = domUtils.getXY(editorHolder); if (!minEditorHeight) { minEditorHeight = editor.options.minFrameHeight + toolbarBox.offsetHeight + bottombar.offsetHeight; } scalelayer.style.cssText = "position:absolute;left:0;display:;top:0;background-color:#41ABFF;opacity:0.4;filter: Alpha(opacity=40);width:" + editorHolder.offsetWidth + "px;height:" + editorHolder.offsetHeight + "px;z-index:" + (editor.options.zIndex + 1); domUtils.on(doc, "mousemove", move); domUtils.on(editorDocument, "mouseup", up); domUtils.on(doc, "mouseup", up); } var me = this; //by xuheng 全屏时关掉缩放 this.editor.addListener('fullscreenchanged', function (e, fullScreen) { if (fullScreen) { me.disableScale(); } else { if (me.editor.options.scaleEnabled) { me.enableScale(); var tmpNode = me.editor.document.createElement('span'); me.editor.body.appendChild(tmpNode); me.editor.body.style.height = Math.max(domUtils.getXY(tmpNode).y, me.editor.iframe.offsetHeight - 20) + 'px'; domUtils.remove(tmpNode) } } }); function move(event) { clearSelection(); var e = event || window.event; pageX = e.pageX || (doc.documentElement.scrollLeft + e.clientX); pageY = e.pageY || (doc.documentElement.scrollTop + e.clientY); scaleWidth = pageX - position.x; scaleHeight = pageY - position.y; if (scaleWidth >= minEditorWidth) { isMouseMove = true; scalelayer.style.width = scaleWidth + 'px'; } if (scaleHeight >= minEditorHeight) { isMouseMove = true; scalelayer.style.height = scaleHeight + "px"; } } function up() { if (isMouseMove) { isMouseMove = false; editor.ui._actualFrameWidth = scalelayer.offsetWidth - 2; editorHolder.style.width = editor.ui._actualFrameWidth + 'px'; editor.setHeight(scalelayer.offsetHeight - bottombar.offsetHeight - toolbarBox.offsetHeight - 2,true); } if (scalelayer) { scalelayer.style.display = "none"; } clearSelection(); domUtils.un(doc, "mousemove", move); domUtils.un(editorDocument, "mouseup", up); domUtils.un(doc, "mouseup", up); } function clearSelection() { if (browser.ie) doc.selection.clear(); else window.getSelection().removeAllRanges(); } this.enableScale = function () { //trace:2868 if (editor.queryCommandState("source") == 1) return; scale.style.display = ""; this.scaleEnabled = true; domUtils.on(scale, "mousedown", down); }; this.disableScale = function () { scale.style.display = "none"; this.scaleEnabled = false; domUtils.un(scale, "mousedown", down); }; }, isFullScreen:function () { return this._fullscreen; }, postRender:function () { UIBase.prototype.postRender.call(this); for (var i = 0; i < this.toolbars.length; i++) { this.toolbars[i].postRender(); } var me = this; var timerId, domUtils = baidu.editor.dom.domUtils, updateFullScreenTime = function () { clearTimeout(timerId); timerId = setTimeout(function () { me._updateFullScreen(); }); }; domUtils.on(window, 'resize', updateFullScreenTime); me.addListener('destroy', function () { domUtils.un(window, 'resize', updateFullScreenTime); clearTimeout(timerId); }) }, showToolbarMsg:function (msg, flag) { this.getDom('toolbarmsg_label').innerHTML = msg; this.getDom('toolbarmsg').style.display = ''; // if (!flag) { var w = this.getDom('upload_dialog'); w.style.display = 'none'; } }, hideToolbarMsg:function () { this.getDom('toolbarmsg').style.display = 'none'; }, mapUrl:function (url) { return url ? url.replace('~/', this.editor.options.UEDITOR_HOME_URL || '') : '' }, triggerLayout:function () { var dom = this.getDom(); if (dom.style.zoom == '1') { dom.style.zoom = '100%'; } else { dom.style.zoom = '1'; } } }; utils.inherits(EditorUI, baidu.editor.ui.UIBase); var instances = {}; UE.ui.Editor = function (options) { var editor = new UE.Editor(options); editor.options.editor = editor; utils.loadFile(document, { href:editor.options.themePath + editor.options.theme + "/css/ueditor.css", tag:"link", type:"text/css", rel:"stylesheet" }); var oldRender = editor.render; editor.render = function (holder) { if (holder.constructor === String) { editor.key = holder; instances[holder] = editor; } utils.domReady(function () { editor.langIsReady ? renderUI() : editor.addListener("langReady", renderUI); function renderUI() { editor.setOpt({ labelMap:editor.options.labelMap || editor.getLang('labelMap') }); new EditorUI(editor.options); if (holder) { if (holder.constructor === String) { holder = document.getElementById(holder); } holder && holder.getAttribute('name') && ( editor.options.textarea = holder.getAttribute('name')); if (holder && /script|textarea/ig.test(holder.tagName)) { var newDiv = document.createElement('div'); holder.parentNode.insertBefore(newDiv, holder); var cont = holder.value || holder.innerHTML; editor.options.initialContent = /^[\t\r\n ]*$/.test(cont) ? editor.options.initialContent : cont.replace(/>[\n\r\t]+([ ]{4})+/g, '>') .replace(/[\n\r\t]+([ ]{4})+[\n\r\t]+<'); holder.className && (newDiv.className = holder.className); holder.style.cssText && (newDiv.style.cssText = holder.style.cssText); if (/textarea/i.test(holder.tagName)) { editor.textarea = holder; editor.textarea.style.display = 'none'; } else { holder.parentNode.removeChild(holder); } if(holder.id){ newDiv.id = holder.id; domUtils.removeAttributes(holder,'id'); } holder = newDiv; holder.innerHTML = ''; } } domUtils.addClass(holder, "edui-" + editor.options.theme); editor.ui.render(holder); var opt = editor.options; //给实例添加一个编辑器的容器引用 editor.container = editor.ui.getDom(); var parents = domUtils.findParents(holder,true); var displays = []; for(var i = 0 ,ci;ci=parents[i];i++){ displays[i] = ci.style.display; ci.style.display = 'block' } if (opt.initialFrameWidth) { opt.minFrameWidth = opt.initialFrameWidth; } else { opt.minFrameWidth = opt.initialFrameWidth = holder.offsetWidth; var styleWidth = holder.style.width; if(/%$/.test(styleWidth)) { opt.initialFrameWidth = styleWidth; } } if (opt.initialFrameHeight) { opt.minFrameHeight = opt.initialFrameHeight; } else { opt.initialFrameHeight = opt.minFrameHeight = holder.offsetHeight; } for(var i = 0 ,ci;ci=parents[i];i++){ ci.style.display = displays[i] } //编辑器最外容器设置了高度,会导致,编辑器不占位 //todo 先去掉,没有找到原因 if(holder.style.height){ holder.style.height = '' } editor.container.style.width = opt.initialFrameWidth + (/%$/.test(opt.initialFrameWidth) ? '' : 'px'); editor.container.style.zIndex = opt.zIndex; oldRender.call(editor, editor.ui.getDom('iframeholder')); editor.fireEvent("afteruiready"); } }) }; return editor; }; /** * @file * @name UE * @short UE * @desc UEditor的顶部命名空间 */ /** * @name getEditor * @since 1.2.4+ * @grammar UE.getEditor(id,[opt]) => Editor实例 * @desc 提供一个全局的方法得到编辑器实例 * * * ''id'' 放置编辑器的容器id, 如果容器下的编辑器已经存在,就直接返回 * * ''opt'' 编辑器的可选参数 * @example * UE.getEditor('containerId',{onready:function(){//创建一个编辑器实例 * this.setContent('hello') * }}); * UE.getEditor('containerId'); //返回刚创建的实例 * */ UE.getEditor = function (id, opt) { var editor = instances[id]; if (!editor) { editor = instances[id] = new UE.ui.Editor(opt); editor.render(id); } return editor; }; UE.delEditor = function (id) { var editor; if (editor = instances[id]) { editor.key && editor.destroy(); delete instances[id] } }; UE.registerUI = function(uiName,fn,index,editorId){ utils.each(uiName.split(/\s+/), function (name) { UE._customizeUI[name] = { id : editorId, execFn:fn, index:index }; }) } })(); // adapter/message.js UE.registerUI('message', function(editor) { var editorui = baidu.editor.ui; var Message = editorui.Message; var holder; var _messageItems = []; var me = editor; me.addListener('ready', function(){ holder = document.getElementById(me.ui.id + '_message_holder'); updateHolderPos(); // HaoChuan9421 // setTimeout(function(){ // updateHolderPos(); // }, 500); }); me.addListener('showmessage', function(type, opt){ opt = utils.isString(opt) ? { 'content': opt } : opt; var message = new Message({ 'timeout': opt.timeout, 'type': opt.type, 'content': opt.content, 'keepshow': opt.keepshow, 'editor': me }), mid = opt.id || ('msg_' + (+new Date()).toString(36)); message.render(holder); _messageItems[mid] = message; message.reset(opt); updateHolderPos(); return mid; }); me.addListener('updatemessage',function(type, id, opt){ opt = utils.isString(opt) ? { 'content': opt } : opt; var message = _messageItems[id]; message.render(holder); message && message.reset(opt); }); me.addListener('hidemessage',function(type, id){ var message = _messageItems[id]; message && message.hide(); }); function updateHolderPos(){ var toolbarbox = me.ui.getDom('toolbarbox'); if (toolbarbox) { holder.style.top = toolbarbox.offsetHeight + 3 + 'px'; } holder.style.zIndex = Math.max(me.options.zIndex, me.iframe.style.zIndex) + 1; } }); // adapter/autosave.js UE.registerUI('autosave', function(editor) { var timer = null,uid = null; editor.on('afterautosave',function(){ clearTimeout(timer); timer = setTimeout(function(){ if(uid){ editor.trigger('hidemessage',uid); } uid = editor.trigger('showmessage',{ content : editor.getLang('autosave.success'), timeout : 2000 }); },2000) }) }); })(); ================================================ FILE: yshop-drink-vue3/public/UEditor22/ueditor.config.js ================================================ /** * ueditor完整配置项 * 可以在这里配置整个编辑器的特性 */ /**************************提示******************************** * 所有被注释的配置项均为UEditor默认值。 * 修改默认配置请首先确保已经完全明确该参数的真实用途。 * 主要有两种修改方案,一种是取消此处注释,然后修改成对应参数;另一种是在实例化编辑器时传入对应参数。 * 当升级编辑器时,可直接使用旧版配置文件替换新版配置文件,不用担心旧版配置文件中因缺少新功能所需的参数而导致脚本报错。 **************************提示********************************/ (function () { /** * 编辑器资源文件根路径。它所表示的含义是:以编辑器实例化页面为当前路径,指向编辑器资源文件(即dialog等文件夹)的路径。 * 鉴于很多同学在使用编辑器的时候出现的种种路径问题,此处强烈建议大家使用"相对于网站根目录的相对路径"进行配置。 * "相对于网站根目录的相对路径"也就是以斜杠开头的形如"/myProject/ueditor/"这样的路径。 * 如果站点中有多个不在同一层级的页面需要实例化编辑器,且引用了同一UEditor的时候,此处的URL可能不适用于每个页面的编辑器。 * 因此,UEditor提供了针对不同页面的编辑器可单独配置的根路径,具体来说,在需要实例化编辑器的页面最顶部写上如下代码即可。当然,需要令此处的URL等于对应的配置。 * window.UEDITOR_HOME_URL = "/xxxx/xxxx/"; */ var URL = window.UEDITOR_HOME_URL || getUEBasePath(); /** * 配置项主体。注意,此处所有涉及到路径的配置别遗漏URL变量。 */ window.UEDITOR_CONFIG = { //为编辑器实例添加一个路径,这个不能被注释 UEDITOR_HOME_URL: URL // 服务器统一请求接口路径 , serverUrl: URL + "php/controller.php" //工具栏上的所有的功能按钮和下拉框,可以在new编辑器的实例时选择自己需要的重新定义 , toolbars: [[ 'fullscreen', 'source', '|', 'undo', 'redo', '|', 'bold', 'italic', 'underline', 'fontborder', 'strikethrough', 'superscript', 'subscript', 'removeformat', 'formatmatch', 'autotypeset', 'blockquote', 'pasteplain', '|', 'forecolor', 'backcolor', 'insertorderedlist', 'insertunorderedlist', 'selectall', 'cleardoc', '|', 'rowspacingtop', 'rowspacingbottom', 'lineheight', '|', 'customstyle', 'paragraph', 'fontfamily', 'fontsize', '|', 'directionalityltr', 'directionalityrtl', 'indent', '|', 'justifyleft', 'justifycenter', 'justifyright', 'justifyjustify', '|', 'touppercase', 'tolowercase', '|', 'link', 'unlink', 'anchor', '|', 'imagenone', 'imageleft', 'imageright', 'imagecenter', '|', 'simpleupload', 'insertimage', 'emotion', 'scrawl', 'insertvideo', 'music', 'attachment', 'map', 'gmap', 'insertframe', 'insertcode', 'webapp', 'pagebreak', 'template', 'background', '|', 'horizontal', 'date', 'time', 'spechars', 'snapscreen', 'wordimage', '|', 'inserttable', 'deletetable', 'insertparagraphbeforetable', 'insertrow', 'deleterow', 'insertcol', 'deletecol', 'mergecells', 'mergeright', 'mergedown', 'splittocells', 'splittorows', 'splittocols', 'charts', '|', 'print', 'preview', 'searchreplace', 'drafts', 'help' ]] //当鼠标放在工具栏上时显示的tooltip提示,留空支持自动多语言配置,否则以配置值为准 //,labelMap:{ // 'anchor':'', 'undo':'' //} //语言配置项,默认是zh-cn。有需要的话也可以使用如下这样的方式来自动多语言切换,当然,前提条件是lang文件夹下存在对应的语言文件: //lang值也可以通过自动获取 (navigator.language||navigator.browserLanguage ||navigator.userLanguage).toLowerCase() //,lang:"zh-cn" //,langPath:URL +"lang/" //主题配置项,默认是default。有需要的话也可以使用如下这样的方式来自动多主题切换,当然,前提条件是themes文件夹下存在对应的主题文件: //现有如下皮肤:default //,theme:'default' //,themePath:URL +"themes/" //,zIndex : 900 //编辑器层级的基数,默认是900 //针对getAllHtml方法,会在对应的head标签中增加该编码设置。 //,charset:"utf-8" //若实例化编辑器的页面手动修改的domain,此处需要设置为true //,customDomain:false //常用配置项目 //,isShow : true //默认显示编辑器 //,textarea:'editorValue' // 提交表单时,服务器获取编辑器提交内容的所用的参数,多实例时可以给容器name属性,会将name给定的值最为每个实例的键值,不用每次实例化的时候都设置这个值 //,initialContent:'欢迎使用ueditor!' //初始化编辑器的内容,也可以通过textarea/script给值,看官网例子 //,autoClearinitialContent:true //是否自动清除编辑器初始内容,注意:如果focus属性设置为true,这个也为真,那么编辑器一上来就会触发导致初始化的内容看不到了 //,focus:false //初始化时,是否让编辑器获得焦点true或false //如果自定义,最好给p标签如下的行高,要不输入中文时,会有跳动感 //,initialStyle:'p{line-height:1em}'//编辑器层级的基数,可以用来改变字体等 //,iframeCssUrl: URL + '/themes/iframe.css' //给编辑区域的iframe引入一个css文件 //indentValue //首行缩进距离,默认是2em //,indentValue:'2em' //,initialFrameWidth:1000 //初始化编辑器宽度,默认1000 //,initialFrameHeight:320 //初始化编辑器高度,默认320 //,readonly : false //编辑器初始化结束后,编辑区域是否是只读的,默认是false //,autoClearEmptyNode : true //getContent时,是否删除空的inlineElement节点(包括嵌套的情况) //启用自动保存 //,enableAutoSave: true //自动保存间隔时间, 单位ms //,saveInterval: 500 //,fullscreen : false //是否开启初始化时即全屏,默认关闭 //,imagePopup:true //图片操作的浮层开关,默认打开 //,autoSyncData:true //自动同步编辑器要提交的数据 //,emotionLocalization:false //是否开启表情本地化,默认关闭。若要开启请确保emotion文件夹下包含官网提供的images表情文件夹 //粘贴只保留标签,去除标签所有属性 //,retainOnlyLabelPasted: false //,pasteplain:false //是否默认为纯文本粘贴。false为不使用纯文本粘贴,true为使用纯文本粘贴 //纯文本粘贴模式下的过滤规则 //'filterTxtRules' : function(){ // function transP(node){ // node.tagName = 'p'; // node.setStyle(); // } // return { // //直接删除及其字节点内容 // '-' : 'script style object iframe embed input select', // 'p': {$:{}}, // 'br':{$:{}}, // 'div':{'$':{}}, // 'li':{'$':{}}, // 'caption':transP, // 'th':transP, // 'tr':transP, // 'h1':transP,'h2':transP,'h3':transP,'h4':transP,'h5':transP,'h6':transP, // 'td':function(node){ // //没有内容的td直接删掉 // var txt = !!node.innerText(); // if(txt){ // node.parentNode.insertAfter(UE.uNode.createText('    '),node); // } // node.parentNode.removeChild(node,node.innerText()) // } // } //}() //,allHtmlEnabled:false //提交到后台的数据是否包含整个html字符串 //insertorderedlist //有序列表的下拉配置,值留空时支持多语言自动识别,若配置值,则以此值为准 //,'insertorderedlist':{ // //自定的样式 // 'num':'1,2,3...', // 'num1':'1),2),3)...', // 'num2':'(1),(2),(3)...', // 'cn':'一,二,三....', // 'cn1':'一),二),三)....', // 'cn2':'(一),(二),(三)....', // //系统自带 // 'decimal' : '' , //'1,2,3...' // 'lower-alpha' : '' , // 'a,b,c...' // 'lower-roman' : '' , //'i,ii,iii...' // 'upper-alpha' : '' , lang //'A,B,C' // 'upper-roman' : '' //'I,II,III...' //} //insertunorderedlist //无序列表的下拉配置,值留空时支持多语言自动识别,若配置值,则以此值为准 //,insertunorderedlist : { //自定的样式 // 'dash' :'— 破折号', //-破折号 // 'dot':' 。 小圆圈', //系统自带 // 'circle' : '', // '○ 小圆圈' // 'disc' : '', // '● 小圆点' // 'square' : '' //'■ 小方块' //} //,listDefaultPaddingLeft : '30'//默认的左边缩进的基数倍 //,listiconpath : 'http://bs.baidu.com/listicon/'//自定义标号的路径 //,maxListLevel : 3 //限制可以tab的级数, 设置-1为不限制 //,autoTransWordToList:false //禁止word中粘贴进来的列表自动变成列表标签 //fontfamily //字体设置 label留空支持多语言自动切换,若配置,则以配置值为准 //,'fontfamily':[ // { label:'',name:'songti',val:'宋体,SimSun'}, // { label:'',name:'kaiti',val:'楷体,楷体_GB2312, SimKai'}, // { label:'',name:'yahei',val:'微软雅黑,Microsoft YaHei'}, // { label:'',name:'heiti',val:'黑体, SimHei'}, // { label:'',name:'lishu',val:'隶书, SimLi'}, // { label:'',name:'andaleMono',val:'andale mono'}, // { label:'',name:'arial',val:'arial, helvetica,sans-serif'}, // { label:'',name:'arialBlack',val:'arial black,avant garde'}, // { label:'',name:'comicSansMs',val:'comic sans ms'}, // { label:'',name:'impact',val:'impact,chicago'}, // { label:'',name:'timesNewRoman',val:'times new roman'} //] //fontsize //字号 //,'fontsize':[10, 11, 12, 14, 16, 18, 20, 24, 36] //paragraph //段落格式 值留空时支持多语言自动识别,若配置,则以配置值为准 //,'paragraph':{'p':'', 'h1':'', 'h2':'', 'h3':'', 'h4':'', 'h5':'', 'h6':''} //rowspacingtop //段间距 值和显示的名字相同 //,'rowspacingtop':['5', '10', '15', '20', '25'] //rowspacingBottom //段间距 值和显示的名字相同 //,'rowspacingbottom':['5', '10', '15', '20', '25'] //lineheight //行内间距 值和显示的名字相同 //,'lineheight':['1', '1.5','1.75','2', '3', '4', '5'] //customstyle //自定义样式,不支持国际化,此处配置值即可最后显示值 //block的元素是依据设置段落的逻辑设置的,inline的元素依据BIU的逻辑设置 //尽量使用一些常用的标签 //参数说明 //tag 使用的标签名字 //label 显示的名字也是用来标识不同类型的标识符,注意这个值每个要不同, //style 添加的样式 //每一个对象就是一个自定义的样式 //,'customstyle':[ // {tag:'h1', name:'tc', label:'', style:'border-bottom:#ccc 2px solid;padding:0 4px 0 0;text-align:center;margin:0 0 20px 0;'}, // {tag:'h1', name:'tl',label:'', style:'border-bottom:#ccc 2px solid;padding:0 4px 0 0;margin:0 0 10px 0;'}, // {tag:'span',name:'im', label:'', style:'font-style:italic;font-weight:bold'}, // {tag:'span',name:'hi', label:'', style:'font-style:italic;font-weight:bold;color:rgb(51, 153, 204)'} //] //打开右键菜单功能 //,enableContextMenu: true //右键菜单的内容,可以参考plugins/contextmenu.js里边的默认菜单的例子,label留空支持国际化,否则以此配置为准 //,contextMenu:[ // { // label:'', //显示的名称 // cmdName:'selectall',//执行的command命令,当点击这个右键菜单时 // //exec可选,有了exec就会在点击时执行这个function,优先级高于cmdName // exec:function () { // //this是当前编辑器的实例 // //this.ui._dialogs['inserttableDialog'].open(); // } // } //] //快捷菜单 //,shortcutMenu:["fontfamily", "fontsize", "bold", "italic", "underline", "forecolor", "backcolor", "insertorderedlist", "insertunorderedlist"] //elementPathEnabled //是否启用元素路径,默认是显示 //,elementPathEnabled : true //wordCount //,wordCount:true //是否开启字数统计 //,maximumWords:10000 //允许的最大字符数 //字数统计提示,{#count}代表当前字数,{#leave}代表还可以输入多少字符数,留空支持多语言自动切换,否则按此配置显示 //,wordCountMsg:'' //当前已输入 {#count} 个字符,您还可以输入{#leave} 个字符 //超出字数限制提示 留空支持多语言自动切换,否则按此配置显示 //,wordOverFlowMsg:'' //你输入的字符个数已经超出最大允许值,服务器可能会拒绝保存! //tab //点击tab键时移动的距离,tabSize倍数,tabNode什么字符做为单位 //,tabSize:4 //,tabNode:' ' //removeFormat //清除格式时可以删除的标签和属性 //removeForamtTags标签 //,removeFormatTags:'b,big,code,del,dfn,em,font,i,ins,kbd,q,samp,small,span,strike,strong,sub,sup,tt,u,var' //removeFormatAttributes属性 //,removeFormatAttributes:'class,style,lang,width,height,align,hspace,valign' //undo //可以最多回退的次数,默认20 //,maxUndoCount:20 //当输入的字符数超过该值时,保存一次现场 //,maxInputCount:1 //autoHeightEnabled // 是否自动长高,默认true //,autoHeightEnabled:true //scaleEnabled //是否可以拉伸长高,默认true(当开启时,自动长高失效) //,scaleEnabled:false //,minFrameWidth:800 //编辑器拖动时最小宽度,默认800 //,minFrameHeight:220 //编辑器拖动时最小高度,默认220 //autoFloatEnabled //是否保持toolbar的位置不动,默认true //,autoFloatEnabled:true //浮动时工具栏距离浏览器顶部的高度,用于某些具有固定头部的页面 //,topOffset:30 //编辑器底部距离工具栏高度(如果参数大于等于编辑器高度,则设置无效) //,toolbarTopOffset:400 //设置远程图片是否抓取到本地保存 //,catchRemoteImageEnable: true //设置是否抓取远程图片 //pageBreakTag //分页标识符,默认是_ueditor_page_break_tag_ //,pageBreakTag:'_ueditor_page_break_tag_' //autotypeset //自动排版参数 //,autotypeset: { // mergeEmptyline: true, //合并空行 // removeClass: true, //去掉冗余的class // removeEmptyline: false, //去掉空行 // textAlign:"left", //段落的排版方式,可以是 left,right,center,justify 去掉这个属性表示不执行排版 // imageBlockLine: 'center', //图片的浮动方式,独占一行剧中,左右浮动,默认: center,left,right,none 去掉这个属性表示不执行排版 // pasteFilter: false, //根据规则过滤没事粘贴进来的内容 // clearFontSize: false, //去掉所有的内嵌字号,使用编辑器默认的字号 // clearFontFamily: false, //去掉所有的内嵌字体,使用编辑器默认的字体 // removeEmptyNode: false, // 去掉空节点 // //可以去掉的标签 // removeTagNames: {标签名字:1}, // indent: false, // 行首缩进 // indentValue : '2em', //行首缩进的大小 // bdc2sb: false, // tobdc: false //} //tableDragable //表格是否可以拖拽 //,tableDragable: true //sourceEditor //源码的查看方式,codemirror 是代码高亮,textarea是文本框,默认是codemirror //注意默认codemirror只能在ie8+和非ie中使用 //,sourceEditor:"codemirror" //如果sourceEditor是codemirror,还用配置一下两个参数 //codeMirrorJsUrl js加载的路径,默认是 URL + "third-party/codemirror/codemirror.js" //,codeMirrorJsUrl:URL + "third-party/codemirror/codemirror.js" //codeMirrorCssUrl css加载的路径,默认是 URL + "third-party/codemirror/codemirror.css" //,codeMirrorCssUrl:URL + "third-party/codemirror/codemirror.css" //编辑器初始化完成后是否进入源码模式,默认为否。 //,sourceEditorFirst:false //iframeUrlMap //dialog内容的路径 ~会被替换成URL,垓属性一旦打开,将覆盖所有的dialog的默认路径 //,iframeUrlMap:{ // 'anchor':'~/dialogs/anchor/anchor.html', //} //allowLinkProtocol 允许的链接地址,有这些前缀的链接地址不会自动添加http //, allowLinkProtocols: ['http:', 'https:', '#', '/', 'ftp:', 'mailto:', 'tel:', 'git:', 'svn:'] //webAppKey 百度应用的APIkey,每个站长必须首先去百度官网注册一个key后方能正常使用app功能,注册介绍,http://app.baidu.com/static/cms/getapikey.html //, webAppKey: "" //默认过滤规则相关配置项目 //,disabledTableInTable:true //禁止表格嵌套 //,allowDivTransToP:true //允许进入编辑器的div标签自动变成p标签 //,rgb2Hex:true //默认产出的数据中的color自动从rgb格式变成16进制格式 // xss 过滤是否开启,inserthtml等操作 ,xssFilterRules: true //input xss过滤 ,inputXssFilter: true //output xss过滤 ,outputXssFilter: true // xss过滤白名单 名单来源: https://raw.githubusercontent.com/leizongmin/js-xss/master/lib/default.js ,whiteList: { a: ['target', 'href', 'title', 'class', 'style'], abbr: ['title', 'class', 'style'], address: ['class', 'style'], area: ['shape', 'coords', 'href', 'alt'], article: [], aside: [], audio: ['autoplay', 'controls', 'loop', 'preload', 'src', 'class', 'style'], b: ['class', 'style'], bdi: ['dir'], bdo: ['dir'], big: [], blockquote: ['cite', 'class', 'style'], br: [], caption: ['class', 'style'], center: [], cite: [], code: ['class', 'style'], col: ['align', 'valign', 'span', 'width', 'class', 'style'], colgroup: ['align', 'valign', 'span', 'width', 'class', 'style'], dd: ['class', 'style'], del: ['datetime'], details: ['open'], div: ['class', 'style'], dl: ['class', 'style'], dt: ['class', 'style'], em: ['class', 'style'], font: ['color', 'size', 'face'], footer: [], h1: ['class', 'style'], h2: ['class', 'style'], h3: ['class', 'style'], h4: ['class', 'style'], h5: ['class', 'style'], h6: ['class', 'style'], header: [], hr: [], i: ['class', 'style'], img: ['src', 'alt', 'title', 'width', 'height', 'id', '_src', 'loadingclass', 'class', 'data-latex'], ins: ['datetime'], li: ['class', 'style'], mark: [], nav: [], ol: ['class', 'style'], p: ['class', 'style'], pre: ['class', 'style'], s: [], section:[], small: [], span: ['class', 'style'], sub: ['class', 'style'], sup: ['class', 'style'], strong: ['class', 'style'], table: ['width', 'border', 'align', 'valign', 'class', 'style'], tbody: ['align', 'valign', 'class', 'style'], td: ['width', 'rowspan', 'colspan', 'align', 'valign', 'class', 'style'], tfoot: ['align', 'valign', 'class', 'style'], th: ['width', 'rowspan', 'colspan', 'align', 'valign', 'class', 'style'], thead: ['align', 'valign', 'class', 'style'], tr: ['rowspan', 'align', 'valign', 'class', 'style'], tt: [], u: [], ul: ['class', 'style'], video: ['autoplay', 'controls', 'loop', 'preload', 'src', 'height', 'width', 'class', 'style'] } }; function getUEBasePath(docUrl, confUrl) { return getBasePath(docUrl || self.document.URL || self.location.href, confUrl || getConfigFilePath()); } function getConfigFilePath() { var configPath = document.getElementsByTagName('script'); return configPath[ configPath.length - 1 ].src; } function getBasePath(docUrl, confUrl) { var basePath = confUrl; if (/^(\/|\\\\)/.test(confUrl)) { basePath = /^.+?\w(\/|\\\\)/.exec(docUrl)[0] + confUrl.replace(/^(\/|\\\\)/, ''); } else if (!/^[a-z]+:/i.test(confUrl)) { docUrl = docUrl.split("#")[0].split("?")[0].replace(/[^\\\/]+$/, ''); basePath = docUrl + "" + confUrl; } return optimizationPath(basePath); } function optimizationPath(path) { var protocol = /^[a-z]+:\/\//.exec(path)[ 0 ], tmp = null, res = []; path = path.replace(protocol, "").split("?")[0].split("#")[0]; path = path.replace(/\\/g, '/').split(/\//); path[ path.length - 1 ] = ""; while (path.length) { if (( tmp = path.shift() ) === "..") { res.pop(); } else if (tmp !== ".") { res.push(tmp); } } return protocol + res.join("/"); } window.UE = { getUEBasePath: getUEBasePath }; })(); ================================================ FILE: yshop-drink-vue3/public/UEditor22/ueditor.parse.js ================================================ /*! * UEditor * version: ueditor * build: Wed Dec 26 2018 17:25:05 GMT+0800 (CST) */ (function(){ (function(){ UE = window.UE || {}; var isIE = !!window.ActiveXObject; //定义utils工具 var utils = { removeLastbs : function(url){ return url.replace(/\/$/,'') }, extend : function(t,s){ var a = arguments, notCover = this.isBoolean(a[a.length - 1]) ? a[a.length - 1] : false, len = this.isBoolean(a[a.length - 1]) ? a.length - 1 : a.length; for (var i = 1; i < len; i++) { var x = a[i]; for (var k in x) { if (!notCover || !t.hasOwnProperty(k)) { t[k] = x[k]; } } } return t; }, isIE : isIE, cssRule : isIE ? function(key,style,doc){ var indexList,index; doc = doc || document; if(doc.indexList){ indexList = doc.indexList; }else{ indexList = doc.indexList = {}; } var sheetStyle; if(!indexList[key]){ if(style === undefined){ return '' } sheetStyle = doc.createStyleSheet('',index = doc.styleSheets.length); indexList[key] = index; }else{ sheetStyle = doc.styleSheets[indexList[key]]; } if(style === undefined){ return sheetStyle.cssText } sheetStyle.cssText = sheetStyle.cssText + '\n' + (style || '') } : function(key,style,doc){ doc = doc || document; var head = doc.getElementsByTagName('head')[0],node; if(!(node = doc.getElementById(key))){ if(style === undefined){ return '' } node = doc.createElement('style'); node.id = key; head.appendChild(node) } if(style === undefined){ return node.innerHTML } if(style !== ''){ node.innerHTML = node.innerHTML + '\n' + style; }else{ head.removeChild(node) } }, domReady : function (onready) { var doc = window.document; if (doc.readyState === "complete") { onready(); }else{ if (isIE) { (function () { if (doc.isReady) return; try { doc.documentElement.doScroll("left"); } catch (error) { setTimeout(arguments.callee, 0); return; } onready(); })(); window.attachEvent('onload', function(){ onready() }); } else { doc.addEventListener("DOMContentLoaded", function () { doc.removeEventListener("DOMContentLoaded", arguments.callee, false); onready(); }, false); window.addEventListener('load', function(){onready()}, false); } } }, each : function(obj, iterator, context) { if (obj == null) return; if (obj.length === +obj.length) { for (var i = 0, l = obj.length; i < l; i++) { if(iterator.call(context, obj[i], i, obj) === false) return false; } } else { for (var key in obj) { if (obj.hasOwnProperty(key)) { if(iterator.call(context, obj[key], key, obj) === false) return false; } } } }, inArray : function(arr,item){ var index = -1; this.each(arr,function(v,i){ if(v === item){ index = i; return false; } }); return index; }, pushItem : function(arr,item){ if(this.inArray(arr,item)==-1){ arr.push(item) } }, trim: function (str) { return str.replace(/(^[ \t\n\r]+)|([ \t\n\r]+$)/g, ''); }, indexOf: function (array, item, start) { var index = -1; start = this.isNumber(start) ? start : 0; this.each(array, function (v, i) { if (i >= start && v === item) { index = i; return false; } }); return index; }, hasClass: function (element, className) { className = className.replace(/(^[ ]+)|([ ]+$)/g, '').replace(/[ ]{2,}/g, ' ').split(' '); for (var i = 0, ci, cls = element.className; ci = className[i++];) { if (!new RegExp('\\b' + ci + '\\b', 'i').test(cls)) { return false; } } return i - 1 == className.length; }, addClass:function (elm, classNames) { if(!elm)return; classNames = this.trim(classNames).replace(/[ ]{2,}/g,' ').split(' '); for(var i = 0,ci,cls = elm.className;ci=classNames[i++];){ if(!new RegExp('\\b' + ci + '\\b').test(cls)){ cls += ' ' + ci; } } elm.className = utils.trim(cls); }, removeClass:function (elm, classNames) { classNames = this.isArray(classNames) ? classNames : this.trim(classNames).replace(/[ ]{2,}/g,' ').split(' '); for(var i = 0,ci,cls = elm.className;ci=classNames[i++];){ cls = cls.replace(new RegExp('\\b' + ci + '\\b'),'') } cls = this.trim(cls).replace(/[ ]{2,}/g,' '); elm.className = cls; !cls && elm.removeAttribute('className'); }, on: function (element, type, handler) { var types = this.isArray(type) ? type : type.split(/\s+/), k = types.length; if (k) while (k--) { type = types[k]; if (element.addEventListener) { element.addEventListener(type, handler, false); } else { if (!handler._d) { handler._d = { els : [] }; } var key = type + handler.toString(),index = utils.indexOf(handler._d.els,element); if (!handler._d[key] || index == -1) { if(index == -1){ handler._d.els.push(element); } if(!handler._d[key]){ handler._d[key] = function (evt) { return handler.call(evt.srcElement, evt || window.event); }; } element.attachEvent('on' + type, handler._d[key]); } } } element = null; }, off: function (element, type, handler) { var types = this.isArray(type) ? type : type.split(/\s+/), k = types.length; if (k) while (k--) { type = types[k]; if (element.removeEventListener) { element.removeEventListener(type, handler, false); } else { var key = type + handler.toString(); try{ element.detachEvent('on' + type, handler._d ? handler._d[key] : handler); }catch(e){} if (handler._d && handler._d[key]) { var index = utils.indexOf(handler._d.els,element); if(index!=-1){ handler._d.els.splice(index,1); } handler._d.els.length == 0 && delete handler._d[key]; } } } }, loadFile : function () { var tmpList = []; function getItem(doc,obj){ try{ for(var i= 0,ci;ci=tmpList[i++];){ if(ci.doc === doc && ci.url == (obj.src || obj.href)){ return ci; } } }catch(e){ return null; } } return function (doc, obj, fn) { var item = getItem(doc,obj); if (item) { if(item.ready){ fn && fn(); }else{ item.funs.push(fn) } return; } tmpList.push({ doc:doc, url:obj.src||obj.href, funs:[fn] }); if (!doc.body) { var html = []; for(var p in obj){ if(p == 'tag')continue; html.push(p + '="' + obj[p] + '"') } doc.write('<' + obj.tag + ' ' + html.join(' ') + ' >'); return; } if (obj.id && doc.getElementById(obj.id)) { return; } var element = doc.createElement(obj.tag); delete obj.tag; for (var p in obj) { element.setAttribute(p, obj[p]); } element.onload = element.onreadystatechange = function () { if (!this.readyState || /loaded|complete/.test(this.readyState)) { item = getItem(doc,obj); if (item.funs.length > 0) { item.ready = 1; for (var fi; fi = item.funs.pop();) { fi(); } } element.onload = element.onreadystatechange = null; } }; element.onerror = function(){ throw Error('The load '+(obj.href||obj.src)+' fails,check the url') }; doc.getElementsByTagName("head")[0].appendChild(element); } }() }; utils.each(['String', 'Function', 'Array', 'Number', 'RegExp', 'Object','Boolean'], function (v) { utils['is' + v] = function (obj) { return Object.prototype.toString.apply(obj) == '[object ' + v + ']'; } }); var parselist = {}; UE.parse = { register : function(parseName,fn){ parselist[parseName] = fn; }, load : function(opt){ utils.each(parselist,function(v){ v.call(opt,utils); }) } }; uParse = function(selector,opt){ utils.domReady(function(){ var contents; if(document.querySelectorAll){ contents = document.querySelectorAll(selector) }else{ if(/^#/.test(selector)){ contents = [document.getElementById(selector.replace(/^#/,''))] }else if(/^\./.test(selector)){ var contents = []; utils.each(document.getElementsByTagName('*'),function(node){ if(node.className && new RegExp('\\b' + selector.replace(/^\./,'') + '\\b','i').test(node.className)){ contents.push(node) } }) }else{ contents = document.getElementsByTagName(selector) } } utils.each(contents,function(v){ UE.parse.load(utils.extend({root:v,selector:selector},opt)) }) }) } })(); UE.parse.register('insertcode',function(utils){ var pres = this.root.getElementsByTagName('pre'); if(pres.length){ if(typeof XRegExp == "undefined"){ var jsurl,cssurl; if(this.rootPath !== undefined){ jsurl = utils.removeLastbs(this.rootPath) + '/third-party/SyntaxHighlighter/shCore.js'; cssurl = utils.removeLastbs(this.rootPath) + '/third-party/SyntaxHighlighter/shCoreDefault.css'; }else{ jsurl = this.highlightJsUrl; cssurl = this.highlightCssUrl; } utils.loadFile(document,{ id : "syntaxhighlighter_css", tag : "link", rel : "stylesheet", type : "text/css", href : cssurl }); utils.loadFile(document,{ id : "syntaxhighlighter_js", src : jsurl, tag : "script", type : "text/javascript", defer : "defer" },function(){ utils.each(pres,function(pi){ if(pi && /brush/i.test(pi.className)){ SyntaxHighlighter.highlight(pi); } }); }); }else{ utils.each(pres,function(pi){ if(pi && /brush/i.test(pi.className)){ SyntaxHighlighter.highlight(pi); } }); } } }); UE.parse.register('table', function (utils) { var me = this, root = this.root, tables = root.getElementsByTagName('table'); if (tables.length) { var selector = this.selector; //追加默认的表格样式 utils.cssRule('table', selector + ' table.noBorderTable td,' + selector + ' table.noBorderTable th,' + selector + ' table.noBorderTable caption{border:1px dashed #ddd !important}' + selector + ' table.sortEnabled tr.firstRow th,' + selector + ' table.sortEnabled tr.firstRow td{padding-right:20px; background-repeat: no-repeat;' + 'background-position: center right; background-image:url(' + this.rootPath + 'themes/default/images/sortable.png);}' + selector + ' table.sortEnabled tr.firstRow th:hover,' + selector + ' table.sortEnabled tr.firstRow td:hover{background-color: #EEE;}' + selector + ' table{margin-bottom:10px;border-collapse:collapse;display:table;}' + selector + ' td,' + selector + ' th{ background:white; padding: 5px 10px;border: 1px solid #DDD;}' + selector + ' caption{border:1px dashed #DDD;border-bottom:0;padding:3px;text-align:center;}' + selector + ' th{border-top:1px solid #BBB;background:#F7F7F7;}' + selector + ' table tr.firstRow th{border-top:2px solid #BBB;background:#F7F7F7;}' + selector + ' tr.ue-table-interlace-color-single td{ background: #fcfcfc; }' + selector + ' tr.ue-table-interlace-color-double td{ background: #f7faff; }' + selector + ' td p{margin:0;padding:0;}', document); //填充空的单元格 utils.each('td th caption'.split(' '), function (tag) { var cells = root.getElementsByTagName(tag); cells.length && utils.each(cells, function (node) { if (!node.firstChild) { node.innerHTML = ' '; } }) }); //表格可排序 var tables = root.getElementsByTagName('table'); utils.each(tables, function (table) { if (/\bsortEnabled\b/.test(table.className)) { utils.on(table, 'click', function(e){ var target = e.target || e.srcElement, cell = findParentByTagName(target, ['td', 'th']); var table = findParentByTagName(target, 'table'), colIndex = utils.indexOf(table.rows[0].cells, cell), sortType = table.getAttribute('data-sort-type'); if(colIndex != -1) { sortTable(table, colIndex, me.tableSortCompareFn || sortType); updateTable(table); } }); } }); //按照标签名查找父节点 function findParentByTagName(target, tagNames) { var i, current = target; tagNames = utils.isArray(tagNames) ? tagNames:[tagNames]; while(current){ for(i = 0;i < tagNames.length; i++) { if(current.tagName == tagNames[i].toUpperCase()) return current; } current = current.parentNode; } return null; } //表格排序 function sortTable(table, sortByCellIndex, compareFn) { var rows = table.rows, trArray = [], flag = rows[0].cells[0].tagName === "TH", lastRowIndex = 0; for (var i = 0,len = rows.length; i < len; i++) { trArray[i] = rows[i]; } var Fn = { 'reversecurrent': function(td1,td2){ return 1; }, 'orderbyasc': function(td1,td2){ var value1 = td1.innerText||td1.textContent, value2 = td2.innerText||td2.textContent; return value1.localeCompare(value2); }, 'reversebyasc': function(td1,td2){ var value1 = td1.innerHTML, value2 = td2.innerHTML; return value2.localeCompare(value1); }, 'orderbynum': function(td1,td2){ var value1 = td1[utils.isIE ? 'innerText':'textContent'].match(/\d+/), value2 = td2[utils.isIE ? 'innerText':'textContent'].match(/\d+/); if(value1) value1 = +value1[0]; if(value2) value2 = +value2[0]; return (value1||0) - (value2||0); }, 'reversebynum': function(td1,td2){ var value1 = td1[utils.isIE ? 'innerText':'textContent'].match(/\d+/), value2 = td2[utils.isIE ? 'innerText':'textContent'].match(/\d+/); if(value1) value1 = +value1[0]; if(value2) value2 = +value2[0]; return (value2||0) - (value1||0); } }; //对表格设置排序的标记data-sort-type table.setAttribute('data-sort-type', compareFn && typeof compareFn === "string" && Fn[compareFn] ? compareFn:''); //th不参与排序 flag && trArray.splice(0, 1); trArray = sort(trArray,function (tr1, tr2) { var result; if (compareFn && typeof compareFn === "function") { result = compareFn.call(this, tr1.cells[sortByCellIndex], tr2.cells[sortByCellIndex]); } else if (compareFn && typeof compareFn === "number") { result = 1; } else if (compareFn && typeof compareFn === "string" && Fn[compareFn]) { result = Fn[compareFn].call(this, tr1.cells[sortByCellIndex], tr2.cells[sortByCellIndex]); } else { result = Fn['orderbyasc'].call(this, tr1.cells[sortByCellIndex], tr2.cells[sortByCellIndex]); } return result; }); var fragment = table.ownerDocument.createDocumentFragment(); for (var j = 0, len = trArray.length; j < len; j++) { fragment.appendChild(trArray[j]); } var tbody = table.getElementsByTagName("tbody")[0]; if(!lastRowIndex){ tbody.appendChild(fragment); }else{ tbody.insertBefore(fragment,rows[lastRowIndex- range.endRowIndex + range.beginRowIndex - 1]) } } //冒泡排序 function sort(array, compareFn){ compareFn = compareFn || function(item1, item2){ return item1.localeCompare(item2);}; for(var i= 0,len = array.length; i 0){ var t = array[i]; array[i] = array[j]; array[j] = t; } } } return array; } //更新表格 function updateTable(table) { //给第一行设置firstRow的样式名称,在排序图标的样式上使用到 if(!utils.hasClass(table.rows[0], "firstRow")) { for(var i = 1; i< table.rows.length; i++) { utils.removeClass(table.rows[i], "firstRow"); } utils.addClass(table.rows[0], "firstRow"); } } } }); UE.parse.register('charts',function( utils ){ utils.cssRule('chartsContainerHeight','.edui-chart-container { height:'+(this.chartContainerHeight||300)+'px}'); var resourceRoot = this.rootPath, containers = this.root, sources = null; //不存在指定的根路径, 则直接退出 if ( !resourceRoot ) { return; } if ( sources = parseSources() ) { loadResources(); } function parseSources () { if ( !containers ) { return null; } return extractChartData( containers ); } /** * 提取数据 */ function extractChartData ( rootNode ) { var data = [], tables = rootNode.getElementsByTagName( "table" ); for ( var i = 0, tableNode; tableNode = tables[ i ]; i++ ) { if ( tableNode.getAttribute( "data-chart" ) !== null ) { data.push( formatData( tableNode ) ); } } return data.length ? data : null; } function formatData ( tableNode ) { var meta = tableNode.getAttribute( "data-chart" ), metaConfig = {}, data = []; //提取table数据 for ( var i = 0, row; row = tableNode.rows[ i ]; i++ ) { var rowData = []; for ( var j = 0, cell; cell = row.cells[ j ]; j++ ) { var value = ( cell.innerText || cell.textContent || '' ); rowData.push( cell.tagName == 'TH' ? value:(value | 0) ); } data.push( rowData ); } //解析元信息 meta = meta.split( ";" ); for ( var i = 0, metaData; metaData = meta[ i ]; i++ ) { metaData = metaData.split( ":" ); metaConfig[ metaData[ 0 ] ] = metaData[ 1 ]; } return { table: tableNode, meta: metaConfig, data: data }; } //加载资源 function loadResources () { loadJQuery(); } function loadJQuery () { //不存在jquery, 则加载jquery if ( !window.jQuery ) { utils.loadFile(document,{ src : resourceRoot + "/third-party/jquery-1.10.2.min.js", tag : "script", type : "text/javascript", defer : "defer" },function(){ loadHighcharts(); }); } else { loadHighcharts(); } } function loadHighcharts () { //不存在Highcharts, 则加载Highcharts if ( !window.Highcharts ) { utils.loadFile(document,{ src : resourceRoot + "/third-party/highcharts/highcharts.js", tag : "script", type : "text/javascript", defer : "defer" },function(){ loadTypeConfig(); }); } else { loadTypeConfig(); } } //加载图表差异化配置文件 function loadTypeConfig () { utils.loadFile(document,{ src : resourceRoot + "/dialogs/charts/chart.config.js", tag : "script", type : "text/javascript", defer : "defer" },function(){ render(); }); } //渲染图表 function render () { var config = null, chartConfig = null, container = null; for ( var i = 0, len = sources.length; i < len; i++ ) { config = sources[ i ]; chartConfig = analysisConfig( config ); container = createContainer( config.table ); renderChart( container, typeConfig[ config.meta.chartType ], chartConfig ); } } /** * 渲染图表 * @param container 图表容器节点对象 * @param typeConfig 图表类型配置 * @param config 图表通用配置 * */ function renderChart ( container, typeConfig, config ) { $( container ).highcharts( $.extend( {}, typeConfig, { credits: { enabled: false }, exporting: { enabled: false }, title: { text: config.title, x: -20 //center }, subtitle: { text: config.subTitle, x: -20 }, xAxis: { title: { text: config.xTitle }, categories: config.categories }, yAxis: { title: { text: config.yTitle }, plotLines: [{ value: 0, width: 1, color: '#808080' }] }, tooltip: { enabled: true, valueSuffix: config.suffix }, legend: { layout: 'vertical', align: 'right', verticalAlign: 'middle', borderWidth: 1 }, series: config.series } )); } /** * 创建图表的容器 * 新创建的容器会替换掉对应的table对象 * */ function createContainer ( tableNode ) { var container = document.createElement( "div" ); container.className = "edui-chart-container"; tableNode.parentNode.replaceChild( container, tableNode ); return container; } //根据config解析出正确的类别和图表数据信息 function analysisConfig ( config ) { var series = [], //数据类别 categories = [], result = [], data = config.data, meta = config.meta; //数据对齐方式为相反的方式, 需要反转数据 if ( meta.dataFormat != "1" ) { for ( var i = 0, len = data.length; i < len ; i++ ) { for ( var j = 0, jlen = data[ i ].length; j < jlen; j++ ) { if ( !result[ j ] ) { result[ j ] = []; } result[ j ][ i ] = data[ i ][ j ]; } } data = result; } result = {}; //普通图表 if ( meta.chartType != typeConfig.length - 1 ) { categories = data[ 0 ].slice( 1 ); for ( var i = 1, curData; curData = data[ i ]; i++ ) { series.push( { name: curData[ 0 ], data: curData.slice( 1 ) } ); } result.series = series; result.categories = categories; result.title = meta.title; result.subTitle = meta.subTitle; result.xTitle = meta.xTitle; result.yTitle = meta.yTitle; result.suffix = meta.suffix; } else { var curData = []; for ( var i = 1, len = data[ 0 ].length; i < len; i++ ) { curData.push( [ data[ 0 ][ i ], data[ 1 ][ i ] | 0 ] ); } //饼图 series[ 0 ] = { type: 'pie', name: meta.tip, data: curData }; result.series = series; result.title = meta.title; result.suffix = meta.suffix; } return result; } }); UE.parse.register('background', function (utils) { var me = this, root = me.root, p = root.getElementsByTagName('p'), styles; for (var i = 0,ci; ci = p[i++];) { styles = ci.getAttribute('data-background'); if (styles){ ci.parentNode.removeChild(ci); } } //追加默认的表格样式 styles && utils.cssRule('ueditor_background', me.selector + '{' + styles + '}', document); }); UE.parse.register('list',function(utils){ var customCss = [], customStyle = { 'cn' : 'cn-1-', 'cn1' : 'cn-2-', 'cn2' : 'cn-3-', 'num' : 'num-1-', 'num1' : 'num-2-', 'num2' : 'num-3-', 'dash' : 'dash', 'dot' : 'dot' }; utils.extend(this,{ liiconpath : 'http://bs.baidu.com/listicon/', listDefaultPaddingLeft : '20' }); var root = this.root, ols = root.getElementsByTagName('ol'), uls = root.getElementsByTagName('ul'), selector = this.selector; if(ols.length){ applyStyle.call(this,ols); } if(uls.length){ applyStyle.call(this,uls); } if(ols.length || uls.length){ customCss.push(selector +' .list-paddingleft-1{padding-left:0}'); customCss.push(selector +' .list-paddingleft-2{padding-left:'+ this.listDefaultPaddingLeft+'px}'); customCss.push(selector +' .list-paddingleft-3{padding-left:'+ this.listDefaultPaddingLeft*2+'px}'); utils.cssRule('list', selector +' ol,'+selector +' ul{margin:0;padding:0;}li{clear:both;}'+customCss.join('\n'), document); } function applyStyle(nodes){ var T = this; utils.each(nodes,function(list){ if(list.className && /custom_/i.test(list.className)){ var listStyle = list.className.match(/custom_(\w+)/)[1]; if(listStyle == 'dash' || listStyle == 'dot'){ utils.pushItem(customCss,selector +' li.list-' + customStyle[listStyle] + '{background-image:url(' + T.liiconpath +customStyle[listStyle]+'.gif)}'); utils.pushItem(customCss,selector +' ul.custom_'+listStyle+'{list-style:none;} '+ selector +' ul.custom_'+listStyle+' li{background-position:0 3px;background-repeat:no-repeat}'); }else{ var index = 1; utils.each(list.childNodes,function(li){ if(li.tagName == 'LI'){ utils.pushItem(customCss,selector + ' li.list-' + customStyle[listStyle] + index + '{background-image:url(' + T.liiconpath + 'list-'+customStyle[listStyle] +index + '.gif)}'); index++; } }); utils.pushItem(customCss,selector + ' ol.custom_'+listStyle+'{list-style:none;}'+selector+' ol.custom_'+listStyle+' li{background-position:0 3px;background-repeat:no-repeat}'); } switch(listStyle){ case 'cn': utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-1{padding-left:25px}'); utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-2{padding-left:40px}'); utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-3{padding-left:55px}'); break; case 'cn1': utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-1{padding-left:30px}'); utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-2{padding-left:40px}'); utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-3{padding-left:55px}'); break; case 'cn2': utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-1{padding-left:40px}'); utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-2{padding-left:55px}'); utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-3{padding-left:68px}'); break; case 'num': case 'num1': utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-1{padding-left:25px}'); break; case 'num2': utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-1{padding-left:35px}'); utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-2{padding-left:40px}'); break; case 'dash': utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft{padding-left:35px}'); break; case 'dot': utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft{padding-left:20px}'); } } }); } }); UE.parse.register('vedio',function(utils){ var video = this.root.getElementsByTagName('video'), audio = this.root.getElementsByTagName('audio'); document.createElement('video');document.createElement('audio'); if(video.length || audio.length){ var sourcePath = utils.removeLastbs(this.rootPath), jsurl = sourcePath + '/third-party/video-js/video.js', cssurl = sourcePath + '/third-party/video-js/video-js.min.css', swfUrl = sourcePath + '/third-party/video-js/video-js.swf'; if(window.videojs) { videojs.autoSetup(); } else { utils.loadFile(document,{ id : "video_css", tag : "link", rel : "stylesheet", type : "text/css", href : cssurl }); utils.loadFile(document,{ id : "video_js", src : jsurl, tag : "script", type : "text/javascript" },function(){ videojs.options.flash.swf = swfUrl; videojs.autoSetup(); }); } } }); })(); ================================================ FILE: yshop-drink-vue3/src/App.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/api/express/index.ts ================================================ import request from '@/config/axios' export interface ExpressVO { id: number code: string name: string sort: number } // 查询快递公司列表 export const getExpressList = async () => { return await request.get({ url: `/order/express/list` }) } // 查询快递公司列表 export const getExpressPage = async (params: ExpressPageReqVO) => { return await request.get({ url: `/order/express/page`, params }) } // 查询快递公司详情 export const getExpress = async (id: number) => { return await request.get({ url: `/order/express/get?id=` + id }) } // 查询快递鸟公司配置详情 export const getExpressSet = async () => { return await request.get({ url: `/order/express/set` }) } export const postExpressSet = async (data) => { return await request.post({ url: `/order/express/set`,data }) } // 新增快递公司 export const createExpress = async (data: ExpressVO) => { return await request.post({ url: `/order/express/create`, data }) } // 修改快递公司 export const updateExpress = async (data: ExpressVO) => { return await request.put({ url: `/order/express/update`, data }) } // 删除快递公司 export const deleteExpress = async (id: number) => { return await request.delete({ url: `/order/express/delete?id=` + id }) } // 导出快递公司 Excel export const exportExpress = async (params) => { return await request.download({ url: `/order/express/export-excel`, params }) } ================================================ FILE: yshop-drink-vue3/src/api/infra/apiAccessLog/index.ts ================================================ import request from '@/config/axios' export interface ApiAccessLogVO { id: number traceId: string userId: number userType: number applicationName: string requestMethod: string requestParams: string responseBody: string requestUrl: string userIp: string userAgent: string operateModule: string operateName: string operateType: number beginTime: Date endTime: Date duration: number resultCode: number resultMsg: string createTime: Date } // 查询列表API 访问日志 export const getApiAccessLogPage = (params: PageParam) => { return request.get({ url: '/infra/api-access-log/page', params }) } // 导出API 访问日志 export const exportApiAccessLog = (params) => { return request.download({ url: '/infra/api-access-log/export-excel', params }) } ================================================ FILE: yshop-drink-vue3/src/api/infra/apiErrorLog/index.ts ================================================ import request from '@/config/axios' export interface ApiErrorLogVO { id: number traceId: string userId: number userType: number applicationName: string requestMethod: string requestParams: string requestUrl: string userIp: string userAgent: string exceptionTime: Date exceptionName: string exceptionMessage: string exceptionRootCauseMessage: string exceptionStackTrace: string exceptionClassName: string exceptionFileName: string exceptionMethodName: string exceptionLineNumber: number processUserId: number processStatus: number processTime: Date resultCode: number createTime: Date } // 查询列表API 访问日志 export const getApiErrorLogPage = (params: PageParam) => { return request.get({ url: '/infra/api-error-log/page', params }) } // 更新 API 错误日志的处理状态 export const updateApiErrorLogPage = (id: number, processStatus: number) => { return request.put({ url: '/infra/api-error-log/update-status?id=' + id + '&processStatus=' + processStatus }) } // 导出API 访问日志 export const exportApiErrorLog = (params) => { return request.download({ url: '/infra/api-error-log/export-excel', params }) } ================================================ FILE: yshop-drink-vue3/src/api/infra/codegen/index.ts ================================================ import request from '@/config/axios' export type CodegenTableVO = { id: number tableId: number isParentMenuIdValid: boolean dataSourceConfigId: number scene: number tableName: string tableComment: string remark: string moduleName: string businessName: string className: string classComment: string author: string createTime: Date updateTime: Date templateType: number parentMenuId: number } export type CodegenColumnVO = { id: number tableId: number columnName: string dataType: string columnComment: string nullable: number primaryKey: number ordinalPosition: number javaType: string javaField: string dictType: string example: string createOperation: number updateOperation: number listOperation: number listOperationCondition: string listOperationResult: number htmlType: string } export type DatabaseTableVO = { name: string comment: string } export type CodegenDetailVO = { table: CodegenTableVO columns: CodegenColumnVO[] } export type CodegenPreviewVO = { filePath: string code: string } export type CodegenUpdateReqVO = { table: CodegenTableVO | any columns: CodegenColumnVO[] } export type CodegenCreateListReqVO = { dataSourceConfigId: number tableNames: string[] } // 查询列表代码生成表定义 export const getCodegenTableList = (dataSourceConfigId: number) => { return request.get({ url: '/infra/codegen/table/list?dataSourceConfigId=' + dataSourceConfigId }) } // 查询列表代码生成表定义 export const getCodegenTablePage = (params: PageParam) => { return request.get({ url: '/infra/codegen/table/page', params }) } // 查询详情代码生成表定义 export const getCodegenTable = (id: number) => { return request.get({ url: '/infra/codegen/detail?tableId=' + id }) } // 新增代码生成表定义 export const createCodegenTable = (data: CodegenCreateListReqVO) => { return request.post({ url: '/infra/codegen/create', data }) } // 修改代码生成表定义 export const updateCodegenTable = (data: CodegenUpdateReqVO) => { return request.put({ url: '/infra/codegen/update', data }) } // 基于数据库的表结构,同步数据库的表和字段定义 export const syncCodegenFromDB = (id: number) => { return request.put({ url: '/infra/codegen/sync-from-db?tableId=' + id }) } // 预览生成代码 export const previewCodegen = (id: number) => { return request.get({ url: '/infra/codegen/preview?tableId=' + id }) } // 下载生成代码 export const downloadCodegen = (id: number) => { return request.download({ url: '/infra/codegen/download?tableId=' + id }) } // 获得表定义 export const getSchemaTableList = (params) => { return request.get({ url: '/infra/codegen/db/table/list', params }) } // 基于数据库的表结构,创建代码生成器的表定义 export const createCodegenList = (data) => { return request.post({ url: '/infra/codegen/create-list', data }) } // 删除代码生成表定义 export const deleteCodegenTable = (id: number) => { return request.delete({ url: '/infra/codegen/delete?tableId=' + id }) } ================================================ FILE: yshop-drink-vue3/src/api/infra/config/index.ts ================================================ import request from '@/config/axios' export interface ConfigVO { id: number | undefined category: string name: string key: string value: string type: number visible: boolean remark: string createTime: Date } // 查询参数列表 export const getConfigPage = (params: PageParam) => { return request.get({ url: '/infra/config/page', params }) } // 查询参数详情 export const getConfig = (id: number) => { return request.get({ url: '/infra/config/get?id=' + id }) } // 根据参数键名查询参数值 export const getConfigKey = (configKey: string) => { return request.get({ url: '/infra/config/get-value-by-key?key=' + configKey }) } // 新增参数 export const createConfig = (data: ConfigVO) => { return request.post({ url: '/infra/config/create', data }) } // 修改参数 export const updateConfig = (data: ConfigVO) => { return request.put({ url: '/infra/config/update', data }) } // 删除参数 export const deleteConfig = (id: number) => { return request.delete({ url: '/infra/config/delete?id=' + id }) } // 导出参数 export const exportConfig = (params) => { return request.download({ url: '/infra/config/export', params }) } ================================================ FILE: yshop-drink-vue3/src/api/infra/dataSourceConfig/index.ts ================================================ import request from '@/config/axios' export interface DataSourceConfigVO { id: number | undefined name: string url: string username: string password: string createTime?: Date } // 新增数据源配置 export const createDataSourceConfig = (data: DataSourceConfigVO) => { return request.post({ url: '/infra/data-source-config/create', data }) } // 修改数据源配置 export const updateDataSourceConfig = (data: DataSourceConfigVO) => { return request.put({ url: '/infra/data-source-config/update', data }) } // 删除数据源配置 export const deleteDataSourceConfig = (id: number) => { return request.delete({ url: '/infra/data-source-config/delete?id=' + id }) } // 查询数据源配置详情 export const getDataSourceConfig = (id: number) => { return request.get({ url: '/infra/data-source-config/get?id=' + id }) } // 查询数据源配置列表 export const getDataSourceConfigList = () => { return request.get({ url: '/infra/data-source-config/list' }) } ================================================ FILE: yshop-drink-vue3/src/api/infra/demo/demo01/index.ts ================================================ import request from '@/config/axios' export interface Demo01ContactVO { id: number name: string sex: number birthday: Date description: string avatar: string } // 查询示例联系人分页 export const getDemo01ContactPage = async (params) => { return await request.get({ url: `/infra/demo01-contact/page`, params }) } // 查询示例联系人详情 export const getDemo01Contact = async (id: number) => { return await request.get({ url: `/infra/demo01-contact/get?id=` + id }) } // 新增示例联系人 export const createDemo01Contact = async (data: Demo01ContactVO) => { return await request.post({ url: `/infra/demo01-contact/create`, data }) } // 修改示例联系人 export const updateDemo01Contact = async (data: Demo01ContactVO) => { return await request.put({ url: `/infra/demo01-contact/update`, data }) } // 删除示例联系人 export const deleteDemo01Contact = async (id: number) => { return await request.delete({ url: `/infra/demo01-contact/delete?id=` + id }) } // 导出示例联系人 Excel export const exportDemo01Contact = async (params) => { return await request.download({ url: `/infra/demo01-contact/export-excel`, params }) } ================================================ FILE: yshop-drink-vue3/src/api/infra/demo/demo02/index.ts ================================================ import request from '@/config/axios' export interface Demo02CategoryVO { id: number name: string parentId: number } // 查询示例分类列表 export const getDemo02CategoryList = async () => { return await request.get({ url: `/infra/demo02-category/list` }) } // 查询示例分类详情 export const getDemo02Category = async (id: number) => { return await request.get({ url: `/infra/demo02-category/get?id=` + id }) } // 新增示例分类 export const createDemo02Category = async (data: Demo02CategoryVO) => { return await request.post({ url: `/infra/demo02-category/create`, data }) } // 修改示例分类 export const updateDemo02Category = async (data: Demo02CategoryVO) => { return await request.put({ url: `/infra/demo02-category/update`, data }) } // 删除示例分类 export const deleteDemo02Category = async (id: number) => { return await request.delete({ url: `/infra/demo02-category/delete?id=` + id }) } // 导出示例分类 Excel export const exportDemo02Category = async (params) => { return await request.download({ url: `/infra/demo02-category/export-excel`, params }) } ================================================ FILE: yshop-drink-vue3/src/api/infra/demo/demo03/erp/index.ts ================================================ import request from '@/config/axios' export interface Demo03StudentVO { id: number name: string sex: number birthday: Date description: string } // 查询学生分页 export const getDemo03StudentPage = async (params) => { return await request.get({ url: `/infra/demo03-student/page`, params }) } // 查询学生详情 export const getDemo03Student = async (id: number) => { return await request.get({ url: `/infra/demo03-student/get?id=` + id }) } // 新增学生 export const createDemo03Student = async (data: Demo03StudentVO) => { return await request.post({ url: `/infra/demo03-student/create`, data }) } // 修改学生 export const updateDemo03Student = async (data: Demo03StudentVO) => { return await request.put({ url: `/infra/demo03-student/update`, data }) } // 删除学生 export const deleteDemo03Student = async (id: number) => { return await request.delete({ url: `/infra/demo03-student/delete?id=` + id }) } // 导出学生 Excel export const exportDemo03Student = async (params) => { return await request.download({ url: `/infra/demo03-student/export-excel`, params }) } // ==================== 子表(学生课程) ==================== // 获得学生课程分页 export const getDemo03CoursePage = async (params) => { return await request.get({ url: `/infra/demo03-student/demo03-course/page`, params }) } // 新增学生课程 export const createDemo03Course = async (data) => { return await request.post({ url: `/infra/demo03-student/demo03-course/create`, data }) } // 修改学生课程 export const updateDemo03Course = async (data) => { return await request.put({ url: `/infra/demo03-student/demo03-course/update`, data }) } // 删除学生课程 export const deleteDemo03Course = async (id: number) => { return await request.delete({ url: `/infra/demo03-student/demo03-course/delete?id=` + id }) } // 获得学生课程 export const getDemo03Course = async (id: number) => { return await request.get({ url: `/infra/demo03-student/demo03-course/get?id=` + id }) } // ==================== 子表(学生班级) ==================== // 获得学生班级分页 export const getDemo03GradePage = async (params) => { return await request.get({ url: `/infra/demo03-student/demo03-grade/page`, params }) } // 新增学生班级 export const createDemo03Grade = async (data) => { return await request.post({ url: `/infra/demo03-student/demo03-grade/create`, data }) } // 修改学生班级 export const updateDemo03Grade = async (data) => { return await request.put({ url: `/infra/demo03-student/demo03-grade/update`, data }) } // 删除学生班级 export const deleteDemo03Grade = async (id: number) => { return await request.delete({ url: `/infra/demo03-student/demo03-grade/delete?id=` + id }) } // 获得学生班级 export const getDemo03Grade = async (id: number) => { return await request.get({ url: `/infra/demo03-student/demo03-grade/get?id=` + id }) } ================================================ FILE: yshop-drink-vue3/src/api/infra/demo/demo03/inner/index.ts ================================================ import request from '@/config/axios' export interface Demo03StudentVO { id: number name: string sex: number birthday: Date description: string } // 查询学生分页 export const getDemo03StudentPage = async (params) => { return await request.get({ url: `/infra/demo03-student/page`, params }) } // 查询学生详情 export const getDemo03Student = async (id: number) => { return await request.get({ url: `/infra/demo03-student/get?id=` + id }) } // 新增学生 export const createDemo03Student = async (data: Demo03StudentVO) => { return await request.post({ url: `/infra/demo03-student/create`, data }) } // 修改学生 export const updateDemo03Student = async (data: Demo03StudentVO) => { return await request.put({ url: `/infra/demo03-student/update`, data }) } // 删除学生 export const deleteDemo03Student = async (id: number) => { return await request.delete({ url: `/infra/demo03-student/delete?id=` + id }) } // 导出学生 Excel export const exportDemo03Student = async (params) => { return await request.download({ url: `/infra/demo03-student/export-excel`, params }) } // ==================== 子表(学生课程) ==================== // 获得学生课程列表 export const getDemo03CourseListByStudentId = async (studentId) => { return await request.get({ url: `/infra/demo03-student/demo03-course/list-by-student-id?studentId=` + studentId }) } // ==================== 子表(学生班级) ==================== // 获得学生班级 export const getDemo03GradeByStudentId = async (studentId) => { return await request.get({ url: `/infra/demo03-student/demo03-grade/get-by-student-id?studentId=` + studentId }) } ================================================ FILE: yshop-drink-vue3/src/api/infra/demo/demo03/normal/index.ts ================================================ import request from '@/config/axios' export interface Demo03StudentVO { id: number name: string sex: number birthday: Date description: string } // 查询学生分页 export const getDemo03StudentPage = async (params) => { return await request.get({ url: `/infra/demo03-student/page`, params }) } // 查询学生详情 export const getDemo03Student = async (id: number) => { return await request.get({ url: `/infra/demo03-student/get?id=` + id }) } // 新增学生 export const createDemo03Student = async (data: Demo03StudentVO) => { return await request.post({ url: `/infra/demo03-student/create`, data }) } // 修改学生 export const updateDemo03Student = async (data: Demo03StudentVO) => { return await request.put({ url: `/infra/demo03-student/update`, data }) } // 删除学生 export const deleteDemo03Student = async (id: number) => { return await request.delete({ url: `/infra/demo03-student/delete?id=` + id }) } // 导出学生 Excel export const exportDemo03Student = async (params) => { return await request.download({ url: `/infra/demo03-student/export-excel`, params }) } // ==================== 子表(学生课程) ==================== // 获得学生课程列表 export const getDemo03CourseListByStudentId = async (studentId) => { return await request.get({ url: `/infra/demo03-student/demo03-course/list-by-student-id?studentId=` + studentId }) } // ==================== 子表(学生班级) ==================== // 获得学生班级 export const getDemo03GradeByStudentId = async (studentId) => { return await request.get({ url: `/infra/demo03-student/demo03-grade/get-by-student-id?studentId=` + studentId }) } ================================================ FILE: yshop-drink-vue3/src/api/infra/file/index.ts ================================================ import request from '@/config/axios' export interface FilePageReqVO extends PageParam { path?: string type?: string createTime?: Date[] } // 文件预签名地址 Response VO export interface FilePresignedUrlRespVO { // 文件配置编号 configId: number // 文件上传 URL uploadUrl: string // 文件 URL url: string } // 查询文件列表 export const getFilePage = (params: FilePageReqVO) => { return request.get({ url: '/infra/file/page', params }) } // 删除文件 export const deleteFile = (id: number) => { return request.delete({ url: '/infra/file/delete?id=' + id }) } // 获取文件预签名地址 export const getFilePresignedUrl = (path: string) => { return request.get({ url: '/infra/file/presigned-url', params: { path } }) } // 创建文件 export const createFile = (data: any) => { return request.post({ url: '/infra/file/create', data }) } // 上传文件 export const updateFile = (data: any) => { return request.upload({ url: '/infra/file/upload', data }) } ================================================ FILE: yshop-drink-vue3/src/api/infra/fileConfig/index.ts ================================================ import request from '@/config/axios' export interface FileClientConfig { basePath: string host?: string port?: number username?: string password?: string mode?: string endpoint?: string bucket?: string accessKey?: string accessSecret?: string domain: string } export interface FileConfigVO { id: number name: string storage?: number master: boolean visible: boolean config: FileClientConfig remark: string createTime: Date } // 查询文件配置列表 export const getFileConfigPage = (params: PageParam) => { return request.get({ url: '/infra/file-config/page', params }) } // 查询文件配置详情 export const getFileConfig = (id: number) => { return request.get({ url: '/infra/file-config/get?id=' + id }) } // 更新文件配置为主配置 export const updateFileConfigMaster = (id: number) => { return request.put({ url: '/infra/file-config/update-master?id=' + id }) } // 新增文件配置 export const createFileConfig = (data: FileConfigVO) => { return request.post({ url: '/infra/file-config/create', data }) } // 修改文件配置 export const updateFileConfig = (data: FileConfigVO) => { return request.put({ url: '/infra/file-config/update', data }) } // 删除文件配置 export const deleteFileConfig = (id: number) => { return request.delete({ url: '/infra/file-config/delete?id=' + id }) } // 测试文件配置 export const testFileConfig = (id: number) => { return request.get({ url: '/infra/file-config/test?id=' + id }) } ================================================ FILE: yshop-drink-vue3/src/api/infra/job/index.ts ================================================ import request from '@/config/axios' export interface JobVO { id: number name: string status: number handlerName: string handlerParam: string cronExpression: string retryCount: number retryInterval: number monitorTimeout: number createTime: Date } // 任务列表 export const getJobPage = (params: PageParam) => { return request.get({ url: '/infra/job/page', params }) } // 任务详情 export const getJob = (id: number) => { return request.get({ url: '/infra/job/get?id=' + id }) } // 新增任务 export const createJob = (data: JobVO) => { return request.post({ url: '/infra/job/create', data }) } // 修改定时任务调度 export const updateJob = (data: JobVO) => { return request.put({ url: '/infra/job/update', data }) } // 删除定时任务调度 export const deleteJob = (id: number) => { return request.delete({ url: '/infra/job/delete?id=' + id }) } // 导出定时任务调度 export const exportJob = (params) => { return request.download({ url: '/infra/job/export-excel', params }) } // 任务状态修改 export const updateJobStatus = (id: number, status: number) => { const params = { id, status } return request.put({ url: '/infra/job/update-status', params }) } // 定时任务立即执行一次 export const runJob = (id: number) => { return request.put({ url: '/infra/job/trigger?id=' + id }) } // 获得定时任务的下 n 次执行时间 export const getJobNextTimes = (id: number) => { return request.get({ url: '/infra/job/get_next_times?id=' + id }) } ================================================ FILE: yshop-drink-vue3/src/api/infra/jobLog/index.ts ================================================ import request from '@/config/axios' export interface JobLogVO { id: number jobId: number handlerName: string handlerParam: string cronExpression: string executeIndex: string beginTime: Date endTime: Date duration: string status: number createTime: string } // 任务日志列表 export const getJobLogPage = (params: PageParam) => { return request.get({ url: '/infra/job-log/page', params }) } // 任务日志详情 export const getJobLog = (id: number) => { return request.get({ url: '/infra/job-log/get?id=' + id }) } // 导出定时任务日志 export const exportJobLog = (params) => { return request.download({ url: '/infra/job-log/export-excel', params }) } ================================================ FILE: yshop-drink-vue3/src/api/infra/redis/index.ts ================================================ import request from '@/config/axios' /** * 获取redis 监控信息 */ export const getCache = () => { return request.get({ url: '/infra/redis/get-monitor-info' }) } ================================================ FILE: yshop-drink-vue3/src/api/infra/redis/types.ts ================================================ export interface RedisMonitorInfoVO { info: RedisInfoVO dbSize: number commandStats: RedisCommandStatsVO[] } export interface RedisInfoVO { io_threaded_reads_processed: string tracking_clients: string uptime_in_seconds: string cluster_connections: string current_cow_size: string maxmemory_human: string aof_last_cow_size: string master_replid2: string mem_replication_backlog: string aof_rewrite_scheduled: string total_net_input_bytes: string rss_overhead_ratio: string hz: string current_cow_size_age: string redis_build_id: string errorstat_BUSYGROUP: string aof_last_bgrewrite_status: string multiplexing_api: string client_recent_max_output_buffer: string allocator_resident: string mem_fragmentation_bytes: string aof_current_size: string repl_backlog_first_byte_offset: string tracking_total_prefixes: string redis_mode: string redis_git_dirty: string aof_delayed_fsync: string allocator_rss_bytes: string repl_backlog_histlen: string io_threads_active: string rss_overhead_bytes: string total_system_memory: string loading: string evicted_keys: string maxclients: string cluster_enabled: string redis_version: string repl_backlog_active: string mem_aof_buffer: string allocator_frag_bytes: string io_threaded_writes_processed: string instantaneous_ops_per_sec: string used_memory_human: string total_error_replies: string role: string maxmemory: string used_memory_lua: string rdb_current_bgsave_time_sec: string used_memory_startup: string used_cpu_sys_main_thread: string lazyfree_pending_objects: string aof_pending_bio_fsync: string used_memory_dataset_perc: string allocator_frag_ratio: string arch_bits: string used_cpu_user_main_thread: string mem_clients_normal: string expired_time_cap_reached_count: string unexpected_error_replies: string mem_fragmentation_ratio: string aof_last_rewrite_time_sec: string master_replid: string aof_rewrite_in_progress: string lru_clock: string maxmemory_policy: string run_id: string latest_fork_usec: string tracking_total_items: string total_commands_processed: string expired_keys: string errorstat_ERR: string used_memory: string module_fork_in_progress: string errorstat_WRONGPASS: string aof_buffer_length: string dump_payload_sanitizations: string mem_clients_slaves: string keyspace_misses: string server_time_usec: string executable: string lazyfreed_objects: string db0: string used_memory_peak_human: string keyspace_hits: string rdb_last_cow_size: string aof_pending_rewrite: string used_memory_overhead: string active_defrag_hits: string tcp_port: string uptime_in_days: string used_memory_peak_perc: string current_save_keys_processed: string blocked_clients: string total_reads_processed: string expire_cycle_cpu_milliseconds: string sync_partial_err: string used_memory_scripts_human: string aof_current_rewrite_time_sec: string aof_enabled: string process_supervised: string master_repl_offset: string used_memory_dataset: string used_cpu_user: string rdb_last_bgsave_status: string tracking_total_keys: string atomicvar_api: string allocator_rss_ratio: string client_recent_max_input_buffer: string clients_in_timeout_table: string aof_last_write_status: string mem_allocator: string used_memory_scripts: string used_memory_peak: string process_id: string master_failover_state: string errorstat_NOAUTH: string used_cpu_sys: string repl_backlog_size: string connected_slaves: string current_save_keys_total: string gcc_version: string total_system_memory_human: string sync_full: string connected_clients: string module_fork_last_cow_size: string total_writes_processed: string allocator_active: string total_net_output_bytes: string pubsub_channels: string current_fork_perc: string active_defrag_key_hits: string rdb_changes_since_last_save: string instantaneous_input_kbps: string used_memory_rss_human: string configured_hz: string expired_stale_perc: string active_defrag_misses: string used_cpu_sys_children: string number_of_cached_scripts: string sync_partial_ok: string used_memory_lua_human: string rdb_last_save_time: string pubsub_patterns: string slave_expires_tracked_keys: string redis_git_sha1: string used_memory_rss: string rdb_last_bgsave_time_sec: string os: string mem_not_counted_for_evict: string active_defrag_running: string rejected_connections: string aof_rewrite_buffer_length: string total_forks: string active_defrag_key_misses: string allocator_allocated: string aof_base_size: string instantaneous_output_kbps: string second_repl_offset: string rdb_bgsave_in_progress: string used_cpu_user_children: string total_connections_received: string migrate_cached_sockets: string } export interface RedisCommandStatsVO { command: string calls: number usec: number } ================================================ FILE: yshop-drink-vue3/src/api/login/index.ts ================================================ import request from '@/config/axios' import { getRefreshToken } from '@/utils/auth' import type { UserLoginVO } from './types' export interface SmsCodeVO { mobile: string scene: number } export interface SmsLoginVO { mobile: string code: string } // 登录 export const login = (data: UserLoginVO) => { return request.post({ url: '/system/auth/login', data }) } // 刷新访问令牌 export const refreshToken = () => { return request.post({ url: '/system/auth/refresh-token?refreshToken=' + getRefreshToken() }) } // 使用租户名,获得租户编号 export const getTenantIdByName = (name: string) => { return request.get({ url: '/system/tenant/get-id-by-name?name=' + name }) } // 使用租户域名,获得租户信息 export const getTenantByWebsite = (website: string) => { return request.get({ url: '/system/tenant/get-by-website?website=' + website }) } // 登出 export const loginOut = () => { return request.post({ url: '/system/auth/logout' }) } // 获取用户权限信息 export const getInfo = () => { return request.get({ url: '/system/auth/get-permission-info' }) } //获取登录验证码 export const sendSmsCode = (data: SmsCodeVO) => { return request.post({ url: '/system/auth/send-sms-code', data }) } // 短信验证码登录 export const smsLogin = (data: SmsLoginVO) => { return request.post({ url: '/system/auth/sms-login', data }) } // 社交快捷登录,使用 code 授权码 export function socialLogin(type: string, code: string, state: string) { return request.post({ url: '/system/auth/social-login', data: { type, code, state } }) } // 社交授权的跳转 export const socialAuthRedirect = (type: number, redirectUri: string) => { return request.get({ url: '/system/auth/social-auth-redirect?type=' + type + '&redirectUri=' + redirectUri }) } // 获取验证图片以及 token export const getCode = (data) => { return request.postOriginal({ url: 'system/captcha/get', data }) } // 滑动或者点选验证 export const reqCheck = (data) => { return request.postOriginal({ url: 'system/captcha/check', data }) } ================================================ FILE: yshop-drink-vue3/src/api/login/oauth2/index.ts ================================================ import request from '@/config/axios' // 获得授权信息 export const getAuthorize = (clientId: string) => { return request.get({ url: '/system/oauth2/authorize?clientId=' + clientId }) } // 发起授权 export const authorize = ( responseType: string, clientId: string, redirectUri: string, state: string, autoApprove: boolean, checkedScopes: string[], uncheckedScopes: string[] ) => { // 构建 scopes const scopes = {} for (const scope of checkedScopes) { scopes[scope] = true } for (const scope of uncheckedScopes) { scopes[scope] = false } // 发起请求 return request.post({ url: '/system/oauth2/authorize', headers: { 'Content-type': 'application/x-www-form-urlencoded' }, params: { response_type: responseType, client_id: clientId, redirect_uri: redirectUri, state: state, auto_approve: autoApprove, scope: JSON.stringify(scopes) } }) } ================================================ FILE: yshop-drink-vue3/src/api/login/types.ts ================================================ export type UserLoginVO = { username: string password: string captchaVerification: string socialType?: string socialCode?: string socialState?: string } export type TokenType = { id: number // 编号 accessToken: string // 访问令牌 refreshToken: string // 刷新令牌 userId: number // 用户编号 userType: number //用户类型 clientId: string //客户端编号 expiresTime: number //过期时间 } export type UserVO = { id: number username: string nickname: string deptId: number email: string mobile: string sex: number avatar: string loginIp: string loginDate: string } ================================================ FILE: yshop-drink-vue3/src/api/mall/coupon/index.ts ================================================ import request from '@/config/axios' export interface VO { id: number shopId: string shopName: string title: string switch: boolean least: number value: number startTime: Date endtIme: Date weigh: number type: boolean exchangeCode: string receive: number distribute: number score: number instructions: string image: string limit: number } // 查询优惠券列表 export const getCouponList = async () => { return await request.get({ url: `/coupon/list` }) } // 查询优惠券列表 export const getCouponPage = async (params: PageReqVO) => { return await request.get({ url: `/coupon/page`, params }) } // 查询优惠券详情 export const getCoupon = async (id: number) => { return await request.get({ url: `/coupon/get?id=` + id }) } // 新增优惠券 export const createCoupon = async (data: VO) => { return await request.post({ url: `/coupon/create`, data }) } // 修改优惠券 export const updateCoupon = async (data: VO) => { return await request.put({ url: `/coupon/update`, data }) } // 删除优惠券 export const deleteCoupon = async (id: number) => { return await request.delete({ url: `/coupon/delete?id=` + id }) } // 导出优惠券 Excel export const exportCoupon = async (params) => { return await request.download({ url: `/coupon/export-excel`, params }) } ================================================ FILE: yshop-drink-vue3/src/api/mall/coupon/user/index.ts ================================================ import request from '@/config/axios' export interface UserVO { id: number shopId: string shopName: string title: string least: number value: number starttime: number endtime: number createtime: number updatetime: number type: boolean score: number instructions: string image: string userId: number status: boolean couponId: number exchangeCode: string } export const getUserList = async (id) => { return await request.get({ url: `/coupon/user/list?couponId=`+id }) } // 查询用户领的优惠券列表 export const getUserPage = async (params: UserPageReqVO) => { return await request.get({ url: `/coupon/user/page`, params }) } // 查询用户领的优惠券详情 export const getUser = async (id: number) => { return await request.get({ url: `/coupon/user/get?id=` + id }) } // 新增用户领的优惠券 export const createUser = async (data: UserVO) => { return await request.post({ url: `/coupon/user/create`, data }) } // 修改用户领的优惠券 export const updateUser = async (data: UserVO) => { return await request.put({ url: `/coupon/user/update`, data }) } // 删除用户领的优惠券 export const deleteUser = async (id: number) => { return await request.delete({ url: `/coupon/user/delete?id=` + id }) } // 导出用户领的优惠券 Excel export const exportUser = async (params) => { return await request.download({ url: `/coupon/user/export-excel`, params }) } ================================================ FILE: yshop-drink-vue3/src/api/mall/order/storeOrder/index.ts ================================================ import request from '@/config/axios' export interface StoreOrderVO { id: number orderId: string extendOrderId: string uid: number realName: string userPhone: string userAddress: string cartId: string freightPrice: number totalNum: number totalPrice: number totalPostage: number payPrice: number payPostage: number deductionPrice: number couponId: number couponPrice: number paid: byte payTime: Date payType: string status: boolean refundStatus: byte refundReasonWapImg: string refundReasonWapExplain: string refundReasonTime: Date refundReasonWap: string refundReason: string refundPrice: number deliverySn: string deliveryName: string deliveryType: string deliveryId: string gainIntegral: number useIntegral: number payIntegral: number backIntegral: number mark: string unique: string remark: string merId: number combinationId: number pinkId: number cost: number seckillId: number bargainId: number verifyCode: string storeId: number shippingType: boolean isChannel: byte isSystemDel: boolean } // 查询订单列表 export const getStoreOrderPage = async (params: StoreOrderPageReqVO) => { return await request.get({ url: `/order/store-order/page`, params }) } // 查询订单详情 export const getStoreOrder = async (id: number) => { return await request.get({ url: `/order/store-order/get?id=` + id }) } // 新增订单 export const createStoreOrder = async (data: StoreOrderVO) => { return await request.post({ url: `/order/store-order/create`, data }) } // 修改订单 export const updateStoreOrder = async (data: StoreOrderVO) => { return await request.put({ url: `/order/store-order/update`, data }) } // 删除订单 export const deleteStoreOrder = async (id: number) => { return await request.delete({ url: `/order/store-order/delete?id=` + id }) } export const payStoreOrder = async (id: number) => { return await request.get({ url: `/order/store-order/pay?id=` + id }) } export const takeStoreOrder = async (id: number) => { return await request.get({ url: `/order/store-order/take?id=` + id }) } export const rufundStoreOrder = async (data) => { return await request.post({ url: `/order/store-order/refund`,data }) } export const getStoreOrderRecordList = async (id: number) => { return await request.get({ url: `/order/store-order/record-list?id=` + id }) } // 导出订单 Excel export const exportStoreOrder = async (params) => { return await request.download({ url: `/order/store-order/export-excel`, params }) } export const getLogistic = async (param1,param2) => { return await request.get({ url: `/order/express/getLogistic?shipperCode=` + param1 + `&logisticCode=` + param2}) } export const getOrderHtml = async (param1,param2) => { return await request.get({ url: `/order/store-order/printOrder?id=` + param1 + `&electId=` + param2}) } export const getShopCount = async () => { return await request.get({ url: `/order/store-order/count`}) } export const orderNoticeUrl = async () => { return await request.get({ url: `/order/store-order/notice`}) } ================================================ FILE: yshop-drink-vue3/src/api/mall/product/brand.ts ================================================ import request from '@/config/axios' /** * 商品品牌 */ export interface BrandVO { /** * 品牌编号 */ id?: number /** * 品牌名称 */ name: string /** * 品牌图片 */ picUrl: string /** * 品牌排序 */ sort?: number /** * 品牌描述 */ description?: string /** * 开启状态 */ status: number } // 创建商品品牌 export const createBrand = (data: BrandVO) => { return request.post({ url: '/product/brand/create', data }) } // 更新商品品牌 export const updateBrand = (data: BrandVO) => { return request.put({ url: '/product/brand/update', data }) } // 删除商品品牌 export const deleteBrand = (id: number) => { return request.delete({ url: `/product/brand/delete?id=${id}` }) } // 获得商品品牌 export const getBrand = (id: number) => { return request.get({ url: `/product/brand/get?id=${id}` }) } // 获得商品品牌列表 export const getBrandParam = (params: PageParam) => { return request.get({ url: '/product/brand/page', params }) } ================================================ FILE: yshop-drink-vue3/src/api/mall/product/category.ts ================================================ import request from '@/config/axios' /** * 产品分类 */ export interface CategoryVO { /** * 分类编号 */ id?: number /** * 父分类编号 */ parentId?: number /** * 分类名称 */ name: string /** * 分类图片 */ picUrl: string /** * 分类排序 */ sort?: number /** * 分类描述 */ description?: string /** * 开启状态 */ status: number } // 创建商品分类 export const createCategory = (data: CategoryVO) => { return request.post({ url: '/product/category/create', data }) } // 更新商品分类 export const updateCategory = (data: CategoryVO) => { return request.put({ url: '/product/category/update', data }) } // 删除商品分类 export const deleteCategory = (id: number) => { return request.delete({ url: `/product/category/delete?id=${id}` }) } // 获得商品分类 export const getCategory = (id: number) => { return request.get({ url: `/product/category/get?id=${id}` }) } // 获得商品分类列表 export const getCategoryList = (params: any) => { return request.get({ url: '/product/category/list', params }) } ================================================ FILE: yshop-drink-vue3/src/api/mall/product/product.ts ================================================ import request from '@/config/axios' export interface StoreProductVO { id: number image: string sliderImage: string storeName: string storeInfo: string keyword: string barCode: string cateId: string price: number vipPrice: number otPrice: number postage: number unitName: string sort: number sales: number stock: number isShow: boolean isHot: boolean isBenefit: boolean isBest: boolean isNew: boolean description: string isPostage: byte merUse: byte giveIntegral: number cost: number isSeckill: byte isBargain: byte isGood: boolean ficti: number browse: number codePath: string isSub: boolean tempId: number specType: boolean isIntegral: byte integral: number } // 查询商品列表 export const getStoreProductPage = async (params: StoreProductPageReqVO) => { return await request.get({ url: `/product/store-product/page`, params }) } // 查询商品详情 export const getStoreProduct = async (id: number) => { return await request.get({ url: `/product/store-product/get?id=` + id }) } // 查询商品详情 export const getStoreProductInfo = async (id: number) => { return await request.get({ url: `/product/store-product/info/` + id }) } // 新增商品 export const createStoreProduct = async (data) => { return await request.post({ url: `/product/store-product/create`, data }) } // 修改商品 export const updateStoreProduct = async (data: StoreProductVO) => { return await request.put({ url: `/product/store-product/update`, data }) } // 删除商品 export const deleteStoreProduct = async (id: number) => { return await request.delete({ url: `/product/store-product/delete?id=` + id }) } // 导出商品 Excel export const exportStoreProduct = async (params) => { return await request.download({ url: `/product/store-product/export-excel`, params }) } // 规格格式化 export const isFormatAttr = async (id, data) => { return await request.post({ url: '/product/store-product/isFormatAttr/' + id, data }) } // 删除商品 export const saleStoreProduct = async (id,isShow) => { return await request.get({ url: `/product/store-product/sale?id=` + id + `&type=` + isShow }) } ================================================ FILE: yshop-drink-vue3/src/api/mall/product/storeProductRelation/index.ts ================================================ import request from '@/config/axios' export interface StoreProductRelationVO { id: number uid: number productId: number type: string category: string } // 查询商品点赞和收藏列表 export const getStoreProductRelationPage = async (params: StoreProductRelationPageReqVO) => { return await request.get({ url: `/product/store-product-relation/page`, params }) } // 查询商品点赞和收藏详情 export const getStoreProductRelation = async (id: number) => { return await request.get({ url: `/product/store-product-relation/get?id=` + id }) } // 新增商品点赞和收藏 export const createStoreProductRelation = async (data: StoreProductRelationVO) => { return await request.post({ url: `/product/store-product-relation/create`, data }) } // 修改商品点赞和收藏 export const updateStoreProductRelation = async (data: StoreProductRelationVO) => { return await request.put({ url: `/product/store-product-relation/update`, data }) } // 删除商品点赞和收藏 export const deleteStoreProductRelation = async (id: number) => { return await request.delete({ url: `/product/store-product-relation/delete?id=` + id }) } // 导出商品点赞和收藏 Excel export const exportStoreProductRelation = async (params) => { return await request.download({ url: `/product/store-product-relation/export-excel`, params }) } ================================================ FILE: yshop-drink-vue3/src/api/mall/product/storeProductReply/index.ts ================================================ import request from '@/config/axios' export interface StoreProductReplyVO { id: number uid: number oid: number unique: string productId: number replyType: string productScore: boolean serviceScore: boolean comment: string pics: string merchantReplyContent: string merchantReplyTime: Date isReply: boolean } // 查询评论列表 export const getStoreProductReplyPage = async (params: StoreProductReplyPageReqVO) => { return await request.get({ url: `/product/store-product-reply/page`, params }) } // 查询评论详情 export const getStoreProductReply = async (id: number) => { return await request.get({ url: `/product/store-product-reply/get?id=` + id }) } // 新增评论 export const createStoreProductReply = async (data: StoreProductReplyVO) => { return await request.post({ url: `/product/store-product-reply/create`, data }) } // 修改评论 export const updateStoreProductReply = async (data: StoreProductReplyVO) => { return await request.put({ url: `/product/store-product-reply/update`, data }) } // 删除评论 export const deleteStoreProductReply = async (id: number) => { return await request.delete({ url: `/product/store-product-reply/delete?id=` + id }) } // 导出评论 Excel export const exportStoreProductReply = async (params) => { return await request.download({ url: `/product/store-product-reply/export-excel`, params }) } ================================================ FILE: yshop-drink-vue3/src/api/mall/shop/ads/index.ts ================================================ import request from '@/config/axios' export interface AdsVO { id: number image: string switch: boolean weigh: number shopId: string } // 查询广告图管理列表 export const getAdsPage = async (params: AdsPageReqVO) => { return await request.get({ url: `/shop/ads/page`, params }) } // 查询广告图管理详情 export const getAds = async (id: number) => { return await request.get({ url: `/shop/ads/get?id=` + id }) } // 新增广告图管理 export const createAds = async (data: AdsVO) => { return await request.post({ url: `/shop/ads/create`, data }) } // 修改广告图管理 export const updateAds = async (data: AdsVO) => { return await request.put({ url: `/shop/ads/update`, data }) } // 删除广告图管理 export const deleteAds = async (id: number) => { return await request.delete({ url: `/shop/ads/delete?id=` + id }) } // 导出广告图管理 Excel export const exportAds = async (params) => { return await request.download({ url: `/shop/ads/export-excel`, params }) } ================================================ FILE: yshop-drink-vue3/src/api/mall/shop/materialGroup/index.ts ================================================ import request from '@/config/axios' export interface MaterialGroupVO { id: number name: string } // 查询素材分组列表 export const getMaterialGroupPage = async (params: MaterialGroupPageReqVO) => { return await request.get({ url: `/shop/material-group/page`, params }) } // 查询素材分组详情 export const getMaterialGroup = async (id: number) => { return await request.get({ url: `/shop/material-group/get?id=` + id }) } // 新增素材分组 export const createMaterialGroup = async (data: MaterialGroupVO) => { return await request.post({ url: `/shop/material-group/create`, data }) } // 修改素材分组 export const updateMaterialGroup = async (data: MaterialGroupVO) => { return await request.put({ url: `/shop/material-group/update`, data }) } // 删除素材分组 export const deleteMaterialGroup = async (id: number) => { return await request.delete({ url: `/shop/material-group/delete?id=` + id }) } // 导出素材分组 Excel export const exportMaterialGroup = async (params) => { return await request.download({ url: `/shop/material-group/export-excel`, params }) } ================================================ FILE: yshop-drink-vue3/src/api/mall/shop/recharge/index.ts ================================================ import request from '@/config/axios' export interface RechargeVO { id: number name: string sales: number value: number weigh: number status: boolean sellPrice: number } // 查询充值金额管理列表 export const getRechargePage = async (params: RechargePageReqVO) => { return await request.get({ url: `/shop/recharge/page`, params }) } // 查询充值金额管理详情 export const getRecharge = async (id: number) => { return await request.get({ url: `/shop/recharge/get?id=` + id }) } // 新增充值金额管理 export const createRecharge = async (data: RechargeVO) => { return await request.post({ url: `/shop/recharge/create`, data }) } // 修改充值金额管理 export const updateRecharge = async (data: RechargeVO) => { return await request.put({ url: `/shop/recharge/update`, data }) } // 删除充值金额管理 export const deleteRecharge = async (id: number) => { return await request.delete({ url: `/shop/recharge/delete?id=` + id }) } // 导出充值金额管理 Excel export const exportRecharge = async (params) => { return await request.download({ url: `/shop/recharge/export-excel`, params }) } ================================================ FILE: yshop-drink-vue3/src/api/mall/shop/service/index.ts ================================================ import request from '@/config/axios' export interface ServiceVO { id: number name: string image: string type: string content: string pid: number appId: string pages: string phone: string weigh: number status: boolean } // 查询我的服务列表 export const getServicePage = async (params: ServicePageReqVO) => { return await request.get({ url: `/shop/service/page`, params }) } // 查询我的服务详情 export const getService = async (id: number) => { return await request.get({ url: `/shop/service/get?id=` + id }) } // 新增我的服务 export const createService = async (data: ServiceVO) => { return await request.post({ url: `/shop/service/create`, data }) } // 修改我的服务 export const updateService = async (data: ServiceVO) => { return await request.put({ url: `/shop/service/update`, data }) } // 删除我的服务 export const deleteService = async (id: number) => { return await request.delete({ url: `/shop/service/delete?id=` + id }) } // 导出我的服务 Excel export const exportService = async (params) => { return await request.download({ url: `/shop/service/export-excel`, params }) } ================================================ FILE: yshop-drink-vue3/src/api/mall/shop/storeProductRule/index.ts ================================================ import request from '@/config/axios' export interface StoreProductRuleVO { id: number ruleName: string ruleValue: string } // 查询商品规则值(规格)列表 export const getStoreProductRulePage = async (params: StoreProductRulePageReqVO) => { return await request.get({ url: `/product/store-product-rule/page`, params }) } // 查询商品规则值(规格)详情 export const getStoreProductRule = async (id: number) => { return await request.get({ url: `/product/store-product-rule/get?id=` + id }) } // 新增商品规则值(规格) export const createStoreProductRule = async (data: StoreProductRuleVO,id: number) => { return await request.post({ url: `/product/store-product-rule/save/` + id, data }) } // 修改商品规则值(规格) export const updateStoreProductRule = async (data: StoreProductRuleVO) => { return await request.put({ url: `/product/store-product-rule/update`, data }) } // 删除商品规则值(规格) export const deleteStoreProductRule = async (id: number) => { return await request.delete({ url: `/product/store-product-rule/delete?id=` + id }) } // 导出商品规则值(规格) Excel export const exportStoreProductRule = async (params) => { return await request.download({ url: `/product/store-product-rule/export-excel`, params }) } ================================================ FILE: yshop-drink-vue3/src/api/mall/store/shop/index.ts ================================================ import request from '@/config/axios' export interface ShopVO { id: number name: string mobile: string image: string images: string address: string addressMap: string lng: string lat: string distance: number minPrice: number deliveryPrice: number notice: string status: boolean adminId: string uniprintId: string startTime: Date endTime: Date } export const getShopList = async () => { return await request.get({ url: `/store/shop/list` }) } // 查询门店管理列表 export const getShopPage = async (params: ShopPageReqVO) => { return await request.get({ url: `/store/shop/page`, params }) } // 查询门店管理详情 export const getShop = async (id: number) => { return await request.get({ url: `/store/shop/get?id=` + id }) } // 新增门店管理 export const createShop = async (data: ShopVO) => { return await request.post({ url: `/store/shop/create`, data }) } // 修改门店管理 export const updateShop = async (data: ShopVO) => { return await request.put({ url: `/store/shop/update`, data }) } // 删除门店管理 export const deleteShop = async (id: number) => { return await request.delete({ url: `/store/shop/delete?id=` + id }) } // 导出门店管理 Excel export const exportShop = async (params) => { return await request.download({ url: `/store/shop/export-excel`, params }) } ================================================ FILE: yshop-drink-vue3/src/api/member/user/index.ts ================================================ import request from '@/config/axios' export interface UserVO { id: number username: string password: string realName: string birthday: number cardId: string mark: string partnerId: number groupId: number nickname: string avatar: string phone: string addIp: string lastIp: string nowMoney: number brokeragePrice: number integral: number signNum: number status: boolean level: byte spreadUid: number spreadTime: Date userType: string isPromoter: byte payCount: number spreadCount: number addres: string adminid: number loginType: string wxProfile: string } // 查询用户列表 export const getUserPage = async (params: UserPageReqVO) => { return await request.get({ url: `/member/user/page`, params }) } // 查询用户详情 export const getUser = async (id: number) => { return await request.get({ url: `/member/user/get?id=` + id }) } // 新增用户 export const createUser = async (data: UserVO) => { return await request.post({ url: `/member/user/create`, data }) } // 修改用户 export const updateUser = async (data: UserVO) => { return await request.put({ url: `/member/user/update`, data }) } // 修改余额 export const updateMony = async (data: UserVO) => { return await request.put({ url: `/member/user/updateMony`, data }) } // 删除用户 export const deleteUser = async (id: number) => { return await request.delete({ url: `/member/user/delete?id=` + id }) } // 导出用户 Excel export const exportUser = async (params) => { return await request.download({ url: `/member/user/export-excel`, params }) } ================================================ FILE: yshop-drink-vue3/src/api/member/userAddress/index.ts ================================================ import request from '@/config/axios' export interface UserAddressVO { id: number uid: number realName: string phone: string province: string city: string cityId: number district: string detail: string postCode: string longitude: string latitude: string isDefault: byte } // 查询用户地址列表 export const getUserAddressPage = async (params: UserAddressPageReqVO) => { return await request.get({ url: `/member/user-address/page`, params }) } // 查询用户地址详情 export const getUserAddress = async (id: number) => { return await request.get({ url: `/member/user-address/get?id=` + id }) } // 新增用户地址 export const createUserAddress = async (data: UserAddressVO) => { return await request.post({ url: `/member/user-address/create`, data }) } // 修改用户地址 export const updateUserAddress = async (data: UserAddressVO) => { return await request.put({ url: `/member/user-address/update`, data }) } // 删除用户地址 export const deleteUserAddress = async (id: number) => { return await request.delete({ url: `/member/user-address/delete?id=` + id }) } // 导出用户地址 Excel export const exportUserAddress = async (params) => { return await request.download({ url: `/member/user-address/export-excel`, params }) } ================================================ FILE: yshop-drink-vue3/src/api/member/userBill/index.ts ================================================ import request from '@/config/axios' export interface UserBillVO { id: number uid: number linkId: string pm: byte title: string category: string type: string number: number balance: number mark: string status: boolean } // 查询用户账单列表 export const getUserBillPage = async (params: UserBillPageReqVO) => { return await request.get({ url: `/member/user-bill/page`, params }) } // 查询用户账单详情 export const getUserBill = async (id: number) => { return await request.get({ url: `/member/user-bill/get?id=` + id }) } // 新增用户账单 export const createUserBill = async (data: UserBillVO) => { return await request.post({ url: `/member/user-bill/create`, data }) } // 修改用户账单 export const updateUserBill = async (data: UserBillVO) => { return await request.put({ url: `/member/user-bill/update`, data }) } // 删除用户账单 export const deleteUserBill = async (id: number) => { return await request.delete({ url: `/member/user-bill/delete?id=` + id }) } // 导出用户账单 Excel export const exportUserBill = async (params) => { return await request.download({ url: `/member/user-bill/export-excel`, params }) } ================================================ FILE: yshop-drink-vue3/src/api/message/wechatTemplate/index.ts ================================================ import request from '@/config/axios' export interface WechatTemplateVO { id: number tempkey: string name: string content: string tempid: string status: byte type: string } // 查询微信模板列表 export const getWechatTemplatePage = async (params: WechatTemplatePageReqVO) => { return await request.get({ url: `/message/wechat-template/page`, params }) } // 查询微信模板详情 export const getWechatTemplate = async (id: number) => { return await request.get({ url: `/message/wechat-template/get?id=` + id }) } // 新增微信模板 export const createWechatTemplate = async (data: WechatTemplateVO) => { return await request.post({ url: `/message/wechat-template/create`, data }) } // 修改微信模板 export const updateWechatTemplate = async (data: WechatTemplateVO) => { return await request.put({ url: `/message/wechat-template/update`, data }) } // 删除微信模板 export const deleteWechatTemplate = async (id: number) => { return await request.delete({ url: `/message/wechat-template/delete?id=` + id }) } // 导出微信模板 Excel export const exportWechatTemplate = async (params) => { return await request.download({ url: `/message/wechat-template/export-excel`, params }) } ================================================ FILE: yshop-drink-vue3/src/api/mp/account/index.ts ================================================ import request from '@/config/axios' export interface AccountVO { id: number name: string } // 创建公众号账号 export const createAccount = async (data) => { return await request.post({ url: '/mp/account/create', data }) } // 更新公众号账号 export const updateAccount = async (data) => { return request.put({ url: '/mp/account/update', data: data }) } // 删除公众号账号 export const deleteAccount = async (id) => { return request.delete({ url: '/mp/account/delete?id=' + id, method: 'delete' }) } // 获得公众号账号 export const getAccount = async (id) => { return request.get({ url: '/mp/account/get?id=' + id }) } // 获得公众号账号分页 export const getAccountPage = async (query) => { return request.get({ url: '/mp/account/page', params: query }) } // 获取公众号账号精简信息列表 export const getSimpleAccountList = async () => { return request.get({ url: '/mp/account/list-all-simple' }) } // 生成公众号二维码 export const generateAccountQrCode = async (id) => { return request.put({ url: '/mp/account/generate-qr-code?id=' + id }) } // 清空公众号 API 配额 export const clearAccountQuota = async (id) => { return request.put({ url: '/mp/account/clear-quota?id=' + id }) } export const setAccountMain = async (id) => { return request.put({ url: '/mp/account/set-main?id=' + id }) } ================================================ FILE: yshop-drink-vue3/src/api/mp/account2/index.ts ================================================ import request from '@/config/axios' export interface AccountVO { id?: number name: string } // 创建公众号账号 export const createAccount = async (data) => { return await request.post({ url: '/ma/account/create', data }) } // 更新公众号账号 export const updateAccount = async (data) => { return request.put({ url: '/ma/account/update', data: data }) } // 删除公众号账号 export const deleteAccount = async (id) => { return request.delete({ url: '/ma/account/delete?id=' + id, method: 'delete' }) } // 获得公众号账号 export const getAccount = async (id) => { return request.get({ url: '/ma/account/get?id=' + id }) } // 获得公众号账号分页 export const getAccountPage = async (query) => { return request.get({ url: '/ma/account/page', params: query }) } export const setAccountMain = async (id) => { return request.put({ url: '/ma/account/set-main?id=' + id }) } ================================================ FILE: yshop-drink-vue3/src/api/mp/autoReply/index.ts ================================================ import request from '@/config/axios' // 创建公众号的自动回复 export const createAutoReply = (data) => { return request.post({ url: '/mp/auto-reply/create', data: data }) } // 更新公众号的自动回复 export const updateAutoReply = (data) => { return request.put({ url: '/mp/auto-reply/update', data: data }) } // 删除公众号的自动回复 export const deleteAutoReply = (id) => { return request.delete({ url: '/mp/auto-reply/delete?id=' + id }) } // 获得公众号的自动回复 export const getAutoReply = (id) => { return request.get({ url: '/mp/auto-reply/get?id=' + id }) } // 获得公众号的自动回复分页 export const getAutoReplyPage = (query) => { return request.get({ url: '/mp/auto-reply/page', params: query }) } ================================================ FILE: yshop-drink-vue3/src/api/mp/draft/index.ts ================================================ import request from '@/config/axios' // 获得公众号草稿分页 export const getDraftPage = (query) => { return request.get({ url: '/mp/draft/page', params: query }) } // 创建公众号草稿 export const createDraft = (accountId, articles) => { return request.post({ url: '/mp/draft/create?accountId=' + accountId, data: { articles } }) } // 更新公众号草稿 export const updateDraft = (accountId, mediaId, articles) => { return request.put({ url: '/mp/draft/update?accountId=' + accountId + '&mediaId=' + mediaId, method: 'put', data: articles }) } // 删除公众号草稿 export const deleteDraft = (accountId, mediaId) => { return request.delete({ url: '/mp/draft/delete?accountId=' + accountId + '&mediaId=' + mediaId }) } ================================================ FILE: yshop-drink-vue3/src/api/mp/freePublish/index.ts ================================================ import request from '@/config/axios' // 获得公众号素材分页 export const getFreePublishPage = (query) => { return request.get({ url: '/mp/free-publish/page', params: query }) } // 删除公众号素材 export const deleteFreePublish = (accountId, articleId) => { return request.delete({ url: '/mp/free-publish/delete?accountId=' + accountId + '&articleId=' + articleId }) } // 发布公众号素材 export const submitFreePublish = (accountId, mediaId) => { return request.post({ url: '/mp/free-publish/submit?accountId=' + accountId + '&mediaId=' + mediaId }) } ================================================ FILE: yshop-drink-vue3/src/api/mp/material/index.ts ================================================ import request from '@/config/axios' // 获得公众号素材分页 export const getMaterialPage = (query) => { return request.get({ url: '/mp/material/page', params: query }) } // 删除公众号永久素材 export const deletePermanentMaterial = (id) => { return request.delete({ url: '/mp/material/delete-permanent?id=' + id }) } ================================================ FILE: yshop-drink-vue3/src/api/mp/menu/index.ts ================================================ import request from '@/config/axios' // 获得公众号菜单列表 export const getMenuList = (accountId) => { return request.get({ url: '/mp/menu/list?accountId=' + accountId }) } // 保存公众号菜单 export const saveMenu = (accountId, menus) => { return request.post({ url: '/mp/menu/save', data: { accountId, menus } }) } // 删除公众号菜单 export const deleteMenu = (accountId) => { return request.delete({ url: '/mp/menu/delete?accountId=' + accountId }) } ================================================ FILE: yshop-drink-vue3/src/api/mp/message/index.ts ================================================ import request from '@/config/axios' // 获得公众号消息分页 export const getMessagePage = (query: PageParam) => { return request.get({ url: '/mp/message/page', params: query }) } // 给粉丝发送消息 export const sendMessage = (data) => { return request.post({ url: '/mp/message/send', data: data }) } ================================================ FILE: yshop-drink-vue3/src/api/mp/statistics/index.ts ================================================ import request from '@/config/axios' // 获取消息发送概况数据 export const getUpstreamMessage = (query) => { return request.get({ url: '/mp/statistics/upstream-message', params: query }) } // 用户增减数据 export const getUserSummary = (query) => { return request.get({ url: '/mp/statistics/user-summary', params: query }) } // 获得用户累计数据 export const getUserCumulate = (query) => { return request.get({ url: '/mp/statistics/user-cumulate', params: query }) } // 获得接口分析数据 export const getInterfaceSummary = (query) => { return request.get({ url: '/mp/statistics/interface-summary', params: query }) } ================================================ FILE: yshop-drink-vue3/src/api/mp/tag/index.ts ================================================ import request from '@/config/axios' export interface TagVO { id?: number name: string accountId: number createTime: Date } // 创建公众号标签 export const createTag = (data: TagVO) => { return request.post({ url: '/mp/tag/create', data: data }) } // 更新公众号标签 export const updateTag = (data: TagVO) => { return request.put({ url: '/mp/tag/update', data: data }) } // 删除公众号标签 export const deleteTag = (id: number) => { return request.delete({ url: '/mp/tag/delete?id=' + id }) } // 获得公众号标签 export const getTag = (id: number) => { return request.get({ url: '/mp/tag/get?id=' + id }) } // 获得公众号标签分页 export const getTagPage = (query: PageParam) => { return request.get({ url: '/mp/tag/page', params: query }) } // 获取公众号标签精简信息列表 export const getSimpleTagList = () => { return request.get({ url: '/mp/tag/list-all-simple' }) } // 同步公众号标签 export const syncTag = (accountId: number) => { return request.post({ url: '/mp/tag/sync?accountId=' + accountId }) } ================================================ FILE: yshop-drink-vue3/src/api/mp/user/index.ts ================================================ import request from '@/config/axios' // 更新公众号粉丝 export const updateUser = (data) => { return request.put({ url: '/mp/user/update', data: data }) } // 获得公众号粉丝 export const getUser = (id) => { return request.get({ url: '/mp/user/get?id=' + id }) } // 获得公众号粉丝分页 export const getUserPage = (query) => { return request.get({ url: '/mp/user/page', params: query }) } // 同步公众号粉丝 export const syncUser = (accountId) => { return request.post({ url: '/mp/user/sync?accountId=' + accountId }) } ================================================ FILE: yshop-drink-vue3/src/api/pay/merchantDetails/index.ts ================================================ import request from '@/config/axios' export interface MerchantDetailsVO { detailsId: string payType: string appid: string mchId: string certStoreType: string keyPrivate: string keyPublic: string keyCert: string keyCertPwd: string notifyUrl: string returnUrl: string signType: string seller: string subAppId: string subMchId: string inputCharset: string isTest: boolean } // 查询支付服务商配置列表 export const getMerchantDetailsPage = async (params: MerchantDetailsPageReqVO) => { return await request.get({ url: `/pay/merchant-details/page`, params }) } // 查询支付服务商配置详情 export const getMerchantDetails = async (id: number) => { return await request.get({ url: `/pay/merchant-details/get?id=` + id }) } // 新增支付服务商配置 export const createMerchantDetails = async (data: MerchantDetailsVO) => { return await request.post({ url: `/pay/merchant-details/create`, data }) } // 修改支付服务商配置 export const updateMerchantDetails = async (data: MerchantDetailsVO) => { return await request.put({ url: `/pay/merchant-details/update`, data }) } // 删除支付服务商配置 export const deleteMerchantDetails = async (id: number) => { return await request.delete({ url: `/pay/merchant-details/delete?id=` + id }) } // 导出支付服务商配置 Excel export const exportMerchantDetails = async (params) => { return await request.download({ url: `/pay/merchant-details/export-excel`, params }) } ================================================ FILE: yshop-drink-vue3/src/api/score/order/index.ts ================================================ import request from '@/config/axios' export interface OrderVO { id: number userId: number productId: number number: number score: number totalScore: number ip: string expressNumber: string expressCompany: string customerName: string customerPhone: string customerAddress: string status: boolean havePaid: number haveDelivered: number haveReceived: number } // 查询积分商城订单列表 export const getOrderPage = async (params: OrderPageReqVO) => { return await request.get({ url: `/score/order/page`, params }) } // 查询积分商城订单详情 export const getOrder = async (id: number) => { return await request.get({ url: `/score/order/get?id=` + id }) } // 新增积分商城订单 export const createOrder = async (data: OrderVO) => { return await request.post({ url: `/score/order/create`, data }) } // 修改积分商城订单 export const updateOrder = async (data: OrderVO) => { return await request.put({ url: `/score/order/update`, data }) } // 删除积分商城订单 export const deleteOrder = async (id: number) => { return await request.delete({ url: `/score/order/delete?id=` + id }) } // 导出积分商城订单 Excel export const exportOrder = async (params) => { return await request.download({ url: `/score/order/export-excel`, params }) } export const getLogistic = async (param1,param2) => { return await request.get({ url: `/order/express/getLogistic?shipperCode=` + param1 + `&logisticCode=` + param2}) } // 收货 export const takeStoreOrder = async (id) => { return await request.get({ url: `/score/order/take?id=` + id }) } ================================================ FILE: yshop-drink-vue3/src/api/score/product/index.ts ================================================ import request from '@/config/axios' export interface ProductVO { id: number title: string image: string images: string desc: string score: number weigh: number stock: number sales: number switch: boolean } // 查询积分产品列表 export const getProductPage = async (params: ProductPageReqVO) => { return await request.get({ url: `/score/product/page`, params }) } // 查询积分产品详情 export const getProduct = async (id: number) => { return await request.get({ url: `/score/product/get?id=` + id }) } // 新增积分产品 export const createProduct = async (data: ProductVO) => { return await request.post({ url: `/score/product/create`, data }) } // 修改积分产品 export const updateProduct = async (data: ProductVO) => { return await request.put({ url: `/score/product/update`, data }) } // 删除积分产品 export const deleteProduct = async (id: number) => { return await request.delete({ url: `/score/product/delete?id=` + id }) } // 导出积分产品 Excel export const exportProduct = async (params) => { return await request.download({ url: `/score/product/export-excel`, params }) } ================================================ FILE: yshop-drink-vue3/src/api/system/area/index.ts ================================================ import request from '@/config/axios' // 获得地区树 export const getAreaTree = async () => { return await request.get({ url: '/system/area/tree' }) } // 获得 IP 对应的地区名 export const getAreaByIp = async (ip: string) => { return await request.get({ url: '/system/area/get-by-ip?ip=' + ip }) } ================================================ FILE: yshop-drink-vue3/src/api/system/dept/index.ts ================================================ import request from '@/config/axios' export interface DeptVO { id?: number name: string parentId: number status: number sort: number leaderUserId: number phone: string email: string createTime: Date } // 查询部门(精简)列表 export const getSimpleDeptList = async (): Promise => { return await request.get({ url: '/system/dept/simple-list' }) } // 查询部门列表 export const getDeptPage = async (params: PageParam) => { return await request.get({ url: '/system/dept/list', params }) } // 查询部门详情 export const getDept = async (id: number) => { return await request.get({ url: '/system/dept/get?id=' + id }) } // 新增部门 export const createDept = async (data: DeptVO) => { return await request.post({ url: '/system/dept/create', data: data }) } // 修改部门 export const updateDept = async (params: DeptVO) => { return await request.put({ url: '/system/dept/update', data: params }) } // 删除部门 export const deleteDept = async (id: number) => { return await request.delete({ url: '/system/dept/delete?id=' + id }) } ================================================ FILE: yshop-drink-vue3/src/api/system/dict/dict.data.ts ================================================ import request from '@/config/axios' export type DictDataVO = { id: number | undefined sort: number | undefined label: string value: string dictType: string status: number colorType: string cssClass: string remark: string createTime: Date } // 查询字典数据(精简)列表 export const getSimpleDictDataList = () => { return request.get({ url: '/system/dict-data/simple-list' }) } // 查询字典数据列表 export const getDictDataPage = (params: PageParam) => { return request.get({ url: '/system/dict-data/page', params }) } // 查询字典数据详情 export const getDictData = (id: number) => { return request.get({ url: '/system/dict-data/get?id=' + id }) } // 新增字典数据 export const createDictData = (data: DictDataVO) => { return request.post({ url: '/system/dict-data/create', data }) } // 修改字典数据 export const updateDictData = (data: DictDataVO) => { return request.put({ url: '/system/dict-data/update', data }) } // 删除字典数据 export const deleteDictData = (id: number) => { return request.delete({ url: '/system/dict-data/delete?id=' + id }) } // 导出字典类型数据 export const exportDictData = (params) => { return request.download({ url: '/system/dict-data/export', params }) } ================================================ FILE: yshop-drink-vue3/src/api/system/dict/dict.type.ts ================================================ import request from '@/config/axios' export type DictTypeVO = { id: number | undefined name: string type: string status: number remark: string createTime: Date } // 查询字典(精简)列表 export const getSimpleDictTypeList = () => { return request.get({ url: '/system/dict-type/list-all-simple' }) } // 查询字典列表 export const getDictTypePage = (params: PageParam) => { return request.get({ url: '/system/dict-type/page', params }) } // 查询字典详情 export const getDictType = (id: number) => { return request.get({ url: '/system/dict-type/get?id=' + id }) } // 新增字典 export const createDictType = (data: DictTypeVO) => { return request.post({ url: '/system/dict-type/create', data }) } // 修改字典 export const updateDictType = (data: DictTypeVO) => { return request.put({ url: '/system/dict-type/update', data }) } // 删除字典 export const deleteDictType = (id: number) => { return request.delete({ url: '/system/dict-type/delete?id=' + id }) } // 导出字典类型 export const exportDictType = (params) => { return request.download({ url: '/system/dict-type/export', params }) } ================================================ FILE: yshop-drink-vue3/src/api/system/loginLog/index.ts ================================================ import request from '@/config/axios' export interface LoginLogVO { id: number logType: number traceId: number userId: number userType: number username: string result: number status: number userIp: string userAgent: string createTime: Date } // 查询登录日志列表 export const getLoginLogPage = (params: PageParam) => { return request.get({ url: '/system/login-log/page', params }) } // 导出登录日志 export const exportLoginLog = (params) => { return request.download({ url: '/system/login-log/export', params }) } ================================================ FILE: yshop-drink-vue3/src/api/system/mail/account/index.ts ================================================ import request from '@/config/axios' export interface MailAccountVO { id: number mail: string username: string password: string host: string port: number sslEnable: boolean starttlsEnable: boolean } // 查询邮箱账号列表 export const getMailAccountPage = async (params: PageParam) => { return await request.get({ url: '/system/mail-account/page', params }) } // 查询邮箱账号详情 export const getMailAccount = async (id: number) => { return await request.get({ url: '/system/mail-account/get?id=' + id }) } // 新增邮箱账号 export const createMailAccount = async (data: MailAccountVO) => { return await request.post({ url: '/system/mail-account/create', data }) } // 修改邮箱账号 export const updateMailAccount = async (data: MailAccountVO) => { return await request.put({ url: '/system/mail-account/update', data }) } // 删除邮箱账号 export const deleteMailAccount = async (id: number) => { return await request.delete({ url: '/system/mail-account/delete?id=' + id }) } // 获得邮箱账号精简列表 export const getSimpleMailAccountList = async () => { return request.get({ url: '/system/mail-account/simple-list' }) } ================================================ FILE: yshop-drink-vue3/src/api/system/mail/log/index.ts ================================================ import request from '@/config/axios' export interface MailLogVO { id: number userId: number userType: number toMail: string accountId: number fromMail: string templateId: number templateCode: string templateNickname: string templateTitle: string templateContent: string templateParams: string sendStatus: number sendTime: Date sendMessageId: string sendException: string } // 查询邮件日志列表 export const getMailLogPage = async (params: PageParam) => { return await request.get({ url: '/system/mail-log/page', params }) } // 查询邮件日志详情 export const getMailLog = async (id: number) => { return await request.get({ url: '/system/mail-log/get?id=' + id }) } ================================================ FILE: yshop-drink-vue3/src/api/system/mail/template/index.ts ================================================ import request from '@/config/axios' export interface MailTemplateVO { id: number name: string code: string accountId: number nickname: string title: string content: string params: string status: number remark: string } export interface MailSendReqVO { mail: string templateCode: string templateParams: Map } // 查询邮件模版列表 export const getMailTemplatePage = async (params: PageParam) => { return await request.get({ url: '/system/mail-template/page', params }) } // 查询邮件模版详情 export const getMailTemplate = async (id: number) => { return await request.get({ url: '/system/mail-template/get?id=' + id }) } // 新增邮件模版 export const createMailTemplate = async (data: MailTemplateVO) => { return await request.post({ url: '/system/mail-template/create', data }) } // 修改邮件模版 export const updateMailTemplate = async (data: MailTemplateVO) => { return await request.put({ url: '/system/mail-template/update', data }) } // 删除邮件模版 export const deleteMailTemplate = async (id: number) => { return await request.delete({ url: '/system/mail-template/delete?id=' + id }) } // 发送邮件 export const sendMail = (data: MailSendReqVO) => { return request.post({ url: '/system/mail-template/send-mail', data }) } ================================================ FILE: yshop-drink-vue3/src/api/system/menu/index.ts ================================================ import request from '@/config/axios' export interface MenuVO { id: number name: string permission: string type: number sort: number parentId: number path: string icon: string component: string componentName?: string status: number visible: boolean keepAlive: boolean alwaysShow?: boolean createTime: Date } // 查询菜单(精简)列表 export const getSimpleMenusList = () => { return request.get({ url: '/system/menu/simple-list' }) } // 查询菜单列表 export const getMenuList = (params) => { return request.get({ url: '/system/menu/list', params }) } // 获取菜单详情 export const getMenu = (id: number) => { return request.get({ url: '/system/menu/get?id=' + id }) } // 新增菜单 export const createMenu = (data: MenuVO) => { return request.post({ url: '/system/menu/create', data }) } // 修改菜单 export const updateMenu = (data: MenuVO) => { return request.put({ url: '/system/menu/update', data }) } // 删除菜单 export const deleteMenu = (id: number) => { return request.delete({ url: '/system/menu/delete?id=' + id }) } ================================================ FILE: yshop-drink-vue3/src/api/system/notice/index.ts ================================================ import request from '@/config/axios' export interface NoticeVO { id: number | undefined title: string type: number content: string status: number remark: string creator: string createTime: Date } // 查询公告列表 export const getNoticePage = (params: PageParam) => { return request.get({ url: '/system/notice/page', params }) } // 查询公告详情 export const getNotice = (id: number) => { return request.get({ url: '/system/notice/get?id=' + id }) } // 新增公告 export const createNotice = (data: NoticeVO) => { return request.post({ url: '/system/notice/create', data }) } // 修改公告 export const updateNotice = (data: NoticeVO) => { return request.put({ url: '/system/notice/update', data }) } // 删除公告 export const deleteNotice = (id: number) => { return request.delete({ url: '/system/notice/delete?id=' + id }) } // 推送公告 export const pushNotice = (id: number) => { return request.post({ url: '/system/notice/push?id=' + id }) } ================================================ FILE: yshop-drink-vue3/src/api/system/notify/message/index.ts ================================================ import request from '@/config/axios' import qs from 'qs' export interface NotifyMessageVO { id: number userId: number userType: number templateId: number templateCode: string templateNickname: string templateContent: string templateType: number templateParams: string readStatus: boolean readTime: Date createTime: Date } // 查询站内信消息列表 export const getNotifyMessagePage = async (params: PageParam) => { return await request.get({ url: '/system/notify-message/page', params }) } // 获得我的站内信分页 export const getMyNotifyMessagePage = async (params: PageParam) => { return await request.get({ url: '/system/notify-message/my-page', params }) } // 批量标记已读 export const updateNotifyMessageRead = async (ids) => { return await request.put({ url: '/system/notify-message/update-read?' + qs.stringify({ ids: ids }, { indices: false }) }) } // 标记所有站内信为已读 export const updateAllNotifyMessageRead = async () => { return await request.put({ url: '/system/notify-message/update-all-read' }) } // 获取当前用户的最新站内信列表 export const getUnreadNotifyMessageList = async () => { return await request.get({ url: '/system/notify-message/get-unread-list' }) } // 获得当前用户的未读站内信数量 export const getUnreadNotifyMessageCount = async () => { return await request.get({ url: '/system/notify-message/get-unread-count' }) } ================================================ FILE: yshop-drink-vue3/src/api/system/notify/template/index.ts ================================================ import request from '@/config/axios' export interface NotifyTemplateVO { id?: number name: string nickname: string code: string content: string type?: number params: string status: number remark: string } export interface NotifySendReqVO { userId: number | null templateCode: string templateParams: Map } // 查询站内信模板列表 export const getNotifyTemplatePage = async (params: PageParam) => { return await request.get({ url: '/system/notify-template/page', params }) } // 查询站内信模板详情 export const getNotifyTemplate = async (id: number) => { return await request.get({ url: '/system/notify-template/get?id=' + id }) } // 新增站内信模板 export const createNotifyTemplate = async (data: NotifyTemplateVO) => { return await request.post({ url: '/system/notify-template/create', data }) } // 修改站内信模板 export const updateNotifyTemplate = async (data: NotifyTemplateVO) => { return await request.put({ url: '/system/notify-template/update', data }) } // 删除站内信模板 export const deleteNotifyTemplate = async (id: number) => { return await request.delete({ url: '/system/notify-template/delete?id=' + id }) } // 发送站内信 export const sendNotify = (data: NotifySendReqVO) => { return request.post({ url: '/system/notify-template/send-notify', data }) } ================================================ FILE: yshop-drink-vue3/src/api/system/oauth2/client.ts ================================================ import request from '@/config/axios' export interface OAuth2ClientVO { id: number clientId: string secret: string name: string logo: string description: string status: number accessTokenValiditySeconds: number refreshTokenValiditySeconds: number redirectUris: string[] autoApprove: boolean authorizedGrantTypes: string[] scopes: string[] authorities: string[] resourceIds: string[] additionalInformation: string isAdditionalInformationJson: boolean createTime: Date } // 查询 OAuth2 客户端的列表 export const getOAuth2ClientPage = (params: PageParam) => { return request.get({ url: '/system/oauth2-client/page', params }) } // 查询 OAuth2 客户端的详情 export const getOAuth2Client = (id: number) => { return request.get({ url: '/system/oauth2-client/get?id=' + id }) } // 新增 OAuth2 客户端 export const createOAuth2Client = (data: OAuth2ClientVO) => { return request.post({ url: '/system/oauth2-client/create', data }) } // 修改 OAuth2 客户端 export const updateOAuth2Client = (data: OAuth2ClientVO) => { return request.put({ url: '/system/oauth2-client/update', data }) } // 删除 OAuth2 export const deleteOAuth2Client = (id: number) => { return request.delete({ url: '/system/oauth2-client/delete?id=' + id }) } ================================================ FILE: yshop-drink-vue3/src/api/system/oauth2/token.ts ================================================ import request from '@/config/axios' export interface OAuth2TokenVO { id: number accessToken: string refreshToken: string userId: number userType: number clientId: string createTime: Date expiresTime: Date } // 查询 token列表 export const getAccessTokenPage = (params: PageParam) => { return request.get({ url: '/system/oauth2-token/page', params }) } // 删除 token export const deleteAccessToken = (accessToken: string) => { return request.delete({ url: '/system/oauth2-token/delete?accessToken=' + accessToken }) } ================================================ FILE: yshop-drink-vue3/src/api/system/operatelog/index.ts ================================================ import request from '@/config/axios' export type OperateLogVO = { id: number traceId: string userType: number userId: number userName: string type: string subType: string bizId: number action: string extra: string requestMethod: string requestUrl: string userIp: string userAgent: string creator: string creatorName: string createTime: Date } // 查询操作日志列表 export const getOperateLogPage = (params: PageParam) => { return request.get({ url: '/system/operate-log/page', params }) } // 导出操作日志 export const exportOperateLog = (params: any) => { return request.download({ url: '/system/operate-log/export', params }) } ================================================ FILE: yshop-drink-vue3/src/api/system/permission/index.ts ================================================ import request from '@/config/axios' export interface PermissionAssignUserRoleReqVO { userId: number roleIds: number[] } export interface PermissionAssignRoleMenuReqVO { roleId: number menuIds: number[] } export interface PermissionAssignRoleDataScopeReqVO { roleId: number dataScope: number dataScopeDeptIds: number[] } // 查询角色拥有的菜单权限 export const getRoleMenuList = async (roleId: number) => { return await request.get({ url: '/system/permission/list-role-menus?roleId=' + roleId }) } // 赋予角色菜单权限 export const assignRoleMenu = async (data: PermissionAssignRoleMenuReqVO) => { return await request.post({ url: '/system/permission/assign-role-menu', data }) } // 赋予角色数据权限 export const assignRoleDataScope = async (data: PermissionAssignRoleDataScopeReqVO) => { return await request.post({ url: '/system/permission/assign-role-data-scope', data }) } // 查询用户拥有的角色数组 export const getUserRoleList = async (userId: number) => { return await request.get({ url: '/system/permission/list-user-roles?userId=' + userId }) } // 赋予用户角色 export const assignUserRole = async (data: PermissionAssignUserRoleReqVO) => { return await request.post({ url: '/system/permission/assign-user-role', data }) } ================================================ FILE: yshop-drink-vue3/src/api/system/post/index.ts ================================================ import request from '@/config/axios' export interface PostVO { id?: number name: string code: string sort: number status: number remark: string createTime?: Date } // 查询岗位列表 export const getPostPage = async (params: PageParam) => { return await request.get({ url: '/system/post/page', params }) } // 获取岗位精简信息列表 export const getSimplePostList = async (): Promise => { return await request.get({ url: '/system/post/simple-list' }) } // 查询岗位详情 export const getPost = async (id: number) => { return await request.get({ url: '/system/post/get?id=' + id }) } // 新增岗位 export const createPost = async (data: PostVO) => { return await request.post({ url: '/system/post/create', data }) } // 修改岗位 export const updatePost = async (data: PostVO) => { return await request.put({ url: '/system/post/update', data }) } // 删除岗位 export const deletePost = async (id: number) => { return await request.delete({ url: '/system/post/delete?id=' + id }) } // 导出岗位 export const exportPost = async (params) => { return await request.download({ url: '/system/post/export', params }) } ================================================ FILE: yshop-drink-vue3/src/api/system/role/index.ts ================================================ import request from '@/config/axios' export interface RoleVO { id: number name: string code: string sort: number status: number type: number dataScope: number dataScopeDeptIds: number[] createTime: Date } export interface UpdateStatusReqVO { id: number status: number } // 查询角色列表 export const getRolePage = async (params: PageParam) => { return await request.get({ url: '/system/role/page', params }) } // 查询角色(精简)列表 export const getSimpleRoleList = async (): Promise => { return await request.get({ url: '/system/role/simple-list' }) } // 查询角色详情 export const getRole = async (id: number) => { return await request.get({ url: '/system/role/get?id=' + id }) } // 新增角色 export const createRole = async (data: RoleVO) => { return await request.post({ url: '/system/role/create', data }) } // 修改角色 export const updateRole = async (data: RoleVO) => { return await request.put({ url: '/system/role/update', data }) } // 修改角色状态 export const updateRoleStatus = async (data: UpdateStatusReqVO) => { return await request.put({ url: '/system/role/update-status', data }) } // 删除角色 export const deleteRole = async (id: number) => { return await request.delete({ url: '/system/role/delete?id=' + id }) } // 导出角色 export const exportRole = (params) => { return request.download({ url: '/system/role/export-excel', params }) } ================================================ FILE: yshop-drink-vue3/src/api/system/sms/smsChannel/index.ts ================================================ import request from '@/config/axios' export interface SmsChannelVO { id: number code: string status: number signature: string remark: string apiKey: string apiSecret: string callbackUrl: string createTime: Date } // 查询短信渠道列表 export const getSmsChannelPage = (params: PageParam) => { return request.get({ url: '/system/sms-channel/page', params }) } // 获得短信渠道精简列表 export function getSimpleSmsChannelList() { return request.get({ url: '/system/sms-channel/simple-list' }) } // 查询短信渠道详情 export const getSmsChannel = (id: number) => { return request.get({ url: '/system/sms-channel/get?id=' + id }) } // 新增短信渠道 export const createSmsChannel = (data: SmsChannelVO) => { return request.post({ url: '/system/sms-channel/create', data }) } // 修改短信渠道 export const updateSmsChannel = (data: SmsChannelVO) => { return request.put({ url: '/system/sms-channel/update', data }) } // 删除短信渠道 export const deleteSmsChannel = (id: number) => { return request.delete({ url: '/system/sms-channel/delete?id=' + id }) } ================================================ FILE: yshop-drink-vue3/src/api/system/sms/smsLog/index.ts ================================================ import request from '@/config/axios' export interface SmsLogVO { id: number | null channelId: number | null channelCode: string templateId: number | null templateCode: string templateType: number | null templateContent: string templateParams: Map | null apiTemplateId: string mobile: string userId: number | null userType: number | null sendStatus: number | null sendTime: Date | null apiSendCode: string apiSendMsg: string apiRequestId: string apiSerialNo: string receiveStatus: number | null receiveTime: Date | null apiReceiveCode: string apiReceiveMsg: string createTime: Date | null } // 查询短信日志列表 export const getSmsLogPage = (params: PageParam) => { return request.get({ url: '/system/sms-log/page', params }) } // 导出短信日志 export const exportSmsLog = (params) => { return request.download({ url: '/system/sms-log/export-excel', params }) } ================================================ FILE: yshop-drink-vue3/src/api/system/sms/smsTemplate/index.ts ================================================ import request from '@/config/axios' export interface SmsTemplateVO { id?: number type?: number status: number code: string name: string content: string remark: string apiTemplateId: string channelId?: number channelCode?: string params?: string[] createTime?: Date } export interface SendSmsReqVO { mobile: string templateCode: string templateParams: Map } // 查询短信模板列表 export const getSmsTemplatePage = (params: PageParam) => { return request.get({ url: '/system/sms-template/page', params }) } // 查询短信模板详情 export const getSmsTemplate = (id: number) => { return request.get({ url: '/system/sms-template/get?id=' + id }) } // 新增短信模板 export const createSmsTemplate = (data: SmsTemplateVO) => { return request.post({ url: '/system/sms-template/create', data }) } // 修改短信模板 export const updateSmsTemplate = (data: SmsTemplateVO) => { return request.put({ url: '/system/sms-template/update', data }) } // 删除短信模板 export const deleteSmsTemplate = (id: number) => { return request.delete({ url: '/system/sms-template/delete?id=' + id }) } // 导出短信模板 export const exportSmsTemplate = (params) => { return request.download({ url: '/system/sms-template/export-excel', params }) } // 发送短信 export const sendSms = (data: SendSmsReqVO) => { return request.post({ url: '/system/sms-template/send-sms', data }) } ================================================ FILE: yshop-drink-vue3/src/api/system/social/client/index.ts ================================================ import request from '@/config/axios' export interface SocialClientVO { id: number name: string socialType: number userType: number clientId: string clientSecret: string agentId: string status: number } // 查询社交客户端列表 export const getSocialClientPage = async (params) => { return await request.get({ url: `/system/social-client/page`, params }) } // 查询社交客户端详情 export const getSocialClient = async (id: number) => { return await request.get({ url: `/system/social-client/get?id=` + id }) } // 新增社交客户端 export const createSocialClient = async (data: SocialClientVO) => { return await request.post({ url: `/system/social-client/create`, data }) } // 修改社交客户端 export const updateSocialClient = async (data: SocialClientVO) => { return await request.put({ url: `/system/social-client/update`, data }) } // 删除社交客户端 export const deleteSocialClient = async (id: number) => { return await request.delete({ url: `/system/social-client/delete?id=` + id }) } ================================================ FILE: yshop-drink-vue3/src/api/system/social/user/index.ts ================================================ import request from '@/config/axios' export interface SocialUserVO { id: number type: number openid: string token: string rawTokenInfo: string nickname: string avatar: string rawUserInfo: string code: string state: string } // 查询社交用户列表 export const getSocialUserPage = async (params) => { return await request.get({ url: `/system/social-user/page`, params }) } // 查询社交用户详情 export const getSocialUser = async (id: number) => { return await request.get({ url: `/system/social-user/get?id=` + id }) } ================================================ FILE: yshop-drink-vue3/src/api/system/tenant/index.ts ================================================ import request from '@/config/axios' export interface TenantVO { id: number name: string contactName: string contactMobile: string status: number domain: string packageId: number username: string password: string expireTime: Date accountCount: number createTime: Date } export interface TenantPageReqVO extends PageParam { name?: string contactName?: string contactMobile?: string status?: number createTime?: Date[] } export interface TenantExportReqVO { name?: string contactName?: string contactMobile?: string status?: number createTime?: Date[] } // 查询租户列表 export const getTenantPage = (params: TenantPageReqVO) => { return request.get({ url: '/system/tenant/page', params }) } // 查询租户详情 export const getTenant = (id: number) => { return request.get({ url: '/system/tenant/get?id=' + id }) } // 新增租户 export const createTenant = (data: TenantVO) => { return request.post({ url: '/system/tenant/create', data }) } // 修改租户 export const updateTenant = (data: TenantVO) => { return request.put({ url: '/system/tenant/update', data }) } // 删除租户 export const deleteTenant = (id: number) => { return request.delete({ url: '/system/tenant/delete?id=' + id }) } // 导出租户 export const exportTenant = (params: TenantExportReqVO) => { return request.download({ url: '/system/tenant/export-excel', params }) } ================================================ FILE: yshop-drink-vue3/src/api/system/tenantPackage/index.ts ================================================ import request from '@/config/axios' export interface TenantPackageVO { id: number name: string status: number remark: string creator: string updater: string updateTime: string menuIds: number[] createTime: Date } // 查询租户套餐列表 export const getTenantPackagePage = (params: PageParam) => { return request.get({ url: '/system/tenant-package/page', params }) } // 获得租户 export const getTenantPackage = (id: number) => { return request.get({ url: '/system/tenant-package/get?id=' + id }) } // 新增租户套餐 export const createTenantPackage = (data: TenantPackageVO) => { return request.post({ url: '/system/tenant-package/create', data }) } // 修改租户套餐 export const updateTenantPackage = (data: TenantPackageVO) => { return request.put({ url: '/system/tenant-package/update', data }) } // 删除租户套餐 export const deleteTenantPackage = (id: number) => { return request.delete({ url: '/system/tenant-package/delete?id=' + id }) } // 获取租户套餐精简信息列表 export const getTenantPackageList = () => { return request.get({ url: '/system/tenant-package/simple-list' }) } ================================================ FILE: yshop-drink-vue3/src/api/system/user/index.ts ================================================ import request from '@/config/axios' export interface UserVO { id: number username: string nickname: string deptId: number postIds: string[] email: string mobile: string sex: number avatar: string loginIp: string status: number remark: string loginDate: Date createTime: Date } // 查询用户管理列表 export const getUserPage = (params: PageParam) => { return request.get({ url: '/system/user/page', params }) } // 查询所有用户列表 export const getAllUser = () => { return request.get({ url: '/system/user/all' }) } // 查询用户详情 export const getUser = (id: number) => { return request.get({ url: '/system/user/get?id=' + id }) } // 新增用户 export const createUser = (data: UserVO) => { return request.post({ url: '/system/user/create', data }) } // 修改用户 export const updateUser = (data: UserVO) => { return request.put({ url: '/system/user/update', data }) } // 删除用户 export const deleteUser = (id: number) => { return request.delete({ url: '/system/user/delete?id=' + id }) } // 导出用户 export const exportUser = (params) => { return request.download({ url: '/system/user/export', params }) } // 下载用户导入模板 export const importUserTemplate = () => { return request.download({ url: '/system/user/get-import-template' }) } // 用户密码重置 export const resetUserPwd = (id: number, password: string) => { const data = { id, password } return request.put({ url: '/system/user/update-password', data: data }) } // 用户状态修改 export const updateUserStatus = (id: number, status: number) => { const data = { id, status } return request.put({ url: '/system/user/update-status', data: data }) } // 获取用户精简信息列表 export const getSimpleUserList = (): Promise => { return request.get({ url: '/system/user/simple-list' }) } ================================================ FILE: yshop-drink-vue3/src/api/system/user/profile.ts ================================================ import request from '@/config/axios' export interface ProfileVO { id: number username: string nickname: string dept: { id: number name: string } roles: { id: number name: string }[] posts: { id: number name: string }[] socialUsers: { type: number openid: string }[] email: string mobile: string sex: number avatar: string status: number remark: string loginIp: string loginDate: Date createTime: Date } export interface UserProfileUpdateReqVO { nickname: string email: string mobile: string sex: number } // 查询用户个人信息 export const getUserProfile = () => { return request.get({ url: '/system/user/profile/get' }) } // 修改用户个人信息 export const updateUserProfile = (data: UserProfileUpdateReqVO) => { return request.put({ url: '/system/user/profile/update', data }) } // 用户密码重置 export const updateUserPassword = (oldPassword: string, newPassword: string) => { return request.put({ url: '/system/user/profile/update-password', data: { oldPassword: oldPassword, newPassword: newPassword } }) } // 用户头像上传 export const uploadAvatar = (data) => { return request.upload({ url: '/system/user/profile/update-avatar', data: data }) } ================================================ FILE: yshop-drink-vue3/src/api/system/user/socialUser.ts ================================================ import request from '@/config/axios' // 社交绑定,使用 code 授权码 export const socialBind = (type, code, state) => { return request.post({ url: '/system/social-user/bind', data: { type, code, state } }) } // 取消社交绑定 export const socialUnbind = (type, openid) => { return request.delete({ url: '/system/social-user/unbind', data: { type, openid } }) } // 社交授权的跳转 export const socialAuthRedirect = (type, redirectUri) => { return request.get({ url: '/system/auth/social-auth-redirect?type=' + type + '&redirectUri=' + redirectUri }) } ================================================ FILE: yshop-drink-vue3/src/api/tools/material.js ================================================ /* * @Author: Gaoxs * @Date: 2023-05-21 23:40:06 * @LastEditors: Gaoxs * @Description: */ import request from '@/config/axios' export async function getPage(query) { return await request.get({ url: '/shop/material/page', params: query }) } export async function addObj(obj) { return await request.post({ url: '/shop/material/create', data: obj }) } export async function getObj(id) { return await request.ge({ url: '/shop/material/' + id }) } export async function delObj(id) { return await request.delete({ url: '/shop/material/delete', params: { id } }) } export async function putObj(obj) { return await request.put({ url: '/shop/material/update', data: obj }) } ================================================ FILE: yshop-drink-vue3/src/api/tools/materialgroup.js ================================================ /* * @Author: Gaoxs * @Date: 2023-05-21 23:40:06 * @LastEditors: Gaoxs * @Description: */ import request from '@/config/axios' export async function getPage(query) { return await request.get({ url: '/shop/material-group/page', params: query }) } export async function getList(query) { return await request.get({ url: '/shop/material-group/list', params: query }) } export async function addObj(obj) { return await request.post({ url: '/shop/material-group/create', data: obj }) } export async function getObj(id) { return await request.ge({ url: '/shop/material-group/' + id }) } export async function delObj(id) { return await request.delete({ url: '/shop/material-group/delete', params: { id } }) } export async function putObj(obj) { return await request.put({ url: '/shop/material-group/update', data: obj }) } ================================================ FILE: yshop-drink-vue3/src/assets/map/json/china.json ================================================ { "type": "FeatureCollection", "features": [ { "type": "Feature", "id": "710000", "properties": { "id": "710000", "cp": [121.509062, 24.044332], "name": "台湾", "childNum": 6 }, "geometry": { "type": "MultiPolygon", "coordinates": [ ["@@°Ü¯Û"], [ "@@ƛĴÕƊÉɼģºðʀ\\ƎsÆNŌÔĚäœnÜƤɊĂǀĆĴžĤNJŨxĚĮǂƺòƌ‚–âÔ®ĮXŦţƸZûЋƕƑGđ¨ĭMó·ęcëƝɉlÝƯֹÅŃ^Ó·śŃNjƏďíåɛGɉ™¿@ăƑŽ¥ĘWǬÏĶŁâ" ], ["@@\\p|WoYG¿¥I†j@¢"], ["@@…¡‰@ˆV^RqˆBbAŒnTXeRz¤Lž«³I"], ["@@ÆEE—„kWqë @œ"], ["@@fced"], ["@@„¯ɜÄèaì¯ØǓIġĽ"], ["@@çûĖ롖hòř "] ], "encodeOffsets": [ [[122886, 24033]], [[123335, 22980]], [[122375, 24193]], [[122518, 24117]], [[124427, 22618]], [[124862, 26043]], [[126259, 26318]], [[127671, 26683]] ] } }, { "type": "Feature", "id": "130000", "properties": { "id": "130000", "cp": [114.502461, 38.045474], "name": "河北", "childNum": 3 }, "geometry": { "type": "MultiPolygon", "coordinates": [ ["@@o~†Z]‚ªr‰ºc_ħ²G¼s`jΟnüsœłNX_“M`ǽÓnUK…Ĝēs¤­©yrý§uģŒc†JŠ›e"], ["@@U`Ts¿m‚"], [ "@@oºƋÄd–eVŽDJj£€J|Ådz•Ft~žKŨ¸IÆv|”‡¢r}膎onb˜}`RÎÄn°ÒdÞ²„^®’lnÐèĄlðӜ×]ªÆ}LiĂ±Ö`^°Ç¶p®đDcœŋ`–ZÔ’¶êqvFƚ†N®ĆTH®¦O’¾ŠIbÐã´BĐɢŴÆíȦp–ĐÞXR€·nndOž¤’OÀĈƒ­Qg˜µFo|gȒęSWb©osx|hYh•gŃfmÖĩnº€T̒Sp›¢dYĤ¶UĈjl’ǐpäìë|³kÛfw²Xjz~ÂqbTŠÑ„ěŨ@|oM‡’zv¢ZrÃVw¬ŧˏfŒ°ÐT€ªqŽs{Sž¯r æÝlNd®²Ğ džiGʂJ™¼lr}~K¨ŸƐÌWö€™ÆŠzRš¤lêmĞL΄’@¡|q]SvK€ÑcwpÏρ†ĿćènĪWlĄkT}ˆJ”¤~ƒÈT„d„™pddʾĬŠ”ŽBVt„EÀ¢ôPĎƗè@~‚k–ü\\rÊĔÖæW_§¼F˜†´©òDòj’ˆYÈrbĞāøŀG{ƀ|¦ðrb|ÀH`pʞkv‚GpuARhÞÆǶgƊTǼƹS£¨¡ù³ŘÍ]¿Ây™ôEP xX¶¹܇O¡“gÚ¡IwÃ鑦ÅB‡Ï|ǰ…N«úmH¯‹âŸDùŽyŜžŲIÄuШDž•¸dɂ‡‚FŸƒ•›Oh‡đ©OŸ›iÃ`ww^ƒÌkŸ‘ÑH«ƇǤŗĺtFu…{Z}Ö@U‡´…ʚLg®¯Oı°ÃwŸ ^˜—€VbÉs‡ˆmA…ê]]w„§›RRl£‡ȭµu¯b{ÍDěïÿȧŽuT£ġƒěŗƃĝ“Q¨fV†Ƌ•ƅn­a@‘³@šď„yýIĹÊKšŭfċŰóŒxV@tˆƯŒJ”]eƒR¾fe|rHA˜|h~Ėƍl§ÏŠlTíb ØoˆÅbbx³^zÃ͚¶Sj®A”yÂhðk`š«P€”ˈµEF†Û¬Y¨Ļrõqi¼‰Wi°§’б´°^[ˆÀ|ĠO@ÆxO\\tŽa\\tĕtû{ġŒȧXýĪÓjùÎRb›š^ΛfK[ݏděYfíÙTyŽuUSyŌŏů@Oi½’éŅ­aVcř§ax¹XŻác‡žWU£ôãºQ¨÷Ñws¥qEH‰Ù|‰›šYQoŕÇyáĂ£MðoťÊ‰P¡mšWO¡€v†{ôvîēÜISpÌhp¨ ‘j†deŔQÖj˜X³à™Ĉ[n`Yp@Už–cM`’RKhŒEbœ”pŞlNut®Etq‚nsÁŠgA‹iú‹oH‡qCX‡”hfgu“~ϋWP½¢G^}¯ÅīGCŸÑ^ãziMáļMTÃƘrMc|O_ž¯Ŏ´|‡morDkO\\mĆJfl@c̬¢aĦtRıҙ¾ùƀ^juųœK­ƒUFy™—Ɲ…›īÛ÷ąV×qƥV¿aȉd³B›qPBm›aËđŻģm“Å®Vйd^K‡KoŸnYg“¯Xhqa”Ldu¥•ÍpDž¡KąÅƒkĝęěhq‡}HyÓ]¹ǧ£…Í÷¿qáµ§š™g‘¤o^á¾ZE‡¤i`ij{n•ƒOl»ŸWÝĔįhg›F[¿¡—ßkOüš_‰€ū‹i„DZàUtėGylƒ}ŒÓM}€jpEC~¡FtoQi‘šHkk{Ãmï‚" ] ], "encodeOffsets": [[[119712, 40641]], [[121616, 39981]], [[116462, 37237]]] } }, { "type": "Feature", "id": "140000", "properties": { "id": "140000", "cp": [111.849248, 36.857014], "name": "山西", "childNum": 1 }, "geometry": { "type": "Polygon", "coordinates": [ "@@Þĩ҃S‰ra}Á€yWix±Üe´lè“ßÓǏok‘ćiµVZģ¡coœ‘TS˹ĪmnÕńe–hZg{gtwªpXaĚThȑp{¶Eh—®RćƑP¿£‘Pmc¸mQÝW•ďȥoÅîɡųAďä³aωJ‘½¥PG­ąSM­™…EÅruµé€‘Yӎ•Ō_d›ĒCo­Èµ]¯_²ÕjāŽK~©ÅØ^ԛkïçămϑk]­±ƒcݯÑÃmQÍ~_a—pm…~ç¡q“ˆu{JÅŧ·Ls}–EyÁÆcI{¤IiCfUc•ƌÃp§]웫vD@¡SÀ‘µM‚ÅwuŽYY‡¡DbÑc¡hƒ×]nkoQdaMç~eD•ÛtT‰©±@¥ù@É¡‰ZcW|WqOJmĩl«ħşvOÓ«IqăV—¥ŸD[mI~Ó¢cehiÍ]Ɠ~ĥqXŠ·eƷœn±“}v•[ěďŽŕ]_‘œ•`‰¹ƒ§ÕōI™o©b­s^}Ét±ū«³p£ÿ·Wµ|¡¥ăFÏs׌¥ŅxŸÊdÒ{ºvĴÎêÌɊ²¶€ü¨|ÞƸµȲ‘LLúÉƎ¤ϊęĔV`„_bª‹S^|ŸdŠzY|dz¥p†ZbÆ£¶ÒK}tĦÔņƠ‚PYzn€ÍvX¶Ěn ĠÔ„zý¦ª˜÷žÑĸَUȌ¸‚dòÜJð´’ìúNM¬ŒXZ´‘¤ŊǸ_tldIš{¦ƀðĠȤ¥NehXnYG‚‡R° ƬDj¬¸|CĞ„Kq‚ºfƐiĺ©ª~ĆOQª ¤@ìǦɌ²æBŒÊ”TœŸ˜ʂōĖ’šĴŞ–ȀœÆÿȄlŤĒö„t”νî¼ĨXhŒ‘˜|ªM¤Ðz" ], "encodeOffsets": [[116874, 41716]] } }, { "type": "Feature", "id": "150000", "properties": { "id": "150000", "cp": [111.670801, 41.818311], "name": "内蒙古", "childNum": 2 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ "@@¯PqƒFB…‰|S•³C|kñ•H‹d‘iÄ¥sˆʼnő…PóÑÑE^‘ÅPpy_YtS™hQ·aHwsOnʼnÚs©iqj›‰€USiº]ïWš‰«gW¡A–Rë¥_ŽsgÁnUI«m‰…„‹]j‡vV¼euhwqA„aW˜ƒ_µj…»çjioQR¹ēÃßt@r³[ÛlćË^ÍÉáG“›OUۗOB±•XŸkŇ¹£k|e]ol™ŸkVͼÕqtaÏõjgÁ£§U^Œ”RLˆËnX°Ç’Bz†^~wfvˆypV ¯„ƫĉ˭ȫƗŷɿÿĿƑ˃ĝÿÃǃßËőó©ǐȍŒĖM×ÍEyx‹þp]Évïè‘vƀnÂĴÖ@‚‰†V~Ĉv¦wĖt—ējyÄDXÄxGQuv_›i¦aBçw‘˛wD™©{ŸtāmQ€{EJ§KPśƘƿ¥@‰sCT•É}ɃwˆƇy±ŸgÑ“}T[÷kÐ禫…SÒ¥¸ëBX½‰HáŵÀğtSÝÂa[ƣ°¯¦P]£ġ“–“Òk®G²„èQ°óMq}EŠóƐÇ\\ƒ‡@áügQ͋u¥Fƒ“T՛¿Jû‡]|mvāÎYua^WoÀa·­ząÒot×¶CLƗi¯¤mƎHNJ¤îìɾŊìTdåwsRÖgĒųúÍġäÕ}Q¶—ˆ¿A•†‹[¡Œ{d×uQAƒ›M•xV‹vMOmăl«ct[wº_šÇʊŽŸjb£ĦS_é“QZ“_lwgOiýe`YYLq§IÁˆdz£ÙË[ÕªuƏ³ÍT—s·bÁĽäė[›b[ˆŗfãcn¥îC¿÷µ[ŏÀQ­ōšĉm¿Á^£mJVm‡—L[{Ï_£›F¥Ö{ŹA}…×Wu©ÅaųijƳhB{·TQqÙIķˑZđ©Yc|M¡…L•eVUóK_QWk’_ĥ‘¿ãZ•»X\\ĴuUƒè‡lG®ěłTĠğDєOrÍd‚ÆÍz]‹±…ŭ©ŸÅ’]ŒÅÐ}UË¥©Tċ™ïxgckfWgi\\ÏĒ¥HkµE˜ë{»ÏetcG±ahUiñiWsɁˆ·c–C‚Õk]wȑ|ća}w…VaĚ᠞ŒG°ùnM¬¯†{ÈˆÐÆA’¥ÄêJxÙ¢”hP¢Ûˆº€µwWOŸóFŽšÁz^ÀŗÎú´§¢T¤ǻƺSė‰ǵhÝÅQgvBHouʝl_o¿Ga{ïq{¥|ſĿHĂ÷aĝÇq‡Z‘ñiñC³ª—…»E`¨åXēÕqÉû[l•}ç@čƘóO¿¡ƒFUsA‰“ʽīccšocƒ‚ƒÇS}„“£‡IS~ălkĩXçmĈ…ŀЂoÐdxÒuL^T{r@¢‘žÍƒĝKén£kQ™‰yšÅõËXŷƏL§~}kqš»IHėDžjĝŸ»ÑÞoŸå°qTt|r©ÏS‹¯·eŨĕx«È[eMˆ¿yuˆ‘pN~¹ÏyN£{©’—g‹ħWí»Í¾s“əšDž_ÃĀɗ±ą™ijĉʍŌŷ—S›É“A‹±åǥɋ@럣R©ąP©}ĹªƏj¹erƒLDĝ·{i«ƫC£µsKCš…GS|úþX”gp›{ÁX¿Ÿć{ƱȏñZáĔyoÁhA™}ŅĆfdʼn„_¹„Y°ėǩÑ¡H¯¶oMQqð¡Ë™|‘Ñ`ƭŁX½·óۓxğįÅcQ‡ˆ“ƒs«tȋDžF“Ÿù^i‘t«Č¯[›hAi©á¥ÇĚ×l|¹y¯YȵƓ‹ñǙµï‚ċ™Ļ|Dœ™üȭ¶¡˜›oŽäÕG\\ďT¿Òõr¯œŸLguÏYęRƩšɷŌO\\İТæ^Ŋ IJȶȆbÜGŽĝ¬¿ĚVĎgª^íu½jÿĕęjık@Ľƒ]ėl¥Ë‡ĭûÁ„ƒėéV©±ćn©­ȇžÍq¯½•YÃÔʼn“ÉNѝÅÝy¹NqáʅDǡËñ­ƁYÅy̱os§ȋµʽǘǏƬɱà‘ưN¢ƔÊuľýľώȪƺɂļžxœZĈ}ÌʼnŪ˜ĺœŽĭFЛĽ̅ȣͽÒŵìƩÇϋÿȮǡŏçƑůĕ~Ǎ›¼ȳÐUf†dIxÿ\\G ˆzâɏÙOº·pqy£†@ŒŠqþ@Ǟ˽IBäƣzsÂZ†ÁàĻdñ°ŕzéØűzșCìDȐĴĺf®ŽÀľưø@ɜÖÞKĊŇƄ§‚͑těï͡VAġÑÑ»d³öǍÝXĉĕÖ{þĉu¸ËʅğU̎éhɹƆ̗̮ȘNJ֥ड़ࡰţાíϲäʮW¬®ҌeרūȠkɬɻ̼ãüfƠSצɩςåȈHϚÎKdzͲOðÏȆƘ¼CϚǚ࢚˼ФԂ¤ƌžĞ̪Qʤ´¼mȠJˀŸƲÀɠmǐnǔĎȆÞǠN~€ʢĜ‚¶ƌĆĘźʆȬ˪ĚǏĞGȖƴƀj`ĢçĶāàŃºē̃ĖćšYŒÀŎüôQÐÂŎŞdžŞêƖš˜oˆDĤÕºÑǘÛˤ³̀gńƘĔÀ^žªƂ`ªt¾äƚêĦĀ¼Ð€Ĕǎ¨Ȕ»͠^ˮÊȦƤøxRrŜH¤¸ÂxDĝŒ|ø˂˜ƮÐ¬ɚwɲFjĔ²Äw°dždÀɞ_ĸdîàŎjʜêTĞªŌ‡ŜWÈ|tqĢUB~´°ÎFC•ŽU¼pĀēƄN¦¾O¶ŠłKĊOj“Ě”j´ĜYp˜{¦„ˆSĚÍ\\Tš×ªV–÷Ší¨ÅDK°ßtŇĔKš¨ǵÂcḷ̌ĚǣȄĽF‡lġUĵœŇ‹ȣFʉɁƒMğįʏƶɷØŭOǽ«ƽū¹Ʊő̝Ȩ§ȞʘĖiɜɶʦ}¨֪ࠜ̀ƇǬ¹ǨE˦ĥªÔêFŽxúQ„Er´W„rh¤Ɛ \\talĈDJ˜Ü|[Pll̚¸ƎGú´Pž¬W¦†^¦–H]prR“n|or¾wLVnÇIujkmon£cX^Bh`¥V”„¦U¤¸}€xRj–[^xN[~ªŠxQ„‚[`ªHÆÂExx^wšN¶Ê˜|¨ì†˜€MrœdYp‚oRzNy˜ÀDs~€bcfÌ`L–¾n‹|¾T‚°c¨È¢a‚r¤–`[|òDŞĔöxElÖdH„ÀI`„Ď\\Àì~ƎR¼tf•¦^¢ķ¶e”ÐÚMŒptgj–„ɡČÅyġLû™ŇV®ŠÄÈƀ†Ď°P|ªVV†ªj–¬ĚÒêp¬–E|ŬÂc|ÀtƐK fˆ{ĘFǜƌXƲąo½Ę‘\\¥–o}›Ûu£ç­kX‘{uĩ«āíÓUŅßŢq€Ť¥lyň[€oi{¦‹L‡ń‡ðFȪȖ”ĒL„¿Ì‹ˆfŒ£K£ʺ™oqNŸƒwğc`ue—tOj×°KJ±qƒÆġm‰Ěŗos¬…qehqsuœƒH{¸kH¡Š…ÊRǪÇƌbȆ¢´ä܍¢NìÉʖ¦â©Ġu¦öČ^â£Ăh–šĖMÈÄw‚\\fŦ°W ¢¾luŸD„wŠ\\̀ʉÌÛM…Ā[bӞEn}¶Vc…ê“sƒ" ] ], "encodeOffsets": [[[129102, 52189]]] } }, { "type": "Feature", "id": "210000", "properties": { "id": "210000", "cp": [123.429096, 41.796767], "name": "辽宁", "childNum": 16 }, "geometry": { "type": "MultiPolygon", "coordinates": [ ["@@L–Ž@@s™a"], ["@@MnNm"], ["@@d‚c"], ["@@eÀ‚C@b‚“‰"], ["@@f‡…Xwkbr–Ä`qg"], ["@@^jtW‘Q"], ["@@~ Y]c"], ["@@G`ĔN^_¿Z‚ÃM"], ["@@iX¶B‹Y"], ["@@„YƒZ"], ["@@L_{Epf"], ["@@^WqCT\\"], ["@@\\[“‹§t|”¤_"], ["@@m`n_"], ["@@Ïxnj{q_×^Giip"], [ "@@@œé^B†‡ntˆaÊU—˜Ÿ]x ¯ÄPIJ­°h€ʙK³†VˆÕ@Y~†|EvĹsDŽ¦­L^p²ŸÒG ’Ël]„xxÄ_˜fT¤Ď¤cŽœP„–C¨¸TVjbgH²sdÎdHt`Bˆ—²¬GJję¶[ÐhjeXdlwhšðSȦªVÊπ‹Æ‘Z˜ÆŶ®²†^ŒÎyÅÎcPqń“ĚDMħĜŁH­ˆk„çvV[ij¼W–‚YÀäĦ’‘`XlžR`žôLUVžfK–¢†{NZdĒª’YĸÌÚJRr¸SA|ƴgŴĴÆbvªØX~†źBŽ|¦ÕœEž¤Ð`\\|Kˆ˜UnnI]¤ÀÂĊnŎ™R®Ő¿¶\\ÀøíDm¦ÎbŨab‰œaĘ\\ľã‚¸a˜tÎSƐ´©v\\ÖÚÌǴ¤Â‡¨JKr€Z_Z€fjþhPkx€`Y”’RIŒjJcVf~sCN¤ ˆE‚œhæm‰–sHy¨SðÑÌ\\\\ŸĐRZk°IS§fqŒßýáЍÙÉÖ[^¯ǤŲ„ê´\\¦¬ĆPM¯£Ÿˆ»uïpùzEx€žanµyoluqe¦W^£ÊL}ñrkqWňûP™‰UP¡ôJŠoo·ŒU}£Œ„[·¨@XŒĸŸ“‹‹DXm­Ûݏº‡›GU‹CÁª½{íĂ^cj‡k“¶Ã[q¤“LÉö³cux«zZfƒ²BWÇ®Yß½ve±ÃC•ý£W{Ú^’q^sÑ·¨‹ÍOt“¹·C¥‡GD›rí@wÕKţ݋˜Ÿ«V·i}xËÍ÷‘i©ĝ‡ɝǡ]ƒˆ{c™±OW‹³Ya±Ÿ‰_穂Hžĕoƫ€Ňqƒr³‰Lys[„ñ³¯OS–ďOMisZ†±ÅFC¥Pq{‚Ã[Pg}\\—¿ghćO…•k^ģÁFıĉĥM­oEqqZûěʼn³F‘¦oĵ—hŸÕP{¯~TÍlª‰N‰ßY“Ð{Ps{ÃVU™™eĎwk±ʼnVÓ½ŽJãÇÇ»Jm°dhcÀff‘dF~ˆ€ĀeĖ€d`sx² šƒ®EżĀdQ‹Âd^~ăÔHˆ¦\\›LKpĄVez¤NP ǹӗR™ÆąJSh­a[¦´Âghwm€BÐ¨źhI|žVVŽ—Ž|p] Â¼èNä¶ÜBÖ¼“L`‚¼bØæŒKV”ŸpoœúNZÞÒKxpw|ÊEMnzEQšŽIZ”ŽZ‡NBˆčÚFÜçmĩ‚WĪñt‘ÞĵÇñZ«uD‚±|Əlij¥ãn·±PmÍa‰–da‡ CL‡Ǒkùó¡³Ï«QaċϑOÃ¥ÕđQȥċƭy‹³ÃA" ] ], "encodeOffsets": [ [[123686, 41445]], [[126019, 40435]], [[124393, 40128]], [[126117, 39963]], [[125322, 40140]], [[126686, 40700]], [[126041, 40374]], [[125584, 40168]], [[125453, 40165]], [[125362, 40214]], [[125280, 40291]], [[125774, 39997]], [[125976, 40496]], [[125822, 39993]], [[125509, 40217]], [[122731, 40949]] ] } }, { "type": "Feature", "id": "220000", "properties": { "id": "220000", "cp": [125.3245, 43.886841], "name": "吉林", "childNum": 1 }, "geometry": { "type": "Polygon", "coordinates": [ "@@‘p䔳PClƒFbbÍzš€wBG’ĭ€Z„Åi“»ƒlY­ċ²SgŽkÇ£—^S‰“qd¯•‹R…©éŽ£¯S†\\cZ¹iűƏCuƍÓX‡oR}“M^o•£…R}oªU­F…uuXHlEŕ‡€Ï©¤ÛmTŽþ¤D–²ÄufàÀ­XXȱAe„yYw¬dvõ´KÊ£”\\rµÄl”iˆdā]|DÂVŒœH¹ˆÞ®ÜWnŒC”Œķ W‹§@\\¸‹ƒ~¤‹Vp¸‰póIO¢ŠVOšŇürXql~òÉK]¤¥Xrfkvzpm¶bwyFoúvð‡¼¤ N°ąO¥«³[ƒéǡű_°Õ\\ÚÊĝŽþâőàerR¨­JYlďQ[ ÏYëЧTGz•tnŠß¡gFkMŸāGÁ¤ia É‰™È¹`\\xs€¬dĆkNnuNUŠ–užP@‚vRY¾•–\\¢…ŒGªóĄ~RãÖÎĢù‚đŴÕhQŽxtcæëSɽʼníëlj£ƍG£nj°KƘµDsØÑpyƸ®¿bXp‚]vbÍZuĂ{nˆ^IüœÀSք”¦EŒvRÎûh@℈[‚Əȉô~FNr¯ôçR±ƒ­HÑl•’Ģ–^¤¢‚OðŸŒævxsŒ]ÞÁTĠs¶¿âƊGW¾ìA¦·TѬ†è¥€ÏÐJ¨¼ÒÖ¼ƒƦɄxÊ~S–tD@ŠĂ¼Ŵ¡jlºWžvЉˆzƦZЎ²CH— „Axiukd‹ŒGgetqmcžÛ£Ozy¥cE}|…¾cZ…k‚‰¿uŐã[oxGikfeäT@…šSUwpiÚFM©’£è^ڟ‚`@v¶eň†f h˜eP¶žt“äOlÔUgƒÞzŸU`lœ}ÔÆUvØ_Ō¬Öi^ĉi§²ÃŠB~¡Ĉ™ÚEgc|DC_Ȧm²rBx¼MÔ¦ŮdĨÃâYx‘ƘDVÇĺĿg¿cwÅ\\¹˜¥Yĭlœ¤žOv†šLjM_a W`zļMž·\\swqÝSA‡š—q‰Śij¯Š‘°kŠRē°wx^Đkǂғ„œž“œŽ„‹\\]˜nrĂ}²ĊŲÒøãh·M{yMzysěnĒġV·°“G³¼XÀ““™¤¹i´o¤ŃšŸÈ`̃DzÄUĞd\\i֚ŒˆmÈBĤÜɲDEh LG¾ƀľ{WaŒYÍȏĢĘÔRîĐj‹}Ǟ“ccj‡oUb½š{“h§Ǿ{K‹ƖµÎ÷žGĀÖŠåưÎs­l›•yiē«‹`姝H¥Ae^§„GK}iã\\c]v©ģZ“mÃ|“[M}ģTɟĵ‘Â`À–çm‰‘FK¥ÚíÁbXš³ÌQґHof{‰]e€pt·GŋĜYünĎųVY^’˜ydõkÅZW„«WUa~U·Sb•wGçǑ‚“iW^q‹F‚“›uNĝ—·Ew„‹UtW·Ýďæ©PuqEzwAV•—XR‰ãQ`­©GŒM‡ehc›c”ďϝd‡©ÑW_ϗYƅŒ»…é\\ƒɹ~ǙG³mØ©BšuT§Ĥ½¢Ã_ý‘L¡‘ýŸqT^rme™\\Pp•ZZbƒyŸ’uybQ—efµ]UhĿDCmûvašÙNSkCwn‰cćfv~…Y‹„ÇG" ], "encodeOffsets": [[130196, 42528]] } }, { "type": "Feature", "id": "230000", "properties": { "id": "230000", "cp": [128.642464, 46.756967], "name": "黑龙江", "childNum": 2 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ "@@UƒµNÿ¥īè灋•HÍøƕ¶LŒǽ|g¨|”™Ža¾pViˆdd”~ÈiŒíďÓQġėǐZ΋ŽXb½|ſÃH½ŸKFgɱCģÛÇA‡n™‹jÕc[VĝDZÃ˄Ç_™ £ń³pŽj£º”š¿”»WH´¯”U¸đĢmžtĜyzzNN|g¸÷äűѱĉā~mq^—Œ[ƒ”››”ƒǁÑďlw]¯xQĔ‰¯l‰’€°řĴrŠ™˜BˆÞTxr[tޏĻN_yŸX`biN™Ku…P›£k‚ZĮ—¦[ºxÆÀdhŽĹŀUÈƗCw’áZħÄŭcÓ¥»NAw±qȥnD`{ChdÙFćš}¢‰A±Äj¨]ĊÕjŋ«×`VuÓś~_kŷVÝyh„“VkÄãPs”Oµ—fŸge‚Ň…µf@u_Ù ÙcŸªNªÙEojVx™T@†ãSefjlwH\\pŏäÀvŠŽlY†½d{†F~¦dyz¤PÜndsrhf‹HcŒvlwjFœ£G˜±DύƥY‡yϊu¹XikĿ¦ÏqƗǀOŜ¨LI|FRĂn sª|Cš˜zxAè¥bœfudTrFWÁ¹Am|˜ĔĕsķÆF‡´Nš‰}ć…UŠÕ@Áijſmužç’uð^ÊýowŒFzØÎĕNőžǏȎôªÌŒDŽàĀÄ˄ĞŀƒʀĀƘŸˮȬƬĊ°ƒUŸzou‡xe]}Ž…AyȑW¯ÌmK‡“Q]‹Īºif¸ÄX|sZt|½ÚUΠlkš^p{f¤lˆºlÆW –€A²˜PVܜPH”Êâ]ÎĈÌÜk´\\@qàsĔÄQºpRij¼èi†`¶—„bXƒrBgxfv»ŽuUiˆŒ^v~”J¬mVp´£Œ´VWrnP½ì¢BX‚¬h™ŠðX¹^TjVœŠriªj™tŊÄm€tPGx¸bgRšŽsT`ZozÆO]’ÒFô҆Oƒ‡ŊŒvŞ”p’cGŒêŠsx´DR–Œ{A†„EOr°Œ•žx|íœbˆ³Wm~DVjºéNN†Ëܲɶ­GƒxŷCStŸ}]ûō•SmtuÇÃĕN•™āg»šíT«u}ç½BĵÞʣ¥ëÊ¡Mێ³ãȅ¡ƋaǩÈÉQ‰†G¢·lG|›„tvgrrf«†ptęŘnŠÅĢr„I²¯LiØsPf˜_vĠd„xM prʹšL¤‹¤‡eˌƒÀđK“žïÙVY§]I‡óáĥ]ķ†Kˆ¥Œj|pŇ\\kzţ¦šnņäÔVĂîά|vW’®l¤èØr‚˜•xm¶ă~lÄƯĄ̈́öȄEÔ¤ØQĄ–Ą»ƢjȦOǺ¨ìSŖÆƬy”Qœv`–cwƒZSÌ®ü±DŽ]ŀç¬B¬©ńzƺŷɄeeOĨS’Œfm Ċ‚ƀP̎ēz©Ċ‚ÄÕÊmgŸÇsJ¥ƔˆŊśæ’΁Ñqv¿íUOµª‰ÂnĦÁ_½ä@ê텣P}Ġ[@gġ}g“ɊדûÏWXá¢užƻÌsNͽƎÁ§č՛AēeL³àydl›¦ĘVçŁpśdžĽĺſʃQíÜçÛġԏsĕ¬—Ǹ¯YßċġHµ ¡eå`ļƒrĉŘóƢFì“ĎWøxÊk†”ƈdƬv|–I|·©NqńRŀƒ¤é”eŊœŀ›ˆàŀU²ŕƀB‚Q£Ď}L¹Îk@©ĈuǰųǨ”Ú§ƈnTËÇéƟÊcfčŤ^Xm‡—HĊĕË«W·ċëx³ǔķÐċJā‚wİ_ĸ˜Ȁ^ôWr­°oú¬Ħ…ŨK~”ȰCĐ´Ƕ£’fNÎèâw¢XnŮeÂÆĶŽ¾¾xäLĴĘlļO¤ÒĨA¢Êɚ¨®‚ØCÔ ŬGƠ”ƦYĜ‡ĘÜƬDJ—g_ͥœ@čŅĻA“¶¯@wÎqC½Ĉ»NŸăëK™ďÍQ“Ùƫ[«Ãí•gßÔÇOÝáW‘ñuZ“¯ĥ€Ÿŕā¡ÑķJu¤E Ÿå¯°WKɱ_d_}}vyŸõu¬ï¹ÓU±½@gÏ¿rýD‰†g…Cd‰µ—°MFYxw¿CG£‹Rƛ½Õ{]L§{qqąš¿BÇƻğëšܭNJË|c²}Fµ}›ÙRsÓpg±ŠQNqǫŋRwŕnéÑÉKŸ†«SeYR…ŋ‹@{¤SJ}šD Ûǖ֍Ÿ]gr¡µŷjqWÛham³~S«“„›Þ]" ] ], "encodeOffsets": [[[134456, 44547]]] } }, { "type": "Feature", "id": "320000", "properties": { "id": "320000", "cp": [119.767413, 33.041544], "name": "江苏", "childNum": 1 }, "geometry": { "type": "Polygon", "coordinates": [ "@@cþÅPiŠ`ZŸRu¥É\\]~°ŽY`µ†Óƒ^phÁbnÀşúŽòa–ĬºTÖŒb‚˜e¦¦€{¸ZâćNpŒ©žHr|^ˆmjhŠSEb\\afv`sz^lkŽlj‹Ätg‹¤D˜­¾Xš¿À’|ДiZ„ȀåB·î}GL¢õcßjaŸyBFµÏC^ĭ•cÙt¿sğH]j{s©HM¢ƒQnDÀ©DaÜތ·jgàiDbPufjDk`dPOîƒhw¡ĥ‡¥šG˜ŸP²ĐobºrY†„î¶aHŢ´ ]´‚rılw³r_{£DB_Ûdåuk|ˆŨ¯F Cºyr{XFy™e³Þċ‡¿Â™kĭB¿„MvÛpm`rÚã”@ƹhågËÖƿxnlč¶Åì½Ot¾dJlŠVJʜǀœŞqvnOŠ^ŸJ”Z‘ż·Q}ê͎ÅmµÒ]Žƍ¦Dq}¬R^èĂ´ŀĻĊIԒtžIJyQŐĠMNtœR®òLh‰›Ěs©»œ}OӌGZz¶A\\jĨFˆäOĤ˜HYš†JvÞHNiÜaϚɖnFQlšNM¤ˆB´ĄNöɂtp–Ŭdf先‹qm¿QûŠùއÚb¤uŃJŴu»¹Ą•lȖħŴw̌ŵ²ǹǠ͛hĭłƕrçü±Y™xci‡tğ®jű¢KOķ•Coy`å®VTa­_Ā]ŐÝɞï²ʯÊ^]afYǸÃĆēĪȣJđ͍ôƋĝÄ͎ī‰çÛɈǥ£­ÛmY`ó£Z«§°Ó³QafusNıDž_k}¢m[ÝóDµ—¡RLčiXy‡ÅNïă¡¸iĔϑNÌŕoēdōîåŤûHcs}~Ûwbù¹£¦ÓCt‹OPrƒE^ÒoŠg™ĉIµžÛÅʹK…¤½phMŠü`o怆ŀ" ], "encodeOffsets": [[121740, 32276]] } }, { "type": "Feature", "id": "330000", "properties": { "id": "330000", "cp": [120.153576, 29.287459], "name": "浙江", "childNum": 45 }, "geometry": { "type": "MultiPolygon", "coordinates": [ ["@@E^dQ]K"], ["@@jX^j‡"], ["@@sfŠbU‡"], ["@@qP\\xz[ck"], ["@@‘Rƒ¢‚FX}°[s_"], ["@@Cbœ\\—}"], ["@@e|v\\la{u"], ["@@v~u}"], ["@@QxÂF¯}"], ["@@¹nŒvÞs¯o"], ["@@rSkUEj"], ["@@bi­ZŒP"], ["@@p[}INf"], ["@@À¿€"], ["@@¹dnbŒ…"], ["@@rSŸBnR"], ["@@g~h}"], ["@@FlEk"], ["@@OdPc"], ["@@v[u\\"], ["@@FjâL~wyoo~›sµL–\\"], ["@@¬e¹aNˆ"], ["@@\\nÔ¡q]L³ë\\ÿ®ŒQ֎"], ["@@ÊA­©[¬"], ["@@KxŒv­"], ["@@@hlIk]"], ["@@pW{o||j"], ["@@Md|_mC"], ["@@¢…X£ÏylD¼XˆtH"], ["@@hlÜ[LykAvyfw^Ež›¤"], ["@@fp¤Mus“R"], ["@@®_ma~•LÁ¬šZ"], ["@@iM„xZ"], ["@@ZcYd"], ["@@Z~dOSo|A¿qZv"], ["@@@`”EN¡v"], ["@@|–TY{"], ["@@@n@m"], ["@@XWkCT\\"], ["@@ºwšZRkĕWO¢"], ["@@™X®±Grƪ\\ÔáXq{‹"], ["@@ůTG°ĄLHm°UC‹"], [ "@@¤Ž€aÜx~}dtüGæţŎíĔcŖpMËВj碷ðĄÆMzˆjWKĎ¢Q¶˜À_꒔_Bı€i«pZ€gf€¤Nrq]§ĂN®«H±‡yƳí¾×ŸīàLłčŴǝĂíÀBŖÕªˆŠÁŖHŗʼnåqûõi¨hÜ·ƒñt»¹ýv_[«¸m‰YL¯‰Qª…mĉÅdMˆ•gÇjcº«•ęœ¬­K­´ƒB«Âącoċ\\xKd¡gěŧ«®á’[~ıxu·Å”KsËɏc¢Ù\\ĭƛëbf¹­ģSƒĜkáƉÔ­ĈZB{ŠaM‘µ‰fzʼnfåÂŧįƋǝÊĕġć£g³ne­ą»@­¦S®‚\\ßðCšh™iqªĭiAu‡A­µ”_W¥ƣO\\lċĢttC¨£t`ˆ™PZäuXßBs‡Ļyek€OđġĵHuXBšµ]׌‡­­\\›°®¬F¢¾pµ¼kŘó¬Wät’¸|@ž•L¨¸µr“ºù³Ù~§WI‹ŸZWŽ®’±Ð¨ÒÉx€`‰²pĜ•rOògtÁZ}þÙ]„’¡ŒŸFK‚wsPlU[}¦Rvn`hq¬\\”nQ´ĘRWb”‚_ rtČFI֊kŠŠĦPJ¶ÖÀÖJĈĄTĚòžC ²@Pú…Øzœ©PœCÈÚœĒ±„hŖ‡l¬â~nm¨f©–iļ«m‡nt–u†ÖZÜÄj“ŠLŽ®E̜Fª²iÊxبžIÈhhst" ], ["@@o\\V’zRZ}y"], ["@@†@°¡mۛGĕ¨§Ianá[ýƤjfæ‡ØL–•äGr™"] ], "encodeOffsets": [ [[125592, 31553]], [[125785, 31436]], [[125729, 31431]], [[125513, 31380]], [[125223, 30438]], [[125115, 30114]], [[124815, 29155]], [[124419, 28746]], [[124095, 28635]], [[124005, 28609]], [[125000, 30713]], [[125111, 30698]], [[125078, 30682]], [[125150, 30684]], [[124014, 28103]], [[125008, 31331]], [[125411, 31468]], [[125329, 31479]], [[125626, 30916]], [[125417, 30956]], [[125254, 30976]], [[125199, 30997]], [[125095, 31058]], [[125083, 30915]], [[124885, 31015]], [[125218, 30798]], [[124867, 30838]], [[124755, 30788]], [[124802, 30809]], [[125267, 30657]], [[125218, 30578]], [[125200, 30562]], [[124968, 30474]], [[125167, 30396]], [[124955, 29879]], [[124714, 29781]], [[124762, 29462]], [[124325, 28754]], [[123990, 28459]], [[125366, 31477]], [[125115, 30363]], [[125369, 31139]], [[122495, 31878]], [[125329, 30690]], [[125192, 30787]] ] } }, { "type": "Feature", "id": "340000", "properties": { "id": "340000", "cp": [117.283042, 31.26119], "name": "安徽", "childNum": 3 }, "geometry": { "type": "MultiPolygon", "coordinates": [ ["@@^iuLX^"], ["@@‚e©Ehl"], [ "@@°ZÆëϵmkǀwÌÕæhºgBĝâqÙĊz›ÖgņtÀÁÊÆá’hEz|WzqD¹€Ÿ°E‡ŧl{ævÜcA`¤C`|´qžxIJkq^³³ŸGšµbƒíZ…¹qpa±ď OH—¦™Ħˆx¢„gPícOl_iCveaOjCh߸i݋bÛªCC¿€m„RV§¢A|t^iĠGÀtÚs–d]ĮÐDE¶zAb àiödK¡~H¸íæAžǿYƒ“j{ď¿‘™À½W—®£ChŒÃsiŒkkly]_teu[bFa‰Tig‡n{]Gqªo‹ĈMYá|·¥f¥—őaSÕė™NµñĞ«ImŒ_m¿Âa]uĜp …Z_§{Cƒäg¤°r[_Yj‰ÆOdý“[ŽI[á·¥“Q_n‡ùgL¾mv™ˊBÜÆ¶ĊJhšp“c¹˜O]iŠ]œ¥ jtsggJǧw×jÉ©±›EFˍ­‰Ki”ÛÃÕYv…s•ˆm¬njĻª•§emná}k«ŕˆƒgđ²Ù›DǤ›í¡ªOy›†×Où±@DŸñSęćăÕIÕ¿IµĥO‰‰jNÕËT¡¿tNæŇàåyķrĕq§ÄĩsWÆßŽF¶žX®¿‰mŒ™w…RIޓfßoG‘³¾©uyH‘į{Ɓħ¯AFnuP…ÍÔzšŒV—dàôº^Ðæd´€‡oG¤{S‰¬ćxã}›ŧ×Kǥĩ«žÕOEзÖdÖsƘѨ[’Û^Xr¢¼˜§xvěƵ`K”§ tÒ´Cvlo¸fzŨð¾NY´ı~ÉĔē…ßúLÃϖ_ÈÏ|]ÂÏFl”g`bšežž€n¾¢pU‚h~ƴ˶_‚r sĄ~cž”ƈ]|r c~`¼{À{ȒiJjz`îÀT¥Û³…]’u}›f…ïQl{skl“oNdŸjŸäËzDvčoQŠďHI¦rb“tHĔ~BmlRš—V_„ħTLnñH±’DžœL‘¼L˜ªl§Ťa¸ŒĚlK²€\\RòvDcÎJbt[¤€D@®hh~kt°ǾzÖ@¾ªdb„YhüóZ ň¶vHrľ\\ʗJuxAT|dmÀO„‹[ÃԋG·ĚąĐlŪÚpSJ¨ĸˆLvÞcPæķŨŽ®mАˆálŸwKhïgA¢ųƩޖ¤OȜm’°ŒK´" ] ], "encodeOffsets": [[[121722, 32278]], [[119475, 30423]], [[119168, 35472]]] } }, { "type": "Feature", "id": "350000", "properties": { "id": "350000", "cp": [118.306239, 26.075302], "name": "福建", "childNum": 18 }, "geometry": { "type": "MultiPolygon", "coordinates": [ ["@@“zht´‡]"], ["@@aj^~ĆG—©O"], ["@@ed¨„C}}i"], ["@@@vˆPGsQ"], ["@@‰sBz‚ddW]Q"], ["@@SލQ“{"], ["@@NŽVucW"], ["@@qptBAq"], ["@@‰’¸[mu"], ["@@Q\\pD]_"], ["@@jSwUadpF"], ["@@eXª~ƒ•"], ["@@AjvFso"], ["@@fT–›_Çí\\Ÿ™—v|ba¦jZÆy€°"], ["@@IjJi"], ["@@wJI€ˆxš«¼AoNe{M­"], ["@@K‰±¡Óˆ”ČäeZ"], [ "@@k¡¹Eh~c®wBk‹UplÀ¡I•~Māe£bN¨gZý¡a±Öcp©PhžI”Ÿ¢Qq…ÇGj‹|¥U™ g[Ky¬ŏ–v@OpˆtÉEŸF„\\@ åA¬ˆV{Xģ‰ĐBy…cpě…¼³Ăp·¤ƒ¥o“hqqÚ¡ŅLsƒ^ᗞ§qlŸÀhH¨MCe»åÇGD¥zPO£čÙkJA¼ß–ėu›ĕeûҍiÁŧSW¥˜QŠûŗ½ùěcݧSùĩąSWó«íęACµ›eR—åǃRCÒÇZÍ¢‹ź±^dlsŒtjD¸•‚ZpužÔâÒH¾oLUêÃÔjjēò´ĄW‚ƛ…^Ñ¥‹ĦŸ@Çò–ŠmŒƒOw¡õyJ†yD}¢ďÑÈġfŠZd–a©º²z£šN–ƒjD°Ötj¶¬ZSÎ~¾c°¶Ðm˜x‚O¸¢Pl´žSL|¥žA†ȪĖM’ņIJg®áIJČĒü` ŽQF‡¬h|ÓJ@zµ |ê³È ¸UÖŬŬÀEttĸr‚]€˜ðŽM¤ĶIJHtÏ A’†žĬkvsq‡^aÎbvŒd–™fÊòSD€´Z^’xPsÞrv‹ƞŀ˜jJd×ŘÉ ®A–ΦĤd€xĆqAŒ†ZR”ÀMźŒnĊ»ŒİÐZ— YX–æJŠyĊ²ˆ·¶q§·–K@·{s‘Xãô«lŗ¶»o½E¡­«¢±¨Yˆ®Ø‹¶^A™vWĶGĒĢžPlzfˆļŽtàAvWYãšO_‡¤sD§ssČġ[kƤPX¦Ž`¶“ž®ˆBBvĪjv©šjx[L¥àï[F…¼ÍË»ğV`«•Ip™}ccÅĥZE‹ãoP…´B@ŠD—¸m±“z«Ƴ—¿å³BRضˆœWlâþäą`“]Z£Tc— ĹGµ¶H™m@_©—kŒ‰¾xĨ‡ôȉðX«½đCIbćqK³Á‹Äš¬OAwã»aLʼn‡ËĥW[“ÂGI—ÂNxij¤D¢ŽîĎÎB§°_JœGsƒ¥E@…¤uć…P‘å†cuMuw¢BI¿‡]zG¹guĮck\\_" ] ], "encodeOffsets": [ [[123250, 27563]], [[122541, 27268]], [[123020, 27189]], [[122916, 27125]], [[122887, 26845]], [[122808, 26762]], [[122568, 25912]], [[122778, 26197]], [[122515, 26757]], [[122816, 26587]], [[123388, 27005]], [[122450, 26243]], [[122578, 25962]], [[121255, 25103]], [[120987, 24903]], [[122339, 25802]], [[121042, 25093]], [[122439, 26024]] ] } }, { "type": "Feature", "id": "360000", "properties": { "id": "360000", "cp": [115.592151, 27.676493], "name": "江西", "childNum": 1 }, "geometry": { "type": "Polygon", "coordinates": [ "@@ĢĨƐgÂMD~ņªe^\\^§„ý©j׍cZ†Ø¨zdÒa¶ˆlҍJŒìõ`oz÷@¤u޸´†ôęöY¼‰HČƶajlÞƩ¥éZ[”|h}^U Œ ¥p„ĄžƦO lt¸Æ €Q\\€ŠaÆ|CnÂOjt­ĚĤd’ÈŒF`’¶„@Ð딠¦ōҞ¨Sêv†HĢûXD®…QgėWiØPÞìºr¤dž€NĠ¢l–•ĄtZoœCƞÔºCxrpĠV®Ê{f_Y`_ƒeq’’®Aot`@o‚DXfkp¨|Šs¬\\D‘ÄSfè©Hn¬…^DhÆyøJh“ØxĢĀLʈ„ƠPżċĄwȠ̦G®ǒĤäTŠÆ~ĦwŠ«|TF¡Šn€c³Ïå¹]ĉđxe{ÎӐ†vOEm°BƂĨİ|G’vz½ª´€H’àp”eJ݆Qšxn‹ÀŠW­žEµàXÅĪt¨ÃĖrÄwÀFÎ|ňÓMå¼ibµ¯»åDT±m[“r«_gŽmQu~¥V\\OkxtL E¢‹ƒ‘Ú^~ýê‹Pó–qo슱_Êw§ÑªåƗ⼋mĉŹ‹¿NQ“…YB‹ąrwģcÍ¥B•Ÿ­ŗÊcØiI—žƝĿuŒqtāwO]‘³YCñTeɕš‹caub͈]trlu€ī…B‘ПGsĵıN£ï—^ķqss¿FūūV՟·´Ç{éĈý‰ÿ›OEˆR_ŸđûIċâJh­ŅıN‘ȩĕB…¦K{Tk³¡OP·wn—µÏd¯}½TÍ«YiµÕsC¯„iM•¤™­•¦¯P|ÿUHv“he¥oFTu‰õ\\ŽOSs‹MòđƇiaºćXŸĊĵà·çhƃ÷ǜ{‘ígu^›đg’m[×zkKN‘¶Õ»lčÓ{XSƉv©_ÈëJbVk„ĔVÀ¤P¾ºÈMÖxlò~ªÚàGĂ¢B„±’ÌŒK˜y’áV‡¼Ã~­…`g›ŸsÙfI›Ƌlę¹e|–~udjˆuTlXµf`¿JdŠ[\\˜„L‚‘²" ], "encodeOffsets": [[116689, 26234]] } }, { "type": "Feature", "id": "370000", "properties": { "id": "370000", "cp": [118.000923, 36.275807], "name": "山东", "childNum": 13 }, "geometry": { "type": "MultiPolygon", "coordinates": [ ["@@Xjd]{K"], ["@@itbFHy"], ["@@HlGk"], ["@@T‚ŒGŸy"], ["@@K¬˜•‹U"], ["@@WdXc"], ["@@PtOs"], ["@@•LnXhc"], ["@@ppVƒu]Or"], ["@@cdzAUa"], ["@@udRhnCI‡"], ["@@ˆoIƒpR„"], [ "@@Ľč{fzƤî’Kš–ÎMĮ]†—ZFˆ½Y]â£ph’™š¶¨râøÀ†ÎǨ¤^ºÄ”Gzˆ~grĚĜlĞÆ„LĆdž¢Îo¦–cv“Kb€gr°Wh”mZp ˆL]LºcU‰Æ­n”żĤÌǜbAnrOAœ´žȊcÀbƦUØrĆUÜøœĬƞ†š˜Ez„VL®öØBkŖÝĐ˹ŧ̄±ÀbÎɜnb²ĦhņBĖ›žįĦåXćì@L¯´ywƕCéõė ƿ¸‘lµ¾Z|†ZWyFYŸ¨Mf~C¿`€à_RÇzwƌfQnny´INoƬˆèôº|sT„JUš›‚L„îVj„ǎ¾Ē؍‚Dz²XPn±ŴPè¸ŔLƔÜƺ_T‘üÃĤBBċȉöA´fa„˜M¨{«M`‡¶d¡ô‰Ö°šmȰBÔjjŒ´PM|”c^d¤u•ƒ¤Û´Œä«ƢfPk¶Môlˆ]Lb„}su^ke{lC‘…M•rDŠÇ­]NÑFsmoõľH‰yGă{{çrnÓE‰‹ƕZGª¹Fj¢ïW…uøCǷ돡ąuhÛ¡^Kx•C`C\\bÅxì²ĝÝ¿_N‰īCȽĿåB¥¢·IŖÕy\\‡¹kx‡Ã£Č×GDyÕ¤ÁçFQ¡„KtŵƋ]CgÏAùSed‡cÚź—ŠuYfƒyMmhUWpSyGwMPqŀ—›Á¼zK›¶†G•­Y§Ëƒ@–´śÇµƕBmœ@Io‚g——Z¯u‹TMx}C‘‰VK‚ï{éƵP—™_K«™pÛÙqċtkkù]gŽ‹Tğwo•ɁsMõ³ă‡AN£™MRkmEʕč™ÛbMjÝGu…IZ™—GPģ‡ãħE[iµBEuŸDPԛ~ª¼ętŠœ]ŒûG§€¡QMsğNPŏįzs£Ug{đJĿļā³]ç«Qr~¥CƎÑ^n¶ÆéÎR~ݏY’I“] P‰umŝrƿ›‰›Iā‹[x‰edz‹L‘¯v¯s¬ÁY…~}…ťuٌg›ƋpÝĄ_ņī¶ÏSR´ÁP~ž¿Cyžċßdwk´Ss•X|t‰`Ä Èð€AªìÎT°¦Dd–€a^lĎDĶÚY°Ž`ĪŴǒˆ”àŠv\\ebŒZH„ŖR¬ŢƱùęO•ÑM­³FۃWp[ƒ" ] ], "encodeOffsets": [ [[123806, 39303]], [[123821, 39266]], [[123742, 39256]], [[123702, 39203]], [[123649, 39066]], [[123847, 38933]], [[123580, 38839]], [[123894, 37288]], [[123043, 36624]], [[123344, 38676]], [[123522, 38857]], [[123628, 38858]], [[118260, 36742]] ] } }, { "type": "Feature", "id": "410000", "properties": { "id": "410000", "cp": [113.665412, 33.757975], "name": "河南", "childNum": 1 }, "geometry": { "type": "Polygon", "coordinates": [ "@@•ýL™ùµP³swIÓxcŢĞð†´E®žÚPt†ĴXØx¶˜@«ŕŕQGƒ‹Yfa[şu“ßǩ™đš_X³ijÕčC]kbc•¥CS¯ëÍB©÷‹–³­Siˆ_}m˜YTtž³xlàcȂzÀD}ÂOQ³ÐTĨ¯†ƗòËŖ[hœł‹Ŧv~††}ÂZž«¤lPǕ£ªÝŴÅR§ØnhcŒtâk‡nύ­ľŹUÓÝdKuķ‡I§oTũÙďkęĆH¸ÓŒ\\ăŒ¿PcnS{wBIvɘĽ[GqµuŸŇôYgûƒZcaŽ©@½Õǽys¯}lgg@­C\\£as€IdÍuCQñ[L±ęk·‹ţb¨©kK—’»›KC²‘òGKmĨS`ƒ˜UQ™nk}AGē”sqaJ¥ĐGR‰ĎpCuÌy ã iMc”plk|tRk†ðœev~^‘´†¦ÜŽSí¿_iyjI|ȑ|¿_»d}qŸ^{“Ƈdă}Ÿtqµ`Ƴĕg}V¡om½fa™Ço³TTj¥„tĠ—Ry”K{ùÓjuµ{t}uËR‘iŸvGŠçJFjµŠÍyqΘàQÂFewixGw½Yŷpµú³XU›½ġy™łå‰kÚwZXˆ·l„¢Á¢K”zO„Λ΀jc¼htoDHr…|­J“½}JZ_¯iPq{tę½ĕ¦Zpĵø«kQ…Ťƒ]MÛfaQpě±ǽ¾]u­Fu‹÷nƒ™čįADp}AjmcEǒaª³o³ÆÍSƇĈÙDIzˑ赟^ˆKLœ—i—Þñ€[œƒaA²zz‰Ì÷Dœ|[šíijgf‚ÕÞd®|`ƒĆ~„oĠƑô³Ŋ‘D×°¯CsŠøÀ«ì‰UMhTº¨¸ǡîS–Ô„DruÂÇZ•ÖEŽ’vPZ„žW”~؋ÐtĄE¢¦Ðy¸bŠô´oŬ¬Ž²Ês~€€]®tªašpŎJ¨Öº„_ŠŔ–`’Ŗ^Ѝ\\Ĝu–”~m²Ƹ›¸fW‰ĦrƔ}Î^gjdfÔ¡J}\\n C˜¦þWxªJRÔŠu¬ĨĨmF†dM{\\d\\ŠYÊ¢ú@@¦ª²SŠÜsC–}fNècbpRmlØ^g„d¢aÒ¢CZˆZxvÆ¶N¿’¢T@€uCœ¬^ĊðÄn|žlGl’™Rjsp¢ED}€Fio~ÔNŽ‹„~zkĘHVsDzßjƒŬŒŠŢ`Pûàl¢˜\\ÀœEhŽİgÞē X¼Pk–„|m" ], "encodeOffsets": [[118256, 37017]] } }, { "type": "Feature", "id": "420000", "properties": { "id": "420000", "cp": [113.298572, 30.684355], "name": "湖北", "childNum": 3 }, "geometry": { "type": "MultiPolygon", "coordinates": [ ["@@AB‚"], ["@@lskt"], [ "@@¾«}{ra®pîÃ\\™›{øCŠËyyB±„b\\›ò˜Ý˜jK›‡L ]ĎĽÌ’JyÚCƈćÎT´Å´pb©È‘dFin~BCo°BĎĚømvŒ®E^vǾ½Ĝ²Ro‚bÜeNŽ„^ĺ£R†¬lĶ÷YoĖ¥Ě¾|sOr°jY`~I”¾®I†{GqpCgyl{‡£œÍƒÍyPL“¡ƒ¡¸kW‡xYlÙæŠšŁĢzœ¾žV´W¶ùŸo¾ZHxjwfx„GNÁ•³Xéæl¶‰EièIH‰ u’jÌQ~v|sv¶Ôi|ú¢Fh˜Qsğ¦ƒSiŠBg™ÐE^ÁÐ{–čnOÂȞUÎóĔ†ÊēIJ}Z³½Mŧïeyp·uk³DsѨŸL“¶_œÅuèw»—€¡WqÜ]\\‘Ò§tƗcÕ¸ÕFÏǝĉăxŻČƟO‡ƒKÉġÿ×wg”÷IÅzCg†]m«ªGeçÃTC’«[‰t§{loWeC@ps_Bp‘­r‘„f_``Z|ei¡—oċMqow€¹DƝӛDYpûs•–‹Ykıǃ}s¥ç³[§ŸcYЧHK„«Qy‰]¢“wwö€¸ïx¼ņ¾Xv®ÇÀµRĠЋžHMž±cÏd„ƒǍũȅȷ±DSyúĝ£ŤĀàtÖÿï[îb\\}pĭÉI±Ñy…¿³x¯N‰o‰|¹H™ÏÛm‹júË~Tš•u˜ęjCöAwě¬R’đl¯ Ñb­‰ŇT†Ŀ_[Œ‘IčĄʿnM¦ğ\\É[T·™k¹œ©oĕ@A¾w•ya¥Y\\¥Âaz¯ãÁ¡k¥ne£Ûw†E©Êō¶˓uoj_Uƒ¡cF¹­[Wv“P©w—huÕyBF“ƒ`R‹qJUw\\i¡{jŸŸEPïÿ½fć…QÑÀQ{ž‚°‡fLԁ~wXg—ītêݾ–ĺ‘Hdˆ³fJd]‹HJ²…E€ƒoU¥†HhwQsƐ»Xmg±çve›]Dm͂PˆoCc¾‹_h”–høYrŊU¶eD°Č_N~øĹĚ·`z’]Äþp¼…äÌQŒv\\rCŒé¾TnkžŐڀÜa‡“¼ÝƆ̶Ûo…d…ĔňТJq’Pb ¾|JŒ¾fXŠƐîĨ_Z¯À}úƲ‹N_ĒĊ^„‘ĈaŐyp»CÇĕKŠšñL³ŠġMŒ²wrIÒŭxjb[œžn«øœ˜—æˆàƒ ^²­h¯Ú€ŐªÞ¸€Y²ĒVø}Ā^İ™´‚LŠÚm„¥ÀJÞ{JVŒųÞŃx×sxxƈē ģMř–ÚðòIf–Ċ“Œ\\Ʈ±ŒdʧĘD†vČ_Àæ~DŒċ´A®µ†¨ØLV¦êHÒ¤" ] ], "encodeOffsets": [[[113712, 34000]], [[115612, 30507]], [[113649, 34054]]] } }, { "type": "Feature", "id": "430000", "properties": { "id": "430000", "cp": [111.782279, 28.09409], "name": "湖南", "childNum": 3 }, "geometry": { "type": "MultiPolygon", "coordinates": [ ["@@—n„FTs"], ["@@ßÅÆá‰½ÔXr—†CO™“…ËR‘ïÿĩ­TooQyšÓ[‹ŅBE¬–ÎÓXa„į§Ã¸G °ITxp‰úxÚij¥Ïš–̾ŠedžÄ©ĸG…œàGh‚€M¤–Â_U}Ċ}¢pczfŠþg¤€”ÇòAV‘‹M"], [ "@@©K—ƒA·³CQ±Á«³BUŠƑ¹AŠtćOw™D]ŒJiØSm¯b£‘ylƒ›X…HËѱH•«–‘C^õľA–Å§¤É¥„ïyuǙuA¢^{ÌC´­¦ŷJ£^[†“ª¿‡ĕ~•Ƈ…•N… skóā‡¹¿€ï]ă~÷O§­@—Vm¡‹Qđ¦¢Ĥ{ºjԏŽŒª¥nf´•~ÕoŸž×Ûą‹MąıuZœmZcÒ IJβSÊDŽŶ¨ƚƒ’CÖŎªQؼrŭŽ­«}NÏürʬŒmjr€@ĘrTW ­SsdHzƓ^ÇÂyUi¯DÅYlŹu{hTœ}mĉ–¹¥ě‰Dÿë©ıÓ[Oº£ž“¥ót€ł¹MՄžƪƒ`Pš…Di–ÛUоÅ‌ìˆU’ñB“È£ýhe‰dy¡oċ€`pfmjP~‚kZa…ZsÐd°wj§ƒ@€Ĵ®w~^‚kÀÅKvNmX\\¨a“”сqvíó¿F„¤¡@ũÑVw}S@j}¾«pĂr–ªg àÀ²NJ¶¶Dô…K‚|^ª†Ž°LX¾ŴäPᜣEXd›”^¶›IJÞܓ~‘u¸ǔ˜Ž›MRhsR…e†`ÄofIÔ\\Ø  i”ćymnú¨cj ¢»–GČìƊÿШXeĈ¾Oð Fi ¢|[jVxrIQŒ„_E”zAN¦zLU`œcªx”OTu RLÄ¢dV„i`p˔vŎµªÉžF~ƒØ€d¢ºgİàw¸Áb[¦Zb¦–z½xBĖ@ªpº›šlS¸Ö\\Ĕ[N¥ˀmĎă’J\\‹ŀ`€…ňSڊĖÁĐiO“Ĝ«BxDõĚiv—ž–S™Ì}iùŒžÜnšÐºGŠ{Šp°M´w†ÀÒzJ²ò¨ oTçüöoÛÿñŽőФ‚ùTz²CȆȸǎۃƑÐc°dPÎŸğ˶[Ƚu¯½WM¡­Éž“’B·rížnZŸÒ `‡¨GA¾\\pē˜XhÆRC­üWGġu…T靧Ŏѝ©ò³I±³}_‘‹EÃħg®ęisÁPDmÅ{‰b[Rşs·€kPŸŽƥƒóRo”O‹ŸVŸ~]{g\\“êYƪ¦kÝbiċƵŠGZ»Ěõ…ó·³vŝž£ø@pyö_‹ëŽIkѵ‡bcѧy…×dY؎ªiþž¨ƒ[]f]Ņ©C}ÁN‡»hĻħƏ’ĩ" ] ], "encodeOffsets": [[[115640, 30489]], [[112543, 27312]], [[116690, 26230]]] } }, { "type": "Feature", "id": "440000", "properties": { "id": "440000", "cp": [113.280637, 23.125178], "name": "广东", "childNum": 24 }, "geometry": { "type": "MultiPolygon", "coordinates": [ ["@@QdˆAua"], ["@@ƒlxDLo"], ["@@sbhNLo"], ["@@Ă āŸ"], ["@@WltO[["], ["@@Krœ]S"], ["@@e„„I]y"], ["@@I|„Mym"], ["@@ƒÛ³LSŒž¼Y"], ["@@nvºB–ëui©`¾"], ["@@zdšÛ›Jw®"], ["@@†°…¯"], ["@@a yAª¸ËJIx،@€ĀHAmßV¡o•fu•o"], ["@@šs‰ŗÃÔėAƁ›ZšÄ ~°ČP‚‹äh"], ["@@‹¶Ý’Ì‚vmĞh­ı‡Q"], ["@@HœŠdSjĒ¢D}war…“u«ZqadYM"], ["@@elŒ\\LqqU"], ["@@~rMo\\"], ["@@f„^ƒC"], ["@@øPªoj÷ÍÝħXČx”°Q¨ıXNv"], ["@@gÇƳˆŽˆ”oˆŠˆ[~tly"], ["@@E–ÆC¿‘"], ["@@OŽP"], [ "@@w‹†đóg‰™ĝ—[³‹¡VÙæÅöM̳¹pÁaËýý©D©Ü“JŹƕģGą¤{Ùū…ǘO²«BƱéA—Ò‰ĥ‡¡«BhlmtÃPµyU¯uc“d·w_bŝcīímGOŽ|KP’ȏ‡ŹãŝIŕŭŕ@Óoo¿ē‹±ß}Ž…ŭ‚ŸIJWÈCőâUâǙI›ğʼn©I›ijEׅÁ”³Aó›wXJþ±ÌŒÜӔĨ£L]ĈÙƺZǾĆĖMĸĤfŒÎĵl•ŨnȈ‘ĐtF”Š–FĤ–‚êk¶œ^k°f¶gŠŽœ}®Fa˜f`vXŲxl˜„¦–ÔÁ²¬ÐŸ¦pqÊ̲ˆi€XŸØRDÎ}†Ä@ZĠ’s„x®AR~®ETtĄZ†–ƈfŠŠHâÒÐA†µ\\S¸„^wĖkRzŠalŽŜ|E¨ÈNĀňZTŒ’pBh£\\ŒĎƀuXĖtKL–¶G|Ž»ĺEļĞ~ÜĢÛĊrˆO˜Ùîvd]nˆ¬VœÊĜ°R֟pM††–‚ƂªFbwžEÀˆ˜©Œž\\…¤]ŸI®¥D³|ˎ]CöAŤ¦…æ’´¥¸Lv¼€•¢ĽBaô–F~—š®²GÌҐEY„„œzk¤’°ahlV՞I^‹šCxĈPŽsB‰ƒºV‰¸@¾ªR²ĨN]´_eavSi‡vc•}p}Đ¼ƌkJœÚe thœ†_¸ ºx±ò_xN›Ë‹²‘@ƒă¡ßH©Ùñ}wkNÕ¹ÇO½¿£ĕ]ly_WìIžÇª`ŠuTÅxYĒÖ¼k֞’µ‚MžjJÚwn\\h‘œĒv]îh|’È›Ƅøègž¸Ķß ĉĈWb¹ƀdéƌNTtP[ŠöSvrCZžžaGuœbo´ŖÒÇА~¡zCI…özx¢„Pn‹•‰Èñ @ŒĥÒ¦†]ƞŠV}³ăĔñiiÄÓVépKG½Ä‘ÓávYo–C·sit‹iaÀy„ŧΡÈYDÑům}‰ý|m[węõĉZÅxUO}÷N¹³ĉo_qtă“qwµŁYلǝŕ¹tïÛUïmRCº…ˆĭ|µ›ÕÊK™½R‘ē ó]‘–GªęAx–»HO£|ām‡¡diď×YïYWªʼnOeÚtĐ«zđ¹T…ā‡úE™á²\\‹ķÍ}jYàÙÆſ¿Çdğ·ùTßÇţʄ¡XgWÀLJğ·¿ÃˆOj YÇ÷Qě‹i" ] ], "encodeOffsets": [ [[117381, 22988]], [[116552, 22934]], [[116790, 22617]], [[116973, 22545]], [[116444, 22536]], [[116931, 22515]], [[116496, 22490]], [[116453, 22449]], [[113301, 21439]], [[118726, 21604]], [[118709, 21486]], [[113210, 20816]], [[115482, 22082]], [[113171, 21585]], [[113199, 21590]], [[115232, 22102]], [[115739, 22373]], [[115134, 22184]], [[113056, 21175]], [[119573, 21271]], [[119957, 24020]], [[115859, 22356]], [[116561, 22649]], [[116285, 22746]] ] } }, { "type": "Feature", "id": "450000", "properties": { "id": "450000", "cp": [108.320004, 22.82402], "name": "广西", "childNum": 2 }, "geometry": { "type": "MultiPolygon", "coordinates": [ ["@@H– TQ§•A"], [ "@@ĨʪƒLƒƊDÎĹĐCǦė¸zÚGn£¾›rªŀÜt¬@֛ڈSx~øOŒ˜ŶÐÂæȠ\\„ÈÜObĖw^oބLf¬°bI lTØB̈F£Ć¹gñĤaY“t¿¤VSñœK¸¤nM†¼‚JE±„½¸šŠño‹ÜCƆæĪ^ŠĚQÖ¦^‡ˆˆf´Q†üÜʝz¯šlzUĺš@쇀p¶n]sxtx¶@„~ÒĂJb©gk‚{°‚~c°`ԙ¬rV\\“la¼¤ôá`¯¹LC†ÆbŒxEræO‚v[H­˜„[~|aB£ÖsºdAĐzNÂðsŽÞƔ…Ĥªbƒ–ab`ho¡³F«èVloޤ™ÔRzpp®SŽĪº¨ÖƒºN…ij„d`’a”¦¤F³ºDÎńĀìŠCžĜº¦Ċ•~nS›|gźvZkCÆj°zVÈÁƔ]LÊFZg…čP­kini«‹qǀcz͔Y®¬Ů»qR×ō©DՄ‘§ƙǃŵTÉĩ±ŸıdÑnYY›IJvNĆÌØÜ Öp–}e³¦m‹©iÓ|¹Ÿħņ›|ª¦QF¢Â¬ʖovg¿em‡^ucà÷gՎuŒíÙćĝ}FϼĹ{µHK•sLSđƃr‹č¤[Ag‘oS‹ŇYMÿ§Ç{Fśbky‰lQxĕƒ]T·¶[B…ÑÏGáşşƇe€…•ăYSs­FQ}­Bƒw‘tYğÃ@~…C̀Q ×W‡j˱rÉ¥oÏ ±«ÓÂ¥•ƒ€k—ŽwWűŒmcih³K›~‰µh¯e]lµ›él•E쉕E“ďs‡’mǖŧē`ãògK_ÛsUʝ“ćğ¶hŒöŒO¤Ǜn³Žc‘`¡y‹¦C‘ez€YŠwa™–‘[ďĵűMę§]X˜Î_‚훘Û]é’ÛUćİÕBƣ±…dƒy¹T^džûÅÑŦ·‡PĻþÙ`K€¦˜…¢ÍeœĥR¿Œ³£[~Œäu¼dl‰t‚†W¸oRM¢ď\\zœ}Æzdvň–{ÎXF¶°Â_„ÒÂÏL©Ö•TmuŸ¼ãl‰›īkiqéfA„·Êµ\\őDc¥ÝF“y›Ôć˜c€űH_hL܋êĺШc}rn`½„Ì@¸¶ªVLŒŠhŒ‹\\•Ţĺk~ŽĠið°|gŒtTĭĸ^x‘vK˜VGréAé‘bUu›MJ‰VÃO¡…qĂXËS‰ģãlýàŸ_ju‡YÛÒB†œG^˜é֊¶§ŽƒEG”ÅzěƒƯ¤Ek‡N[kdåucé¬dnYpAyČ{`]þ¯T’bÜÈk‚¡Ġ•vŒàh„ÂƄ¢Jî¶²" ] ], "encodeOffsets": [[[111707, 21520]], [[107619, 25527]]] } }, { "type": "Feature", "id": "460000", "properties": { "id": "460000", "cp": [109.83119, 19.031971], "name": "海南", "childNum": 1 }, "geometry": { "type": "Polygon", "coordinates": [ "@@š¦Ŝil¢”XƦ‘ƞò–ïè§ŞCêɕrŧůÇąĻõ™·ĉ³œ̅kÇm@ċȧƒŧĥ‰Ľʉ­ƅſ“ȓÒ˦ŝE}ºƑ[ÍĜȋ gÎfǐÏĤ¨êƺ\\Ɔ¸ĠĎvʄȀœÐ¾jNðĀÒRŒšZdž™zÐŘΰH¨Ƣb²_Ġ " ], "encodeOffsets": [[112750, 20508]] } }, { "type": "Feature", "id": "510000", "properties": { "id": "510000", "cp": [104.065735, 30.659462], "name": "四川", "childNum": 2 }, "geometry": { "type": "MultiPolygon", "coordinates": [ ["@@LqKr"], [ "@@Š[ĻéV£ž_ţġñpG •réÏ·~ąSfy×͂·ºſƽiÍıƣıĻmHH}siaX@iǰÁÃ×t«ƒ­Tƒ¤J–JJŒyJ•ÈŠ`Ohߦ¡uËhIyCjmÿw…ZG……Ti‹SˆsO‰žB²ŸfNmsPaˆ{M{ŠõE‘^Hj}gYpaeuž¯‘oáwHjÁ½M¡pM“–uå‡mni{fk”\\oƒÎqCw†EZ¼K›ĝŠƒAy{m÷L‡wO×SimRI¯rK™õBS«sFe‡]fµ¢óY_ÆPRcue°Cbo׌bd£ŌIHgtrnyPt¦foaXďx›lBowz‹_{ÊéWiêE„GhܸºuFĈIxf®Ž•Y½ĀǙ]¤EyŸF²ċ’w¸¿@g¢§RGv»–áŸW`ÃĵJwi]t¥wO­½a[׈]`Ãi­üL€¦LabbTÀå’c}Íh™Æhˆ‹®BH€î|Ék­¤S†y£„ia©taį·Ɖ`ō¥Uh“O…ƒĝLk}©Fos‰´›Jm„µlŁu—…ø–nÑJWΪ–YÀïAetTžŅ‚ӍG™Ë«bo‰{ıwodƟ½ƒžOġܑµxàNÖ¾P²§HKv¾–]|•B‡ÆåoZ`¡Ø`ÀmºĠ~ÌЧnDž¿¤]wğ@sƒ‰rğu‰~‘Io”[é±¹ ¿žſđӉ@q‹gˆ¹zƱřaí°KtǤV»Ã[ĩǭƑ^ÇÓ@ỗs›Zϕ‹œÅĭ€Ƌ•ěpwDóÖሯneQˌq·•GCœýS]xŸ·ý‹q³•O՜Œ¶Qzßti{ř‰áÍÇWŝŭñzÇW‹pç¿JŒ™‚Xœĩè½cŒF–ÂLiVjx}\\N†ŇĖ¥Ge–“JA¼ÄHfÈu~¸Æ«dE³ÉMA|b˜Ò…˜ćhG¬CM‚õŠ„ƤąAvƒüV€éŀ‰_V̳ĐwQj´·ZeÈÁ¨X´Æ¡Qu·»Ÿ“˜ÕZ³ġqDo‰y`L¬gdp°şŠp¦ėìÅĮZްIä”h‚‘ˆzŠĵœf²å ›ĚрKp‹IN|‹„Ñz]ń……·FU×é»R³™MƒÉ»GM«€ki€™ér™}Ã`¹ăÞmȝnÁîRǀ³ĜoİzŔwǶVÚ£À]ɜ»ĆlƂ²Ġ…þTº·àUȞÏʦ¶†I’«dĽĢdĬ¿–»Ĕ׊h\\c¬†ä²GêëĤł¥ÀǿżÃÆMº}BÕĢyFVvw–ˆxBèĻĒ©Ĉ“tCĢɽŠȣ¦āæ·HĽî“ôNԓ~^¤Ɗœu„œ^s¼{TA¼ø°¢İªDè¾Ň¶ÝJ‘®Z´ğ~Sn|ªWÚ©òzPOȸ‚bð¢|‹øĞŠŒœŒQìÛÐ@Ğ™ǎRS¤Á§d…i“´ezÝúØã]Hq„kIŸþËQǦÃsǤ[E¬ÉŪÍxXƒ·ÖƁİlƞ¹ª¹|XÊwn‘ÆƄmÀêErĒtD®ċæcQƒ”E®³^ĭ¥©l}äQto˜ŖÜqƎkµ–„ªÔĻĴ¡@Ċ°B²Èw^^RsºT£ڿœQP‘JvÄz„^Đ¹Æ¯fLà´GC²‘dt˜­ĀRt¼¤ĦOðğfÔðDŨŁĞƘïžPȆ®âbMüÀXZ ¸£@Ś›»»QÉ­™]d“sÖ×_͖_ÌêŮPrĔĐÕGĂeZÜîĘqBhtO ¤tE[h|Y‹Ô‚ZśÎs´xº±UŒ’ñˆt|O’ĩĠºNbgþŠJy^dÂY Į„]Řz¦gC‚³€R`Šz’¢AjŒ¸CL„¤RÆ»@­Ŏk\\Ç´£YW}z@Z}‰Ã¶“oû¶]´^N‡Ò}èN‚ª–P˜Íy¹`S°´†ATe€VamdUĐwʄvĮÕ\\ƒu‹Æŗ¨Yp¹àZÂm™Wh{á„}WØǍ•Éüw™ga§áCNęÎ[ĀÕĪgÖɪX˜øx¬½Ů¦¦[€—„NΆL€ÜUÖ´òrÙŠxR^–†J˜k„ijnDX{Uƒ~ET{ļº¦PZc”jF²Ė@Žp˜g€ˆ¨“B{ƒu¨ŦyhoÚD®¯¢˜ WòàFΤ¨GDäz¦kŮPœġq˚¥À]€Ÿ˜eŽâÚ´ªKxī„Pˆ—Ö|æ[xäJÞĥ‚s’NÖ½ž€I†¬nĨY´®Ð—ƐŠ€mD™ŝuäđđEb…e’e_™v¡}ìęNJē}q”É埁T¯µRs¡M@}ůa†a­¯wvƉåZwž\\Z{åû^›" ] ], "encodeOffsets": [[[108815, 30935]], [[110617, 31811]]] } }, { "type": "Feature", "id": "520000", "properties": { "id": "520000", "cp": [106.713478, 26.578343], "name": "贵州", "childNum": 3 }, "geometry": { "type": "MultiPolygon", "coordinates": [ ["@@†G\\†lY£‘in"], ["@@q‚|ˆ‚mc¯tχVSÎ"], [ "@@hÑ£Is‡NgßH†›HªķÃh_¹ƒ¡ĝħń¦uيùŽgS¯JHŸ|sÝÅtÁïyMDč»eÕtA¤{b\\}—ƒG®u\\åPFq‹wÅaD…žK°ºâ_£ùbµ”mÁ‹ÛœĹM[q|hlaªāI}тƒµ@swtwm^oµˆD鼊yV™ky°ÉžûÛR…³‚‡eˆ‡¥]RՋěħ[ƅåÛDpŒ”J„iV™™‰ÂF²I…»mN·£›LbÒYb—WsÀbŽ™pki™TZĄă¶HŒq`……ĥ_JŸ¯ae«ƒKpÝx]aĕÛPƒÇȟ[ÁåŵÏő—÷Pw}‡TœÙ@Õs«ĿÛq©½œm¤ÙH·yǥĘĉBµĨÕnđ]K„©„œá‹ŸG纍§Õßg‡ǗĦTèƤƺ{¶ÉHÎd¾ŚÊ·OÐjXWrãLyzÉAL¾ę¢bĶėy_qMĔąro¼hĊžw¶øV¤w”²Ĉ]ʚKx|`ź¦ÂÈdr„cȁbe¸›`I¼čTF´¼Óýȃr¹ÍJ©k_șl³´_pН`oÒh޶pa‚^ÓĔ}D»^Xyœ`d˜[Kv…JPhèhCrĂĚÂ^Êƌ wˆZL­Ġ£šÁbrzOIl’MM”ĪŐžËr×ÎeŦŽtw|Œ¢mKjSǘňĂStÎŦEtqFT†¾†E쬬ôxÌO¢Ÿ KгŀºäY†„”PVgŎ¦Ŋm޼VZwVlŒ„z¤…ž£Tl®ctĽÚó{G­A‡ŒÇgeš~Αd¿æaSba¥KKûj®_ć^\\ؾbP®¦x^sxjĶI_Ä X‚⼕Hu¨Qh¡À@Ëô}ޱžGNìĎlT¸ˆ…`V~R°tbÕĊ`¸úÛtπFDu€[ƒMfqGH·¥yA‰ztMFe|R‚_Gk†ChZeÚ°to˜v`x‹b„ŒDnÐ{E}šZ˜è€x—†NEފREn˜[Pv@{~rĆAB§‚EO¿|UZ~ì„Uf¨J²ĂÝÆ€‚sª–B`„s¶œfvö¦ŠÕ~dÔq¨¸º»uù[[§´sb¤¢zþFœ¢Æ…Àhˆ™ÂˆW\\ıŽËI݊o±ĭŠ£þˆÊs}¡R]ŒěƒD‚g´VG¢‚j±®è†ºÃmpU[Á›‘Œëº°r›ÜbNu¸}Žº¼‡`ni”ºÔXĄ¤¼Ôdaµ€Á_À…†ftQQgœR—‘·Ǔ’v”}Ýלĵ]µœ“Wc¤F²›OĩųãW½¯K‚©…]€{†LóµCIµ±Mß¿hŸ•©āq¬o‚½ž~@i~TUxŪÒ¢@ƒ£ÀEîôruń‚”“‚b[§nWuMÆLl¿]x}ij­€½" ] ], "encodeOffsets": [[[112158, 27383]], [[112105, 27474]], [[112095, 27476]]] } }, { "type": "Feature", "id": "530000", "properties": { "id": "530000", "cp": [101.512251, 24.740609], "name": "云南", "childNum": 1 }, "geometry": { "type": "Polygon", "coordinates": [ "@@[„ùx½}ÑRH‘YīĺûsÍn‘iEoã½Ya²ė{c¬ĝg•ĂsA•ØÅwď‚õzFjw}—«Dx¿}UũlŸê™@•HÅ­F‰¨ÇoJ´Ónũuą¡Ã¢pÒŌ“Ø TF²‚xa²ËX€‚cʋlHîAßËŁkŻƑŷÉ©h™W­æßU‡“Ës¡¦}•teèÆ¶StǀÇ}Fd£j‹ĈZĆÆ‹¤T‚č\\Dƒ}O÷š£Uˆ§~ŃG™‚åŃDĝ¸œTsd¶¶Bªš¤u¢ŌĎo~t¾ÍŶÒtD¦Ú„iôö‰€z›ØX²ghįh½Û±¯€ÿm·zR¦Ɵ`ªŊÃh¢rOԍ´£Ym¼èêf¯ŪĽn„†cÚbŒw\\zlvWžªâˆ ¦g–mĿBş£¢ƹřbĥkǫßeeZkÙIKueT»sVesb‘aĕ  ¶®dNœĄÄpªyސ¼—„³BE˜®l‡ŽGœŭCœǶwêżĔÂe„pÍÀQƞpC„–¼ŲÈ­AÎô¶R„ä’Q^Øu¬°š_Èôc´¹ò¨P΢hlϦ´Ħ“Æ´sâDŽŲPnÊD^¯°’Upv†}®BP̪–jǬx–Söwlfòªv€qĸ|`H€­viļ€ndĜ­Ćhň•‚em·FyށqóžSᝑ³X_ĞçêtryvL¤§z„¦c¦¥jnŞk˜ˆlD¤øz½ĜàžĂŧMÅ|áƆàÊcðÂF܎‚áŢ¥\\\\º™İøÒÐJĴ‡„îD¦zK²ǏÎEh~’CD­hMn^ÌöÄ©ČZÀžaü„fɭyœpį´ěFűk]Ôě¢qlÅĆÙa¶~Äqššê€ljN¬¼H„ÊšNQ´ê¼VظE††^ŃÒyŒƒM{ŒJLoÒœęæŸe±Ķ›y‰’‡gã“¯JYÆĭĘëo¥Š‰o¯hcK«z_pŠrC´ĢÖY”—¼ v¸¢RŽÅW³Â§fǸYi³xR´ďUˊ`êĿU„û€uĆBƒƣö‰N€DH«Ĉg†——Ñ‚aB{ÊNF´¬c·Åv}eÇÃGB»”If•¦HňĕM…~[iwjUÁKE•Ž‹¾dĪçW›šI‹èÀŒoÈXòyŞŮÈXâÎŚŠj|àsRy‹µÖ›–Pr´þŒ ¸^wþTDŔ–Hr¸‹žRÌmf‡żÕâCôox–ĜƌÆĮŒ›Ð–œY˜tâŦÔ@]ÈǮƒ\\μģUsȯLbîƲŚºyh‡rŒŠ@ĒԝƀŸÀ²º\\êp“’JŠ}ĠvŠqt„Ġ@^xÀ£È†¨mËÏğ}n¹_¿¢×Y_æpˆÅ–A^{½•Lu¨GO±Õ½ßM¶w’ÁĢۂP‚›Ƣ¼pcIJxŠ|ap̬HšÐŒŊSfsðBZ¿©“XÏÒK•k†÷Eû¿‰S…rEFsÕūk”óVǥʼniTL‚¡n{‹uxţÏh™ôŝ¬ğōN“‘NJkyPaq™Âğ¤K®‡YŸxÉƋÁ]āęDqçgOg†ILu—\\_gz—]W¼ž~CÔē]bµogpў_oď`´³Țkl`IªºÎȄqÔþž»E³ĎSJ»œ_f·‚adÇqƒÇc¥Á_Źw{™L^ɱćx“U£µ÷xgĉp»ĆqNē`rĘzaĵĚ¡K½ÊBzyäKXqiWPÏɸ½řÍcÊG|µƕƣG˛÷Ÿk°_^ý|_zċBZocmø¯hhcæ\\lˆMFlư£Ĝ„ÆyH“„F¨‰µêÕ]—›HA…àӄ^it `þßäkŠĤÎT~Wlÿ¨„ÔPzUC–NVv [jâôDôď[}ž‰z¿–msSh‹¯{jïğl}šĹ[–őŒ‰gK‹©U·µË@¾ƒm_~q¡f¹…ÅË^»‘f³ø}Q•„¡Ö˳gͱ^ǁ…\\ëÃA_—¿bW›Ï[¶ƛ鏝£F{īZgm@|kHǭƁć¦UĔťƒ×ë}ǝƒeďºȡȘÏíBə£āĘPªij¶“ʼnÿ‡y©n‰ď£G¹¡I›Š±LÉĺÑdĉ܇W¥˜‰}g˜Á†{aqÃ¥aŠıęÏZ—ï`" ], "encodeOffsets": [[104636, 22969]] } }, { "type": "Feature", "id": "540000", "properties": { "id": "540000", "cp": [89.132212, 30.860361], "name": "西藏", "childNum": 1 }, "geometry": { "type": "Polygon", "coordinates": [ "@@hžľxŽŖ‰xƒÒVކºÅâAĪÝȆµę¯Ňa±r_w~uSÕň‘qOj]ɄQ…£Z……UDûoY’»©M[‹L¼qãË{V͕çWViŽ]ë©Ä÷àyƛh›ÚU°ŒŒa”d„cQƒ~Mx¥™cc¡ÙaSyF—ցk­ŒuRýq¿Ôµ•QĽ³aG{¿FµëªéĜÿª@¬·–K‰·àariĕĀ«V»Ŷ™Ĵū˜gèLǴŇƶaf‹tŒèBŚ£^Šâ†ǐÝ®–šM¦ÁǞÿ¬LhŸŽJ¾óƾƺcxw‹f]Y…´ƒ¦|œQLn°aœdĊ…œ\\¨o’œǀÍŎœ´ĩĀd`tÊQŞŕ|‚¨C^©œĈ¦„¦ÎJĊ{ŽëĎjª²rЉšl`¼Ą[t|¦St辉PŒÜK¸€d˜Ƅı]s¤—î_v¹ÎVòŦj˜£Əsc—¬_Ğ´|٘¦Avަw`ăaÝaa­¢e¤ı²©ªSªšÈMĄwžÉØŔì@T‘¤—Ę™\\õª@”þo´­xA s”ÂtŎKzó´ÇĊµ¢rž^nĊ­Æ¬×üGž¢‚³ {âĊ]š™G‚~bÀgVjzlhǶf€žOšfdЉªB]pj„•TO–tĊ‚n¤}®¦ƒČ¥d¢¼»ddš”Y¼Žt—¢eȤJ¤}Ǿ¡°§¤AГlc@ĝ”sªćļđAç‡wx•UuzEÖġ~AN¹ÄÅȀݦ¿ģŁéì±H…ãd«g[؉¼ēÀ•cīľġ¬cJ‘µ…ÐʥVȝ¸ßS¹†ý±ğkƁ¼ą^ɛ¤Ûÿ‰b[}¬ōõÃ]ËNm®g@•Bg}ÍF±ǐyL¥íCˆƒIij€Ï÷њį[¹¦[⚍EÛïÁÉdƅß{âNÆāŨߝ¾ě÷yC£‡k­´ÓH@¹†TZ¥¢įƒ·ÌAЧ®—Zc…v½ŸZ­¹|ŕWZqgW“|ieZÅYVӁqdq•bc²R@†c‡¥Rã»Ge†ŸeƃīQ•}J[ғK…¬Ə|o’ėjġĠÑN¡ð¯EBčnwôɍėªƒ²•CλŹġǝʅįĭạ̃ūȹ]ΓͧgšsgȽóϧµǛ†ęgſ¶ҍć`ĘąŌJޚä¤rÅň¥ÖÁUětęuůÞiĊÄÀ\\Æs¦ÓRb|Â^řÌkÄŷ¶½÷‡f±iMݑ›‰@ĥ°G¬ÃM¥n£Øą‚ğ¯ß”§aëbéüÑOčœk£{\\‘eµª×M‘šÉfm«Ƒ{Å׃Gŏǩãy³©WÑăû‚··‘Q—òı}¯ã‰I•éÕÂZ¨īès¶ZÈsŽæĔTŘvŽgÌsN@îá¾ó@‰˜ÙwU±ÉT廣TđŸWxq¹Zo‘b‹s[׌¯cĩv‡Œėŧ³BM|¹k‰ªħ—¥TzNYnݍßpęrñĠĉRS~½ŠěVVе‚õ‡«ŒM££µB•ĉ¥áºae~³AuĐh`Ü³ç@BۘïĿa©|z²Ý¼D”£à貋ŸƒIƒû›I ā€óK¥}rÝ_Á´éMaň¨€~ªSĈ½Ž½KÙóĿeƃÆBŽ·¬ën×W|Uº}LJrƳ˜lŒµ`bÔ`QˆˆÐÓ@s¬ñIŒÍ@ûws¡åQÑßÁ`ŋĴ{Ī“T•ÚÅTSij‚‹Yo|Ç[ǾµMW¢ĭiÕØ¿@˜šMh…pÕ]j†éò¿OƇĆƇp€êĉâlØw–ěsˆǩ‚ĵ¸c…bU¹ř¨WavquSMzeo_^gsÏ·¥Ó@~¯¿RiīB™Š\\”qTGªÇĜçPoŠÿfñòą¦óQīÈáP•œābß{ƒZŗĸIæÅ„hnszÁCËìñšÏ·ąĚÝUm®ó­L·ăU›Èíoù´Êj°ŁŤ_uµ^‘°Œìǖ@tĶĒ¡Æ‡M³Ģ«˜İĨÅ®ğ†RŽāð“ggheÆ¢z‚Ê©Ô\\°ÝĎz~ź¤Pn–MĪÖB£Ÿk™n鄧żćŠ˜ĆK„ǰ¼L¶è‰âz¨u¦¥LDĘz¬ýÎmĘd¾ß”Fz“hg²™Fy¦ĝ¤ċņbΛ@y‚Ąæm°NĮZRÖíŽJ²öLĸÒ¨Y®ƌÐV‰à˜tt_ڀÂyĠzž]Ţh€zĎ{†ĢX”ˆc|šÐqŽšfO¢¤ög‚ÌHNŽ„PKŖœŽ˜Uú´xx[xˆvĐCûŠìÖT¬¸^}Ìsòd´_އKgžLĴ…ÀBon|H@–Êx˜—¦BpŰˆŌ¿fµƌA¾zLjRxжF”œkĄźRzŀˆ~¶[”´Hnª–VƞuĒ­È¨ƎcƽÌm¸ÁÈM¦x͊ëÀxdžB’šú^´W†£–d„kɾĬpœw‚˂ØɦļĬIŚœÊ•n›Ŕa¸™~J°î”lɌxĤÊÈðhÌ®‚g˜T´øŽàCˆŽÀ^ªerrƘdž¢İP|Ė ŸWœªĦ^¶´ÂL„aT±üWƜ˜ǀRšŶUńšĖ[QhlLüA†‹Ü\\†qR›Ą©" ], "encodeOffsets": [[90849, 37210]] } }, { "type": "Feature", "id": "610000", "properties": { "id": "610000", "cp": [108.948024, 34.263161], "name": "陕西", "childNum": 1 }, "geometry": { "type": "Polygon", "coordinates": [ "@@˜p¢—ȮµšûG™Ħ}Ħšðǚ¶òƄ€jɂz°{ºØkÈęâ¦jª‚Bg‚\\œċ°s¬Ž’]jžú ‚E”Ȍdž¬s„t‡”RˆÆdĠݎwܔ¸ôW¾ƮłÒ_{’Ìšû¼„jº¹¢GǪÒ¯ĘƒZ`ºŊƒecņąš~BÂgzpâēòYǠȰÌTΨÂWœ|fcŸă§uF—Œ@NŸ¢XLƒŠRMº[ğȣſï|¥J™kc`sʼnǷ’Y¹‹W@µ÷K…ãï³ÛIcñ·VȋڍÒķø©—þ¥ƒy‚ÓŸğęmWµÎumZyOŅƟĥÓ~sÑL¤µaŅY¦ocyZ{‰y c]{ŒTa©ƒ`U_Ěē£ωÊƍKù’K¶ȱÝƷ§{û»ÅÁȹÍéuij|¹cÑd‘ŠìUYƒŽO‘uF–ÕÈYvÁCqӃT•Ǣí§·S¹NgŠV¬ë÷Át‡°Dد’C´ʼnƒópģ}„ċcE˅FŸŸéGU¥×K…§­¶³B‹Č}C¿åċ`wġB·¤őcƭ²ő[Å^axwQO…ÿEËߌ•ĤNĔŸwƇˆÄŠńwĪ­Šo[„_KÓª³“ÙnK‰Çƒěœÿ]ď€ă_d©·©Ýŏ°Ù®g]±„Ÿ‡ß˜å›—¬÷m\\›iaǑkěX{¢|ZKlçhLt€Ňîŵ€œè[€É@ƉĄEœ‡tƇÏ˜³­ħZ«mJ…›×¾‘MtÝĦ£IwÄå\\Õ{‡˜ƒOwĬ©LÙ³ÙgBƕŀr̛ĢŭO¥lãyC§HÍ£ßEñŸX¡—­°ÙCgpťz‘ˆb`wI„vA|§”‡—hoĕ@E±“iYd¥OϹS|}F@¾oAO²{tfžÜ—¢Fǂ҈W²°BĤh^Wx{@„¬‚­F¸¡„ķn£P|ŸªĴ@^ĠĈæb–Ôc¶l˜Yi…–^Mi˜cϰÂ[ä€vï¶gv@À“Ĭ·lJ¸sn|¼u~a]’ÆÈtŌºJp’ƒþ£KKf~ЦUbyäIšĺãn‡Ô¿^­žŵMT–hĠܤko¼Ŏìąǜh`[tŒRd²IJ_œXPrɲ‰l‘‚XžiL§àƒ–¹ŽH˜°Ȧqº®QC—bA†„ŌJ¸ĕÚ³ĺ§ `d¨YjžiZvRĺ±öVKkjGȊĐePОZmļKÀ€‚[ŠŽ`ösìh†ïÎoĬdtKÞ{¬èÒÒBŒÔpIJÇĬJŊ¦±J«ˆY§‹@·pH€µàåVKe›pW†ftsAÅqC·¬ko«pHÆuK@oŸHĆۄķhx“e‘n›S³àǍrqƶRbzy€¸ËАl›¼EºpĤ¼Œx¼½~Ğ’”à@†ÚüdK^ˆmÌSj" ], "encodeOffsets": [[110234, 38774]] } }, { "type": "Feature", "id": "620000", "properties": { "id": "620000", "cp": [103.823557, 36.058039], "name": "甘肃", "childNum": 2 }, "geometry": { "type": "MultiPolygon", "coordinates": [ ["@@VuUv"], [ "@@ũ‹EĠtt~nkh`Q‰¦ÅÄÜdw˜Ab×ĠąJˆ¤DüègĺqBqœj°lI¡ĨÒ¤úSHbš‡ŠjΑBаaZˆ¢KJŽ’O[|A£žDx}Nì•HUnrk„ kp€¼Y kMJn[aG‚áÚÏ[½rc†}aQxOgsPMnUs‡nc‹Z…ž–sKúvA›t„Þġ’£®ĀYKdnFwš¢JE°”Latf`¼h¬we|€Æ‡šbj}GA€·~WŽ”—`†¢MC¤tL©IJ°qdf”O‚“bÞĬ¹ttu`^ZúE`Œ[@„Æsîz®¡’C„ƳƜG²“R‘¢R’m”fŽwĸg܃‚ą G@pzJM½mŠhVy¸uÈÔO±¨{LfæU¶ßGĂq\\ª¬‡²I‚¥IʼnÈīoı‹ÓÑAçÑ|«LÝcspīðÍg…të_õ‰\\ĉñLYnĝg’ŸRǡÁiHLlõUĹ²uQjYi§Z_c¨Ÿ´ĹĖÙ·ŋI…ƒaBD˜­R¹ȥr—¯G•ºß„K¨jWk’ɱŠOq›Wij\\a­‹Q\\sg_ĆǛōëp»£lğۀgS•ŶN®À]ˆÓäm™ĹãJaz¥V}‰Le¤L„ýo‘¹IsŋÅÇ^‘Žbz…³tmEÁ´aйcčecÇN•ĊãÁ\\蝗dNj•]j†—ZµkÓda•ćå]ğij@ ©O{¤ĸm¢ƒE·®ƒ«|@Xwg]A챝‡XǁÑdzªc›wQÚŝñsÕ³ÛV_ýƒ˜¥\\ů¥©¾÷w—Ž©WÕÊĩhÿÖÁRo¸V¬âDb¨šhûx–Ê×nj~Zâƒg|šXÁnßYoº§ZÅŘvŒ[„ĭÖʃuďxcVbnUSf…B¯³_Tzº—ΕO©çMÑ~Mˆ³]µ^püµ”ŠÄY~y@X~¤Z³€[Èōl@®Å¼£QKƒ·Di‹¡By‘ÿ‰Q_´D¥hŗyƒ^ŸĭÁZ]cIzý‰ah¹MĪğP‘s{ò‡‹‘²Vw¹t³Ŝˁ[ŽÑ}X\\gsFŸ£sPAgěp×ëfYHāďÖqēŭOÏë“dLü•\\iŒ”t^c®šRʺ¶—¢H°mˆ‘rYŸ£BŸ¹čIoľu¶uI]vģSQ{ƒUŻ”Å}QÂ|̋°ƅ¤ĩŪU ęĄžÌZҞ\\v˜²PĔ»ƢNHƒĂyAmƂwVmž`”]ȏb•”H`‰Ì¢²ILvĜ—H®¤Dlt_„¢JJÄämèÔDëþgºƫ™”aʎÌrêYi~ ÎݤNpÀA¾Ĕ¼b…ð÷’Žˆ‡®‚”üs”zMzÖĖQdȨý†v§Tè|ªH’þa¸|šÐ ƒwKĢx¦ivr^ÿ ¸l öæfƟĴ·PJv}n\\h¹¶v†·À|\\ƁĚN´Ĝ€çèÁz]ġ¤²¨QÒŨTIl‡ªťØ}¼˗ƦvÄùØE‹’«Fï˛Iq”ōŒTvāÜŏ‚íÛߜÛV—j³âwGăÂíNOŠˆŠPìyV³ʼnĖýZso§HіiYw[߆\\X¦¥c]ÔƩÜ·«j‡ÐqvÁ¦m^ċ±R™¦΋ƈťĚgÀ»IïĨʗƮްƝ˜ĻþÍAƉſ±tÍEÕÞāNU͗¡\\ſčåÒʻĘm ƭÌŹöʥ’ëQ¤µ­ÇcƕªoIýˆ‰Iɐ_mkl³ă‰Ɠ¦j—¡Yz•Ňi–}Msßõ–īʋ —}ƒÁVmŸ_[n}eı­Uĥ¼‘ª•I{ΧDӜƻėoj‘qYhĹT©oūĶ£]ďxĩ‹ǑMĝ‰q`B´ƃ˺Ч—ç~™²ņj@”¥@đ´ί}ĥtPńǾV¬ufӃÉC‹tÓ̻‰…¹£G³€]ƖƾŎĪŪĘ̖¨ʈĢƂlɘ۪üºňUðǜȢƢż̌ȦǼ‚ĤŊɲĖ­Kq´ï¦—ºĒDzņɾªǀÞĈĂD†½ĄĎÌŗĞrôñnŽœN¼â¾ʄľԆ|DŽŽ֦ज़ȗlj̘̭ɺƅêgV̍ʆĠ·ÌĊv|ýĖÕWĊǎÞ´õ¼cÒÒBĢ͢UĜð͒s¨ňƃLĉÕÝ@ɛƯ÷¿Ľ­ĹeȏijëCȚDŲyê×Ŗyò¯ļcÂßY…tÁƤyAã˾J@ǝrý‹‰@¤…rz¸oP¹ɐÚyᐇHŸĀ[Jw…cVeȴϜ»ÈŽĖ}ƒŰŐèȭǢόĀƪÈŶë;Ñ̆ȤМľĮEŔ—ĹŊũ~ËUă{ŸĻƹɁύȩþĽvĽƓÉ@ē„ĽɲßǐƫʾǗĒpäWÐxnsÀ^ƆwW©¦cÅ¡Ji§vúF¶Ž¨c~c¼īŒeXǚ‹\\đ¾JŽwÀďksãA‹fÕ¦L}wa‚o”Z’‹D½†Ml«]eÒÅaɲáo½FõÛ]ĻÒ¡wYR£¢rvÓ®y®LF‹LzĈ„ôe]gx}•|KK}xklL]c¦£fRtív¦†PĤoH{tK" ] ], "encodeOffsets": [[[108619, 36299]], [[108589, 36341]]] } }, { "type": "Feature", "id": "630000", "properties": { "id": "630000", "cp": [96.778916, 35.623178], "name": "青海", "childNum": 2 }, "geometry": { "type": "MultiPolygon", "coordinates": [ ["@@InJm"], [ "@@CƒÆ½OŃĦsΰ~dz¦@@“Ņiš±è}ؘƄ˹A³r_ĞŠǒNΌĐw¤^ŬĵªpĺSZg’rpiƼĘԛ¨C|͖J’©Ħ»®VIJ~f\\m `Un„˜~ʌŸ•ĬàöNt•~ňjy–¢Zi˜Ɣ¥ĄŠk´nl`JʇŠJþ©pdƖ®È£¶ìRʦ‘źõƮËnŸʼėæÑƀĎ[‚˜¢VÎĂMÖÝÎF²sƊƀÎBļýƞ—¯ʘƭðħ¼Jh¿ŦęΌƇš¥²Q]Č¥nuÂÏriˆ¸¬ƪÛ^Ó¦d€¥[Wà…x\\ZŽjҕ¨GtpþYŊĕ´€zUO뇉P‰îMĄÁxH´á˜iÜUà›îÜՁĂÛSuŎ‹r“œJð̬EŒ‘FÁú×uÃÎkr“Ē{V}İ«O_ÌËĬ©ŽÓŧSRѱ§Ģ£^ÂyèçěM³Ƃę{[¸¿u…ºµ[gt£¸OƤĿéYŸõ·kŸq]juw¥Dĩƍ€õÇPéĽG‘ž©ã‡¤G…uȧþRcÕĕNy“yût“ˆ­‡ø‘†ï»a½ē¿BMoᣟÍj}éZËqbʍš“Ƭh¹ìÿÓAçãnIáI`ƒks£CG­ě˜Uy×Cy•…’Ÿ@¶ʡÊBnāzG„ơMē¼±O÷õJËĚăVŸĪũƆ£Œ¯{ËL½Ìzż“„VR|ĠTbuvJvµhĻĖH”Aëáa…­OÇðñęNw‡…œľ·L›mI±íĠĩPÉ×®ÿs—’cB³±JKßĊ«`…ađ»·QAmO’‘Vţéÿ¤¹SQt]]Çx€±¯A@ĉij¢Ó祖•ƒl¶ÅÛr—ŕspãRk~¦ª]Į­´“FR„åd­ČsCqđéFn¿Åƃm’Éx{W©ºƝºįkÕƂƑ¸wWūЩÈFž£\\tÈ¥ÄRÈýÌJ ƒlGr^×äùyÞ³fj”c†€¨£ÂZ|ǓMĝšÏ@ëÜőR‹›ĝ‰Œ÷¡{aïȷPu°ËXÙ{©TmĠ}Y³’­ÞIňµç½©C¡į÷¯B»|St»›]vƒųƒs»”}MÓ ÿʪƟǭA¡fs˜»PY¼c¡»¦c„ċ­¥£~msĉP•–Siƒ^o©A‰Šec‚™PeǵŽkg‚yUi¿h}aH™šĉ^|ᴟ¡HØûÅ«ĉ®]m€¡qĉ¶³ÈyôōLÁst“BŸ®wn±ă¥HSò뚣˜S’ë@לÊăxÇN©™©T±ª£IJ¡fb®ÞbŽb_Ą¥xu¥B—ž{łĝ³«`d˜Ɛt—¤ťiñžÍUuºí`£˜^tƃIJc—·ÛLO‹½Šsç¥Ts{ă\\_»™kϊ±q©čiìĉ|ÍIƒ¥ć¥›€]ª§D{ŝŖÉR_sÿc³Īō›ƿΑ›§p›[ĉ†›c¯bKm›R¥{³„Z†e^ŽŒwx¹dƽŽôIg §Mĕ ƹĴ¿—ǣÜ̓]‹Ý–]snåA{‹eŒƭ`ǻŊĿ\\ijŬű”YÂÿ¬jĖqŽßbЏ•L«¸©@ěĀ©ê¶ìÀEH|´bRľž–Ó¶rÀQþ‹vl®Õ‚E˜TzÜdb ˜hw¤{LR„ƒd“c‹b¯‹ÙVgœ‚ƜßzÃô쮍^jUèXΖ|UäÌ»rKŽ\\ŒªN‘¼pZCü†VY††¤ɃRi^rPҒTÖ}|br°qňb̰ªiƶGQ¾²„x¦PœmlŜ‘[Ĥ¡ΞsĦŸÔÏâ\\ªÚŒU\\f…¢N²§x|¤§„xĔsZPòʛ²SÐqF`ª„VƒÞŜĶƨVZŒÌL`ˆ¢dŐIqr\\oäõ–F礻Ŷ×h¹]Clـ\\¦ďÌį¬řtTӺƙgQÇÓHţĒ”´ÃbEÄlbʔC”|CˆŮˆk„Ʈ[ʼ¬ňœ´KŮÈΰÌζƶlð”ļA†TUvdTŠG†º̼ŠÔ€ŒsÊDԄveOg" ] ], "encodeOffsets": [[[105308, 37219]], [[95370, 40081]]] } }, { "type": "Feature", "id": "640000", "properties": { "id": "640000", "cp": [106.278179, 37.26637], "name": "宁夏", "childNum": 2 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ "@@KëÀęĞ«OęȿȕŸı]ʼn¡åįÕÔ«Ǵõƪ™ĚQÐZhv K°›öqÀѐS[ÃÖHƖčË‡nL]ûc…Ùß@‚“ĝ‘¾}w»»‹oģF¹œ»kÌÏ·{zPƒ§B­¢íyÅt@ƒ@áš]Yv_ssģ¼i߁”ĻL¾ġsKD£¡N_…“˜X¸}B~Haiˆ™Åf{«x»ge_bs“KF¯¡Ix™mELcÿZ¤­Ģ‘ƒÝœsuBLù•t†ŒYdˆmVtNmtOPhRw~bd…¾qÐ\\âÙH\\bImlNZŸ»loƒŸqlVm–Gā§~QCw¤™{A\\‘PKŸNY‡¯bF‡kC¥’sk‹Šs_Ã\\ă«¢ħkJi¯r›rAhĹûç£CU‡ĕĊ_ԗBixÅُĄnªÑaM~ħpOu¥sîeQ¥¤^dkKwlL~{L~–hw^‚ófćƒKyEŒ­K­zuÔ¡qQ¤xZÑ¢^ļöܾEpž±âbÊÑÆ^fk¬…NC¾‘Œ“YpxbK~¥Že֎ŒäBlt¿Đx½I[ĒǙŒWž‹f»Ĭ}d§dµùEuj¨‚IÆ¢¥dXªƅx¿]mtÏwßR͌X¢͎vÆzƂZò®ǢÌʆCrâºMÞzžÆMҔÊÓŊZľ–r°Î®Ȉmª²ĈUªĚøºˆĮ¦ÌĘk„^FłĬhĚiĀ˾iİbjÕ" ], ["@@mfwěwMrŢªv@G‰"] ], "encodeOffsets": [[[109366, 40242]], [[108600, 36303]]] } }, { "type": "Feature", "id": "650000", "properties": { "id": "650000", "cp": [85.617733, 40.792818], "name": "新疆", "childNum": 1 }, "geometry": { "type": "Polygon", "coordinates": [ "@@QØĔ²X¨”~ǘBºjʐߨvK”ƔX¨vĊOžÃƒ·¢i@~c—‡ĝe_«”Eš“}QxgɪëÏÃ@sÅyXoŖ{ô«ŸuX…ê•Îf`œC‚¹ÂÿÐGĮÕĞXŪōŸMźÈƺQèĽôe|¿ƸJR¤ĘEjcUóº¯Ĩ_ŘÁMª÷Ð¥Oéȇ¿ÖğǤǷÂF҇zÉx[]­Ĥĝ‰œ¦EP}ûƥé¿İƷTėƫœŕƅ™ƱB»Đ±’ēO…¦E–•}‘`cȺrĦáŖuҞª«IJ‡πdƺÏØZƴwʄ¤ĖGЙǂZ̓èH¶}ÚZצʥĪï|ÇĦMŔ»İĝLj‹ì¥Βœba­¯¥ǕǚkĆŵĦɑĺƯxūД̵nơʃĽá½M»›òmqóŘĝč˾ăC…ćāƿÝɽ©DZŅ¹đ¥˜³ðLrÁ®ɱĕģʼnǻ̋ȥơŻǛȡVï¹Ň۩ûkɗġƁ§ʇė̕ĩũƽō^ƕŠUv£ƁQï“Ƶkŏ½ΉÃŭdzLқʻ«ƭ\\lƒ‡ŭD‡“{ʓDkaFÃÄa“³ŤđÔGRÈƚhSӹŚsİ«ĐË[¥ÚDkº^Øg¼ŵ¸£EÍö•€ůʼnT¡c_‡ËKY‹ƧUśĵ„݃U_©rETÏʜ±OñtYw獃{£¨uM³x½şL©Ùá[ÓÐĥ Νtģ¢\\‚ś’nkO›w¥±ƒT»ƷFɯàĩÞáB¹Æ…ÑUw„੍žĽw[“mG½Èå~‡Æ÷QyŠěCFmĭZī—ŵVÁ™ƿQƛ—ûXS²‰b½KϽĉS›©ŷXĕŸ{ŽĕK·¥Ɨcqq©f¿]‡ßDõU³h—­gËÇïģÉɋw“k¯í}I·šœbmœÉ–ř›īJɥĻˁ×xo›ɹī‡l•c…¤³Xù]‘™DžA¿w͉ì¥wÇN·ÂËnƾƍdǧđ®Ɲv•Um©³G\\“}µĿ‡QyŹl㓛µEw‰LJQ½yƋBe¶ŋÀů‡ož¥A—˜Éw@•{Gpm¿Aij†ŽKLhˆ³`ñcËtW‚±»ÕS‰ëüÿďD‡u\\wwwù³—V›LŕƒOMËGh£õP¡™er™Ïd{“‡ġWÁ…č|yšg^ğyÁzÙs`—s|ÉåªÇ}m¢Ń¨`x¥’ù^•}ƒÌ¥H«‰Yªƅ”Aйn~Ꝛf¤áÀz„gŠÇDIԝ´AňĀ҄¶ûEYospõD[{ù°]u›Jq•U•|Soċxţ[õÔĥkŋÞŭZ˺óYËüċrw €ÞkrťË¿XGÉbřaDü·Ē÷Aê[Ää€I®BÕИÞ_¢āĠpŠÛÄȉĖġDKwbm‡ÄNô‡ŠfœƫVÉvi†dz—H‘‹QµâFšù­Âœ³¦{YGžƒd¢ĚÜO „€{Ö¦ÞÍÀPŒ^b–ƾŠlŽ[„vt×ĈÍE˨¡Đ~´î¸ùÎh€uè`¸ŸHÕŔVºwĠââWò‡@{œÙNÝ´ə²ȕn{¿¥{l—÷eé^e’ďˆXj©î\\ªÑò˜Üìc\\üqˆÕ[Č¡xoÂċªbØ­Œø|€¶ȴZdÆÂšońéŒGš\\”¼C°ÌƁn´nxšÊOĨ’ہƴĸ¢¸òTxÊǪMīИÖŲÃɎOvˆʦƢ~FއRěò—¿ġ~åŊœú‰Nšžš¸qŽ’Ę[Ĕ¶ÂćnÒPĒÜvúĀÊbÖ{Äî¸~Ŕünp¤ÂH¾œĄYÒ©ÊfºmԈĘcDoĬMŬ’˜S¤„s²‚”ʘچžȂVŦ –ŽèW°ªB|IJXŔþÈJĦÆæFĚêŠYĂªĂ]øªŖNÞüA€’fɨJ€˜¯ÎrDDšĤ€`€mz\\„§~D¬{vJÂ˜«lµĂb–¤p€ŌŰNĄ¨ĊXW|ų ¿¾ɄĦƐMT”‡òP˜÷fØĶK¢ȝ˔Sô¹òEð­”`Ɩ½ǒÂň×äı–§ĤƝ§C~¡‚hlå‚ǺŦŞkâ’~}ŽFøàIJaĞ‚fƠ¥Ž„Ŕdž˜®U¸ˆźXœv¢aƆúŪtŠųƠjd•ƺŠƺÅìnrh\\ĺ¯äɝĦ]èpĄ¦´LƞĬŠ´ƤǬ˼Ēɸ¤rºǼ²¨zÌPðŀbþ¹ļD¢¹œ\\ĜÑŚŸ¶ZƄ³àjĨoâŠȴLʉȮŒĐ­ĚăŽÀêZǚŐ¤qȂ\\L¢ŌİfÆs|zºeªÙæ§΢{Ā´ƐÚ¬¨Ĵà²łhʺKÞºÖTŠiƢ¾ªì°`öøu®Ê¾ãØ" ], "encodeOffsets": [[88824, 50096]] } }, { "type": "Feature", "id": "110000", "properties": { "id": "110000", "cp": [116.405285, 39.904989], "name": "北京", "childNum": 1 }, "geometry": { "type": "Polygon", "coordinates": [ "@@ĽOÁ›ûtŷmiÍt_H»Ĩ±d`й­{bw…Yr“³S]§§o¹€qGtm_Sŧ€“oa›‹FLg‘QN_•dV€@Zom_ć\\ߚc±x¯oœRcfe…£’o§ËgToÛJíĔóu…|wP¤™XnO¢ÉˆŦ¯rNÄā¤zâŖÈRpŢZŠœÚ{GŠrFt¦Òx§ø¹RóäV¤XdˆżâºWbwڍUd®bêņ¾‘jnŎGŃŶŠnzÚSeîĜZczî¾i]͜™QaúÍÔiþĩȨWĢ‹ü|Ėu[qb[swP@ÅğP¿{\\‡¥A¨Ï‘Ѩj¯ŠX\\¯œMK‘pA³[H…īu}}" ], "encodeOffsets": [[120023, 41045]] } }, { "type": "Feature", "id": "120000", "properties": { "id": "120000", "cp": [117.190182, 39.125596], "name": "天津", "childNum": 1 }, "geometry": { "type": "Polygon", "coordinates": [ "@@ŬgX§Ü«E…¶Ḟ“¬O_™ïlÁg“z±AXe™µÄĵ{¶]gitgšIj·›¥îakS€‰¨ÐƎk}ĕ{gB—qGf{¿a†U^fI“ư‹³õ{YƒıëNĿžk©ïËZŏ‘R§òoY×Ógc…ĥs¡bġ«@dekąI[nlPqCnp{ˆō³°`{PNdƗqSÄĻNNâyj]äžÒD ĬH°Æ]~¡HO¾ŒX}ÐxŒgp“gWˆrDGˆŒpù‚Š^L‚ˆrzWxˆZ^¨´T\\|~@I‰zƒ–bĤ‹œjeĊªz£®Ĕvě€L†mV¾Ô_ȔNW~zbĬvG†²ZmDM~”~" ], "encodeOffsets": [[120237, 41215]] } }, { "type": "Feature", "id": "310000", "properties": { "id": "310000", "cp": [121.472644, 31.231706], "name": "上海", "childNum": 6 }, "geometry": { "type": "MultiPolygon", "coordinates": [ ["@@ɧư¬EpƸÁxc‡"], ["@@©„ªƒ"], ["@@”MA‹‘š"], ["@@Qp݁E§ÉC¾"], ["@@bŝՕÕEȣÚƥêImɇǦèÜĠŒÚžÃƌÃ͎ó"], ["@@ǜûȬɋŠŭ™×^‰sYŒɍDŋ‘ŽąñCG²«ªč@h–_p¯A{‡oloY€¬j@IJ`•gQڛhr|ǀ^MIJvtbe´R¯Ô¬¨YŽô¤r]ì†Ƭį"] ], "encodeOffsets": [ [[124702, 32062]], [[124547, 32200]], [[124808, 31991]], [[124726, 32110]], [[124903, 32376]], [[124438, 32149]] ] } }, { "type": "Feature", "id": "500000", "properties": { "id": "500000", "cp": [107.304962, 29.533155], "name": "重庆", "childNum": 2 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ "@@vjG~nGŘŬĶȂƀƾ¹¸ØÎezĆT¸}êЖqHŸðqĖ䒊¥^CƒIj–²p…\\_ æüY|[YxƊæuž°xb®…Űb@~¢NQt°¶‚S栓Ê~rljĔëĚ¢~šuf`‘‚†fa‚ĔJåĊ„nÖ]„jƎćÊ@Š£¾a®£Ű{ŶĕF‹ègLk{Y|¡ĜWƔtƬJÑxq‹±ĢN´‰òK‰™–LÈüD|s`ŋ’ć]ƒÃ‰`đŒMûƱ½~Y°ħ`ƏíW‰½eI‹½{aŸ‘OIrÏ¡ĕŇa†p†µÜƅġ‘œ^ÖÛbÙŽŏml½S‹êqDu[R‹ãË»†ÿw`»y‘¸_ĺę}÷`M¯ċfCVµqʼn÷Z•gg“Œ`d½pDO‡ÎCnœ^uf²ènh¼WtƏxRGg¦…pV„†FI±ŽG^ŒIc´ec‡’G•ĹÞ½sëĬ„h˜xW‚}Kӈe­Xsbk”F¦›L‘ØgTkïƵNï¶}Gy“w\\oñ¡nmĈzjŸ•@™Óc£»Wă¹Ój“_m»ˆ¹·~MvÛaqœ»­‰êœ’\\ÂoVnŽÓØÍ™²«‹bq¿efE „€‹Ĝ^Qž~ Évý‡ş¤²Į‰pEİ}zcĺƒL‹½‡š¿gņ›¡ýE¡ya£³t\\¨\\vú»¼§·Ñr_oÒý¥u‚•_n»_ƒ•At©Þűā§IVeëƒY}{VPÀFA¨ąB}q@|Ou—\\Fm‰QF݅Mw˜å}]•€|FmϋCaƒwŒu_p—¯sfÙgY…DHl`{QEfNysBЦzG¸rHe‚„N\\CvEsÐùÜ_·ÖĉsaQ¯€}_U‡†xÃđŠq›NH¬•Äd^ÝŰR¬ã°wećJEž·vÝ·Hgƒ‚éFXjÉê`|yŒpxkAwœWĐpb¥eOsmzwqChóUQl¥F^laf‹anòsr›EvfQdÁUVf—ÎvÜ^efˆtET¬ôA\\œ¢sJŽnQTjP؈xøK|nBz‰„œĞ»LY‚…FDxӄvr“[ehľš•vN”¢o¾NiÂxGp⬐z›bfZo~hGi’]öF|‰|Nb‡tOMn eA±ŠtPT‡LjpYQ|†SH††YĀxinzDJ€Ìg¢và¥Pg‰_–ÇzII‹€II•„£®S¬„Øs쐣ŒN" ], ["@@ifjN@s"] ], "encodeOffsets": [[[109628, 30765]], [[111725, 31320]]] } }, { "type": "Feature", "id": "810000", "properties": { "id": "810000", "cp": [114.173355, 22.320048], "name": "香港", "childNum": 5 }, "geometry": { "type": "MultiPolygon", "coordinates": [ ["@@AlBk"], ["@@mŽn"], ["@@EpFo"], ["@@ea¢pl¸Eõ¹‡hj[ƒ]ÔCΖ@lj˜¡uBXŸ…•´‹AI¹…[‹yDUˆ]W`çwZkmc–…M›žp€Åv›}I‹oJlcaƒfёKްä¬XJmРđhI®æÔtSHn€Eˆ„ÒrÈc"], ["@@rMUw‡AS®€e"] ], "encodeOffsets": [ [[117111, 23002]], [[117072, 22876]], [[117045, 22887]], [[116975, 23082]], [[116882, 22747]] ] } }, { "type": "Feature", "id": "820000", "properties": { "id": "820000", "cp": [113.54909, 22.198951], "name": "澳门", "childNum": 1 }, "geometry": { "type": "Polygon", "coordinates": ["@@kÊd°å§s"], "encodeOffsets": [[116279, 22639]] } } ], "UTF8Encoding": true } ================================================ FILE: yshop-drink-vue3/src/components/AppLinkInput/AppLinkSelectDialog.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/AppLinkInput/data.ts ================================================ // APP 链接分组 export interface AppLinkGroup { // 分组名称 name: string // 链接列表 links: AppLink[] } // APP 链接 export interface AppLink { // 链接名称 name: string // 链接地址 path: string // 链接的类型 type?: APP_LINK_TYPE_ENUM } // APP 链接类型(需要特殊处理,例如商品详情) export const enum APP_LINK_TYPE_ENUM { // 拼团活动 ACTIVITY_COMBINATION, // 秒杀活动 ACTIVITY_SECKILL, // 文章详情 ARTICLE_DETAIL, // 优惠券详情 COUPON_DETAIL, // 自定义页面详情 DIY_PAGE_DETAIL, // 品类列表 PRODUCT_CATEGORY_LIST, // 商品列表 PRODUCT_LIST, // 商品详情 PRODUCT_DETAIL_NORMAL, // 拼团商品详情 PRODUCT_DETAIL_COMBINATION, // 秒杀商品详情 PRODUCT_DETAIL_SECKILL } // APP 链接列表(做一下持久化?) export const APP_LINK_GROUP_LIST = [ { name: '商城', links: [ { name: '首页', path: '/pages/index/index' }, { name: '商品分类', path: '/pages/index/category', type: APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST }, { name: '购物车', path: '/pages/index/cart' }, { name: '个人中心', path: '/pages/index/user' }, { name: '商品搜索', path: '/pages/index/search' }, { name: '自定义页面', path: '/pages/index/page', type: APP_LINK_TYPE_ENUM.DIY_PAGE_DETAIL }, { name: '客服', path: '/pages/chat/index' }, { name: '系统设置', path: '/pages/public/setting' }, { name: '常见问题', path: '/pages/public/faq' } ] }, { name: '商品', links: [ { name: '商品列表', path: '/pages/goods/list', type: APP_LINK_TYPE_ENUM.PRODUCT_LIST }, { name: '商品详情', path: '/pages/goods/index', type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_NORMAL }, { name: '拼团商品详情', path: '/pages/goods/groupon', type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_COMBINATION }, { name: '秒杀商品详情', path: '/pages/goods/seckill', type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_SECKILL } ] }, { name: '营销活动', links: [ { name: '拼团订单', path: '/pages/activity/groupon/order' }, { name: '营销商品', path: '/pages/activity/index' }, { name: '拼团活动', path: '/pages/activity/groupon/list', type: APP_LINK_TYPE_ENUM.ACTIVITY_COMBINATION }, { name: '秒杀活动', path: '/pages/activity/seckill/list', type: APP_LINK_TYPE_ENUM.ACTIVITY_SECKILL }, { name: '签到中心', path: '/pages/app/sign' }, { name: '优惠券中心', path: '/pages/coupon/list' }, { name: '优惠券详情', path: '/pages/coupon/detail', type: APP_LINK_TYPE_ENUM.COUPON_DETAIL }, { name: '文章详情', path: '/pages/public/richtext', type: APP_LINK_TYPE_ENUM.ARTICLE_DETAIL } ] }, { name: '分销商城', links: [ { name: '分销中心', path: '/pages/commission/index' }, { name: '推广商品', path: '/pages/commission/goods' }, { name: '分销订单', path: '/pages/commission/order' }, { name: '我的团队', path: '/pages/commission/team' } ] }, { name: '支付', links: [ { name: '充值余额', path: '/pages/pay/recharge' }, { name: '充值记录', path: '/pages/pay/recharge-log' } ] }, { name: '用户中心', links: [ { name: '用户信息', path: '/pages/user/info' }, { name: '用户订单', path: '/pages/order/list' }, { name: '售后订单', path: '/pages/order/aftersale/list' }, { name: '商品收藏', path: '/pages/user/goods-collect' }, { name: '浏览记录', path: '/pages/user/goods-log' }, { name: '地址管理', path: '/pages/user/address/list' }, { name: '用户佣金', path: '/pages/user/wallet/commission' }, { name: '用户余额', path: '/pages/user/wallet/money' }, { name: '用户积分', path: '/pages/user/wallet/score' } ] } ] as AppLinkGroup[] ================================================ FILE: yshop-drink-vue3/src/components/AppLinkInput/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Backtop/index.ts ================================================ import Backtop from './src/Backtop.vue' export { Backtop } ================================================ FILE: yshop-drink-vue3/src/components/Backtop/src/Backtop.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Card/index.ts ================================================ import CardTitle from './src/CardTitle.vue' export { CardTitle } ================================================ FILE: yshop-drink-vue3/src/components/Card/src/CardTitle.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/ColorInput/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/ConfigGlobal/index.ts ================================================ import ConfigGlobal from './src/ConfigGlobal.vue' export { ConfigGlobal } ================================================ FILE: yshop-drink-vue3/src/components/ConfigGlobal/src/ConfigGlobal.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/ContentDetailWrap/index.ts ================================================ import ContentDetailWrap from './src/ContentDetailWrap.vue' export { ContentDetailWrap } ================================================ FILE: yshop-drink-vue3/src/components/ContentDetailWrap/src/ContentDetailWrap.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/ContentWrap/index.ts ================================================ import ContentWrap from './src/ContentWrap.vue' export { ContentWrap } ================================================ FILE: yshop-drink-vue3/src/components/ContentWrap/src/ContentWrap.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/CountTo/index.ts ================================================ import CountTo from './src/CountTo.vue' export { CountTo } ================================================ FILE: yshop-drink-vue3/src/components/CountTo/src/CountTo.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Crontab/index.ts ================================================ import Crontab from './src/Crontab.vue' export { Crontab } ================================================ FILE: yshop-drink-vue3/src/components/Crontab/src/Crontab.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Cropper/index.ts ================================================ import CropperImage from './src/Cropper.vue' import CropperAvatar from './src/CropperAvatar.vue' export { CropperImage, CropperAvatar } ================================================ FILE: yshop-drink-vue3/src/components/Cropper/src/CopperModal.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Cropper/src/Cropper.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Cropper/src/CropperAvatar.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Cropper/src/types.ts ================================================ import type Cropper from 'cropperjs' export interface CropendResult { imgBase64: string imgInfo: Cropper.Data } export type { Cropper } ================================================ FILE: yshop-drink-vue3/src/components/Descriptions/index.ts ================================================ import Descriptions from './src/Descriptions.vue' import DescriptionsItemLabel from './src/DescriptionsItemLabel.vue' export { Descriptions, DescriptionsItemLabel } ================================================ FILE: yshop-drink-vue3/src/components/Descriptions/src/Descriptions.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Descriptions/src/DescriptionsItemLabel.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Dialog/index.ts ================================================ import Dialog from './src/Dialog.vue' export { Dialog } ================================================ FILE: yshop-drink-vue3/src/components/Dialog/src/Dialog.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/DictTag/index.ts ================================================ import DictTag from './src/DictTag.vue' export { DictTag } ================================================ FILE: yshop-drink-vue3/src/components/DictTag/src/DictTag.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/DocAlert/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Draggable/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Echart/index.ts ================================================ import Echart from './src/Echart.vue' export { Echart } ================================================ FILE: yshop-drink-vue3/src/components/Echart/src/Echart.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Editor/index.ts ================================================ import Editor from './src/Editor.vue' import { IDomEditor } from '@wangeditor/editor' export interface EditorExpose { getEditorRef: () => Promise } export { Editor } ================================================ FILE: yshop-drink-vue3/src/components/Editor/src/Editor.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Error/index.ts ================================================ import Error from './src/Error.vue' export { Error } ================================================ FILE: yshop-drink-vue3/src/components/Error/src/Error.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Form/index.ts ================================================ import Form from './src/Form.vue' import { ElForm } from 'element-plus' import { FormSchema, FormSetPropsType } from '@/types/form' export interface FormExpose { setValues: (data: Recordable) => void setProps: (props: Recordable) => void delSchema: (field: string) => void addSchema: (formSchema: FormSchema, index?: number) => void setSchema: (schemaProps: FormSetPropsType[]) => void formModel: Recordable getElFormRef: () => ComponentRef } export { Form } ================================================ FILE: yshop-drink-vue3/src/components/Form/src/Form.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Form/src/componentMap.ts ================================================ import type { Component } from 'vue' import { ElCascader, ElCheckboxGroup, ElColorPicker, ElDatePicker, ElInput, ElInputNumber, ElRadioGroup, ElRate, ElSelect, ElSelectV2, ElTreeSelect, ElSlider, ElSwitch, ElTimePicker, ElTimeSelect, ElTransfer, ElAutocomplete, ElDivider } from 'element-plus' import { InputPassword } from '@/components/InputPassword' import { Editor } from '@/components/Editor' import { UploadImg, UploadImgs, UploadFile } from '@/components/UploadFile' import { ComponentName } from '@/types/components' const componentMap: Recordable = { Radio: ElRadioGroup, Checkbox: ElCheckboxGroup, CheckboxButton: ElCheckboxGroup, Input: ElInput, Autocomplete: ElAutocomplete, InputNumber: ElInputNumber, Select: ElSelect, Cascader: ElCascader, Switch: ElSwitch, Slider: ElSlider, TimePicker: ElTimePicker, DatePicker: ElDatePicker, Rate: ElRate, ColorPicker: ElColorPicker, Transfer: ElTransfer, Divider: ElDivider, TimeSelect: ElTimeSelect, SelectV2: ElSelectV2, TreeSelect: ElTreeSelect, RadioButton: ElRadioGroup, InputPassword: InputPassword, Editor: Editor, UploadImg: UploadImg, UploadImgs: UploadImgs, UploadFile: UploadFile } export { componentMap } ================================================ FILE: yshop-drink-vue3/src/components/Form/src/components/useRenderCheckbox.tsx ================================================ import { FormSchema } from '@/types/form' import { ElCheckbox, ElCheckboxButton } from 'element-plus' import { defineComponent } from 'vue' export const useRenderCheckbox = () => { const renderCheckboxOptions = (item: FormSchema) => { // 如果有别名,就取别名 const labelAlias = item?.componentProps?.optionsAlias?.labelField const valueAlias = item?.componentProps?.optionsAlias?.valueField const Com = (item.component === 'Checkbox' ? ElCheckbox : ElCheckboxButton) as ReturnType< typeof defineComponent > return item?.componentProps?.options?.map((option) => { const { ...other } = option return ( {option[labelAlias || 'label']} ) }) } return { renderCheckboxOptions } } ================================================ FILE: yshop-drink-vue3/src/components/Form/src/components/useRenderRadio.tsx ================================================ import { FormSchema } from '@/types/form' import { ElRadio, ElRadioButton } from 'element-plus' import { defineComponent } from 'vue' export const useRenderRadio = () => { const renderRadioOptions = (item: FormSchema) => { // 如果有别名,就取别名 const labelAlias = item?.componentProps?.optionsAlias?.labelField const valueAlias = item?.componentProps?.optionsAlias?.valueField const Com = (item.component === 'Radio' ? ElRadio : ElRadioButton) as ReturnType< typeof defineComponent > return item?.componentProps?.options?.map((option) => { const { ...other } = option return ( {option[labelAlias || 'label']} ) }) } return { renderRadioOptions } } ================================================ FILE: yshop-drink-vue3/src/components/Form/src/components/useRenderSelect.tsx ================================================ import { FormSchema } from '@/types/form' import { ComponentOptions } from '@/types/components' import { ElOption, ElOptionGroup } from 'element-plus' import { getSlot } from '@/utils/tsxHelper' import { Slots } from 'vue' export const useRenderSelect = (slots: Slots) => { // 渲染 select options const renderSelectOptions = (item: FormSchema) => { // 如果有别名,就取别名 const labelAlias = item?.componentProps?.optionsAlias?.labelField return item?.componentProps?.options?.map((option) => { if (option?.options?.length) { return ( {() => { return option?.options?.map((v) => { return renderSelectOptionItem(item, v) }) }} ) } else { return renderSelectOptionItem(item, option) } }) } // 渲染 select option item const renderSelectOptionItem = (item: FormSchema, option: ComponentOptions) => { // 如果有别名,就取别名 const labelAlias = item?.componentProps?.optionsAlias?.labelField const valueAlias = item?.componentProps?.optionsAlias?.valueField const { label, value, ...other } = option return ( {{ default: () => // option 插槽名规则,{field}-option item?.componentProps?.optionsSlot ? getSlot(slots, `${item.field}-option`, { item: option }) : undefined }} ) } return { renderSelectOptions } } ================================================ FILE: yshop-drink-vue3/src/components/Form/src/helper.ts ================================================ import type { Slots } from 'vue' import { getSlot } from '@/utils/tsxHelper' import { PlaceholderModel } from './types' import { FormSchema } from '@/types/form' import { ColProps } from '@/types/components' /** * * @param schema 对应组件数据 * @returns 返回提示信息对象 * @description 用于自动设置placeholder */ export const setTextPlaceholder = (schema: FormSchema): PlaceholderModel => { const { t } = useI18n() const textMap = ['Input', 'Autocomplete', 'InputNumber', 'InputPassword'] const selectMap = ['Select', 'SelectV2', 'TimePicker', 'DatePicker', 'TimeSelect', 'TimeSelect'] if (textMap.includes(schema?.component as string)) { return { placeholder: t('common.inputText') + schema.label } } if (selectMap.includes(schema?.component as string)) { // 一些范围选择器 const twoTextMap = ['datetimerange', 'daterange', 'monthrange', 'datetimerange', 'daterange'] if ( twoTextMap.includes( (schema?.componentProps?.type || schema?.componentProps?.isRange) as string ) ) { return { startPlaceholder: t('common.startTimeText'), endPlaceholder: t('common.endTimeText'), rangeSeparator: '-' } } else { return { placeholder: t('common.selectText') + schema.label } } } return {} } /** * * @param col 内置栅格 * @returns 返回栅格属性 * @description 合并传入进来的栅格属性 */ export const setGridProp = (col: ColProps = {}): ColProps => { const colProps: ColProps = { // 如果有span,代表用户优先级更高,所以不需要默认栅格 ...(col.span ? {} : { xs: 24, sm: 12, md: 12, lg: 12, xl: 12 }), ...col } return colProps } /** * * @param item 传入的组件属性 * @returns 默认添加 clearable 属性 */ export const setComponentProps = (item: FormSchema): Recordable => { const notNeedClearable = ['ColorPicker'] const componentProps: Recordable = notNeedClearable.includes(item.component as string) ? { ...item.componentProps } : { clearable: true, ...item.componentProps } // 需要删除额外的属性 delete componentProps?.slots return componentProps } /** * * @param slots 插槽 * @param slotsProps 插槽属性 * @param field 字段名 */ export const setItemComponentSlots = ( slots: Slots, slotsProps: Recordable = {}, field: string ): Recordable => { const slotObj: Recordable = {} for (const key in slotsProps) { if (slotsProps[key]) { // 由于组件有可能重复,需要有一个唯一的前缀 slotObj[key] = (data: Recordable) => { return getSlot(slots, `${field}-${key}`, data) } } } return slotObj } /** * * @param schema Form表单结构化数组 * @param formModel FormModel * @returns FormModel * @description 生成对应的formModel */ export const initModel = (schema: FormSchema[], formModel: Recordable) => { const model: Recordable = { ...formModel } schema.map((v) => { // 如果是hidden,就删除对应的值 if (v.hidden) { delete model[v.field] } else if (v.component && v.component !== 'Divider') { const hasField = Reflect.has(model, v.field) // 如果先前已经有值存在,则不进行重新赋值,而是采用现有的值 model[v.field] = hasField ? model[v.field] : v.value !== void 0 ? v.value : '' } }) return model } /** * @param slots 插槽 * @param field 字段名 * @returns 返回FormIiem插槽 */ export const setFormItemSlots = (slots: Slots, field: string): Recordable => { const slotObj: Recordable = {} if (slots[`${field}-error`]) { slotObj['error'] = (data: Recordable) => { return getSlot(slots, `${field}-error`, data) } } if (slots[`${field}-label`]) { slotObj['label'] = (data: Recordable) => { return getSlot(slots, `${field}-label`, data) } } return slotObj } ================================================ FILE: yshop-drink-vue3/src/components/Form/src/types.ts ================================================ import { FormSchema } from '@/types/form' export interface PlaceholderModel { placeholder?: string startPlaceholder?: string endPlaceholder?: string rangeSeparator?: string } export type FormProps = { schema?: FormSchema[] isCol?: boolean model?: Recordable autoSetPlaceholder?: boolean isCustom?: boolean labelWidth?: string | number } & Recordable ================================================ FILE: yshop-drink-vue3/src/components/FormCreate/index.ts ================================================ import { useFormCreateDesigner } from './src/useFormCreateDesigner' import { useApiSelect } from './src/components/useApiSelect' export { useFormCreateDesigner, useApiSelect } ================================================ FILE: yshop-drink-vue3/src/components/FormCreate/src/components/DictSelect.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/FormCreate/src/components/useApiSelect.tsx ================================================ import request from '@/config/axios' import { isEmpty } from '@/utils/is' import { ApiSelectProps } from '@/components/FormCreate/src/type' import { jsonParse } from '@/utils' export const useApiSelect = (option: ApiSelectProps) => { return defineComponent({ name: option.name, props: { // 选项标签 labelField: { type: String, default: () => option.labelField ?? 'label' }, // 选项的值 valueField: { type: String, default: () => option.valueField ?? 'value' }, // api 接口 url: { type: String, default: () => option.url ?? '' }, // 请求类型 method: { type: String, default: 'GET' }, // 请求参数 data: { type: String, default: '' }, // 选择器类型,下拉框 select、多选框 checkbox、单选框 radio selectType: { type: String, default: 'select' }, // 是否多选 multiple: { type: Boolean, default: false } }, setup(props) { const attrs = useAttrs() const options = ref([]) // 下拉数据 const getOptions = async () => { options.value = [] // 接口选择器 if (isEmpty(props.url)) { return } let data = [] switch (props.method) { case 'GET': data = await request.get({ url: props.url }) break case 'POST': data = await request.post({ url: props.url, data: jsonParse(props.data) }) break } if (Array.isArray(data)) { options.value = data.map((item: any) => ({ label: item[props.labelField], value: item[props.valueField] })) return } console.error(`接口[${props.url}] 返回结果不是一个数组`) } onMounted(async () => { await getOptions() }) const buildSelect = () => { if (props.multiple) { // fix:多写此步是为了解决 multiple 属性问题 return ( {options.value.map((item, index) => ( ))} ) } return ( {options.value.map((item, index) => ( ))} ) } const buildCheckbox = () => { if (isEmpty(options.value)) { options.value = [ { label: '选项1', value: '选项1' }, { label: '选项2', value: '选项2' } ] } return ( {options.value.map((item, index) => ( ))} ) } const buildRadio = () => { if (isEmpty(options.value)) { options.value = [ { label: '选项1', value: '选项1' }, { label: '选项2', value: '选项2' } ] } return ( {options.value.map((item, index) => ( {item.label} ))} ) } return () => ( <> {props.selectType === 'select' ? buildSelect() : props.selectType === 'radio' ? buildRadio() : props.selectType === 'checkbox' ? buildCheckbox() : buildSelect()} ) } }) } ================================================ FILE: yshop-drink-vue3/src/components/FormCreate/src/config/index.ts ================================================ import { useUploadFileRule } from './useUploadFileRule' import { useUploadImgRule } from './useUploadImgRule' import { useUploadImgsRule } from './useUploadImgsRule' import { useDictSelectRule } from './useDictSelectRule' import { useEditorRule } from './useEditorRule' import { useSelectRule } from './useSelectRule' export { useUploadFileRule, useUploadImgRule, useUploadImgsRule, useDictSelectRule, useEditorRule, useSelectRule } ================================================ FILE: yshop-drink-vue3/src/components/FormCreate/src/config/selectRule.ts ================================================ const selectRule = [ { type: 'select', field: 'selectType', title: '选择器类型', value: 'select', options: [ { label: '下拉框', value: 'select' }, { label: '单选框', value: 'radio' }, { label: '多选框', value: 'checkbox' } ], // 参考 https://www.form-create.com/v3/guide/control 组件联动,单选框和多选框不需要多选属性 control: [ { value: 'select', condition: '=', method: 'hidden', rule: ['multiple'] } ] }, { type: 'switch', field: 'multiple', title: '是否多选' }, { type: 'switch', field: 'disabled', title: '是否禁用' }, { type: 'switch', field: 'clearable', title: '是否可以清空选项' }, { type: 'switch', field: 'collapseTags', title: '多选时是否将选中值按文字的形式展示' }, { type: 'inputNumber', field: 'multipleLimit', title: '多选时用户最多可以选择的项目数,为 0 则不限制', props: { min: 0 } }, { type: 'input', field: 'autocomplete', title: 'autocomplete 属性' }, { type: 'input', field: 'placeholder', title: '占位符' }, { type: 'switch', field: 'filterable', title: '是否可搜索' }, { type: 'switch', field: 'allowCreate', title: '是否允许用户创建新条目' }, { type: 'input', field: 'noMatchText', title: '搜索条件无匹配时显示的文字' }, { type: 'switch', field: 'remote', title: '其中的选项是否从服务器远程加载' }, { type: 'Struct', field: 'remoteMethod', title: '自定义远程搜索方法' }, { type: 'input', field: 'noDataText', title: '选项为空时显示的文字' }, { type: 'switch', field: 'reserveKeyword', title: '多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词' }, { type: 'switch', field: 'defaultFirstOption', title: '在输入框按下回车,选择第一个匹配项' }, { type: 'switch', field: 'popperAppendToBody', title: '是否将弹出框插入至 body 元素', value: true }, { type: 'switch', field: 'automaticDropdown', title: '对于不可搜索的 Select,是否在输入框获得焦点后自动弹出选项菜单' } ] const apiSelectRule = [ { type: 'input', field: 'url', title: 'url 地址', props: { placeholder: '/system/user/simple-list' } }, { type: 'select', field: 'method', title: '请求类型', value: 'GET', options: [ { label: 'GET', value: 'GET' }, { label: 'POST', value: 'POST' } ], control: [ { value: 'GET', condition: '!=', method: 'hidden', rule: [ { type: 'input', field: 'data', title: '请求参数 JSON 格式', props: { autosize: true, type: 'textarea', placeholder: '{"type": 1}' } } ] } ] }, { type: 'input', field: 'labelField', title: 'label 属性', props: { placeholder: 'nickname' } }, { type: 'input', field: 'valueField', title: 'value 属性', props: { placeholder: 'id' } } ] export { selectRule, apiSelectRule } ================================================ FILE: yshop-drink-vue3/src/components/FormCreate/src/config/useDictSelectRule.ts ================================================ import { generateUUID } from '@/utils' import * as DictDataApi from '@/api/system/dict/dict.type' import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils' import { selectRule } from '@/components/FormCreate/src/config/selectRule' /** * 字典选择器规则,如果规则使用到动态数据则需要单独配置不能使用 useSelectRule */ export const useDictSelectRule = () => { const label = '字典选择器' const name = 'DictSelect' const dictOptions = ref<{ label: string; value: string }[]>([]) // 字典类型下拉数据 onMounted(async () => { const data = await DictDataApi.getSimpleDictTypeList() if (!data || data.length === 0) { return } dictOptions.value = data?.map((item: DictDataApi.DictTypeVO) => ({ label: item.name, value: item.type })) ?? [] }) return { icon: 'icon-doc-text', label, name, rule() { return { type: name, field: generateUUID(), title: label, info: '', $required: false } }, props(_, { t }) { return localeProps(t, name + '.props', [ makeRequiredRule(), { type: 'select', field: 'dictType', title: '字典类型', value: '', options: dictOptions.value }, { type: 'select', field: 'dictValueType', title: '字典值类型', value: 'str', options: [ { label: '数字', value: 'int' }, { label: '字符串', value: 'str' }, { label: '布尔值', value: 'bool' } ] }, ...selectRule ]) } } } ================================================ FILE: yshop-drink-vue3/src/components/FormCreate/src/config/useEditorRule.ts ================================================ import { generateUUID } from '@/utils' import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils' export const useEditorRule = () => { const label = '富文本' const name = 'Editor' return { icon: 'icon-editor', label, name, rule() { return { type: name, field: generateUUID(), title: label, info: '', $required: false } }, props(_, { t }) { return localeProps(t, name + '.props', [ makeRequiredRule(), { type: 'input', field: 'height', title: '高度' }, { type: 'switch', field: 'readonly', title: '是否只读' } ]) } } } ================================================ FILE: yshop-drink-vue3/src/components/FormCreate/src/config/useSelectRule.ts ================================================ import { generateUUID } from '@/utils' import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils' import { selectRule } from '@/components/FormCreate/src/config/selectRule' import { SelectRuleOption } from '@/components/FormCreate/src/type' /** * 通用选择器规则 hook * * @param option 规则配置 */ export const useSelectRule = (option: SelectRuleOption) => { const label = option.label const name = option.name return { icon: option.icon, label, name, rule() { return { type: name, field: generateUUID(), title: label, info: '', $required: false } }, props(_, { t }) { if (!option.props) { option.props = [] } return localeProps(t, name + '.props', [makeRequiredRule(), ...option.props, ...selectRule]) } } } ================================================ FILE: yshop-drink-vue3/src/components/FormCreate/src/config/useUploadFileRule.ts ================================================ import { generateUUID } from '@/utils' import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils' export const useUploadFileRule = () => { const label = '文件上传' const name = 'UploadFile' return { icon: 'icon-upload', label, name, rule() { return { type: name, field: generateUUID(), title: label, info: '', $required: false } }, props(_, { t }) { return localeProps(t, name + '.props', [ makeRequiredRule(), { type: 'select', field: 'fileType', title: '文件类型', value: ['doc', 'xls', 'ppt', 'txt', 'pdf'], options: [ { label: 'doc', value: 'doc' }, { label: 'xls', value: 'xls' }, { label: 'ppt', value: 'ppt' }, { label: 'txt', value: 'txt' }, { label: 'pdf', value: 'pdf' } ], props: { multiple: true } }, { type: 'switch', field: 'autoUpload', title: '是否在选取文件后立即进行上传', value: true }, { type: 'switch', field: 'drag', title: '拖拽上传', value: false }, { type: 'switch', field: 'isShowTip', title: '是否显示提示', value: true }, { type: 'inputNumber', field: 'fileSize', title: '大小限制(MB)', value: 5, props: { min: 0 } }, { type: 'inputNumber', field: 'limit', title: '数量限制', value: 5, props: { min: 0 } }, { type: 'switch', field: 'disabled', title: '是否禁用', value: false } ]) } } } ================================================ FILE: yshop-drink-vue3/src/components/FormCreate/src/config/useUploadImgRule.ts ================================================ import { generateUUID } from '@/utils' import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils' export const useUploadImgRule = () => { const label = '单图上传' const name = 'UploadImg' return { icon: 'icon-upload', label, name, rule() { return { type: name, field: generateUUID(), title: label, info: '', $required: false } }, props(_, { t }) { return localeProps(t, name + '.props', [ makeRequiredRule(), { type: 'switch', field: 'drag', title: '拖拽上传', value: false }, { type: 'select', field: 'fileType', title: '图片类型限制', value: ['image/jpeg', 'image/png', 'image/gif'], options: [ { label: 'image/apng', value: 'image/apng' }, { label: 'image/bmp', value: 'image/bmp' }, { label: 'image/gif', value: 'image/gif' }, { label: 'image/jpeg', value: 'image/jpeg' }, { label: 'image/pjpeg', value: 'image/pjpeg' }, { label: 'image/svg+xml', value: 'image/svg+xml' }, { label: 'image/tiff', value: 'image/tiff' }, { label: 'image/webp', value: 'image/webp' }, { label: 'image/x-icon', value: 'image/x-icon' } ], props: { multiple: true } }, { type: 'inputNumber', field: 'fileSize', title: '大小限制(MB)', value: 5, props: { min: 0 } }, { type: 'input', field: 'height', title: '组件高度', value: '150px' }, { type: 'input', field: 'width', title: '组件宽度', value: '150px' }, { type: 'input', field: 'borderradius', title: '组件边框圆角', value: '8px' }, { type: 'switch', field: 'disabled', title: '是否显示删除按钮', value: true }, { type: 'switch', field: 'showBtnText', title: '是否显示按钮文字', value: true } ]) } } } ================================================ FILE: yshop-drink-vue3/src/components/FormCreate/src/config/useUploadImgsRule.ts ================================================ import { generateUUID } from '@/utils' import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils' export const useUploadImgsRule = () => { const label = '多图上传' const name = 'UploadImgs' return { icon: 'icon-upload', label, name, rule() { return { type: name, field: generateUUID(), title: label, info: '', $required: false } }, props(_, { t }) { return localeProps(t, name + '.props', [ makeRequiredRule(), { type: 'switch', field: 'drag', title: '拖拽上传', value: false }, { type: 'select', field: 'fileType', title: '图片类型限制', value: ['image/jpeg', 'image/png', 'image/gif'], options: [ { label: 'image/apng', value: 'image/apng' }, { label: 'image/bmp', value: 'image/bmp' }, { label: 'image/gif', value: 'image/gif' }, { label: 'image/jpeg', value: 'image/jpeg' }, { label: 'image/pjpeg', value: 'image/pjpeg' }, { label: 'image/svg+xml', value: 'image/svg+xml' }, { label: 'image/tiff', value: 'image/tiff' }, { label: 'image/webp', value: 'image/webp' }, { label: 'image/x-icon', value: 'image/x-icon' } ], props: { multiple: true } }, { type: 'inputNumber', field: 'fileSize', title: '大小限制(MB)', value: 5, props: { min: 0 } }, { type: 'inputNumber', field: 'limit', title: '数量限制', value: 5, props: { min: 0 } }, { type: 'input', field: 'height', title: '组件高度', value: '150px' }, { type: 'input', field: 'width', title: '组件宽度', value: '150px' }, { type: 'input', field: 'borderradius', title: '组件边框圆角', value: '8px' } ]) } } } ================================================ FILE: yshop-drink-vue3/src/components/FormCreate/src/type/index.ts ================================================ import { Rule } from '@form-create/element-ui' //左侧拖拽按钮 // 左侧拖拽按钮 export interface MenuItem { label: string name: string icon: string } // 左侧拖拽按钮分类 export interface Menu { title: string name: string list: MenuItem[] } export interface MenuList extends Array {} // 拖拽组件的规则 export interface DragRule { icon: string name: string label: string children?: string inside?: true drag?: true | String dragBtn?: false mask?: false rule(): Rule props(v: any, v1: any): Rule[] } // 通用下拉组件 Props 类型 export interface ApiSelectProps { name: string // 组件名称 labelField?: string // 选项标签 valueField?: string // 选项的值 url?: string // url 接口 isDict?: boolean // 是否字典选择器 } // 选择组件规则配置类型 export interface SelectRuleOption { label: string // label 名称 name: string // 组件名称 icon: string // 组件图标 props?: any[] // 组件规则 } ================================================ FILE: yshop-drink-vue3/src/components/FormCreate/src/useFormCreateDesigner.ts ================================================ import { useDictSelectRule, useEditorRule, useSelectRule, useUploadFileRule, useUploadImgRule, useUploadImgsRule } from './config' import { Ref } from 'vue' import { Menu } from '@/components/FormCreate/src/type' import { apiSelectRule } from '@/components/FormCreate/src/config/selectRule' /** * 表单设计器增强 hook * 新增 * - 文件上传 * - 单图上传 * - 多图上传 * - 字典选择器 * - 用户选择器 * - 部门选择器 * - 富文本 */ export const useFormCreateDesigner = async (designer: Ref) => { const editorRule = useEditorRule() const uploadFileRule = useUploadFileRule() const uploadImgRule = useUploadImgRule() const uploadImgsRule = useUploadImgsRule() /** * 构建表单组件 */ const buildFormComponents = () => { // 移除自带的上传组件规则,使用 uploadFileRule、uploadImgRule、uploadImgsRule 替代 designer.value?.removeMenuItem('upload') // 移除自带的富文本组件规则,使用 editorRule 替代 designer.value?.removeMenuItem('fc-editor') const components = [editorRule, uploadFileRule, uploadImgRule, uploadImgsRule] components.forEach((component) => { // 插入组件规则 designer.value?.addComponent(component) // 插入拖拽按钮到 `main` 分类下 designer.value?.appendMenuItem('main', { icon: component.icon, name: component.name, label: component.label }) }) } const userSelectRule = useSelectRule({ name: 'UserSelect', label: '用户选择器', icon: 'icon-user-o' }) const deptSelectRule = useSelectRule({ name: 'DeptSelect', label: '部门选择器', icon: 'icon-address-card-o' }) const dictSelectRule = useDictSelectRule() const apiSelectRule0 = useSelectRule({ name: 'ApiSelect', label: '接口选择器', icon: 'icon-server', props: [...apiSelectRule] }) /** * 构建系统字段菜单 */ const buildSystemMenu = () => { // 移除自带的下拉选择器组件,使用 currencySelectRule 替代 designer.value?.removeMenuItem('select') designer.value?.removeMenuItem('radio') designer.value?.removeMenuItem('checkbox') const components = [userSelectRule, deptSelectRule, dictSelectRule, apiSelectRule0] const menu: Menu = { name: 'system', title: '系统字段', list: components.map((component) => { // 插入组件规则 designer.value?.addComponent(component) // 插入拖拽按钮到 `system` 分类下 return { icon: component.icon, name: component.name, label: component.label } }) } designer.value?.addMenu(menu) } onMounted(async () => { await nextTick() buildFormComponents() buildSystemMenu() }) } ================================================ FILE: yshop-drink-vue3/src/components/FormCreate/src/utils/index.ts ================================================ // TODO puhui999: 借鉴一下 form-create-designer utils 方法 🤣 (导入不了只能先 copy 过来用下) export function makeRequiredRule() { return { type: 'Required', field: 'formCreate$required', title: '是否必填' } } export const localeProps = (t, prefix, rules) => { return rules.map((rule) => { if (rule.field === 'formCreate$required') { rule.title = t('props.required') || rule.title } else if (rule.field && rule.field !== '_optionType') { rule.title = t('components.' + prefix + '.' + rule.field) || rule.title } return rule }) } export function upper(str) { return str.replace(str[0], str[0].toLocaleUpperCase()) } export function makeOptionsRule(t, to, userOptions) { console.log(userOptions[0]) const options = [ { label: t('props.optionsType.struct'), value: 0 }, { label: t('props.optionsType.json'), value: 1 }, { label: '用户数据', value: 2 } ] const control = [ { value: 0, rule: [ { type: 'TableOptions', field: 'formCreate' + upper(to).replace('.', '>'), props: { defaultValue: [] } } ] }, { value: 1, rule: [ { type: 'Struct', field: 'formCreate' + upper(to).replace('.', '>'), props: { defaultValue: [] } } ] }, { value: 2, rule: [ { type: 'TableOptions', field: 'formCreate' + upper(to).replace('.', '>'), props: { modelValue: [] } } ] } ] options.splice(0, 0) control.push() return { type: 'radio', title: t('props.options'), field: '_optionType', value: 0, options, props: { type: 'button' }, control } } ================================================ FILE: yshop-drink-vue3/src/components/Highlight/index.ts ================================================ import Highlight from './src/Highlight.vue' export { Highlight } ================================================ FILE: yshop-drink-vue3/src/components/Highlight/src/Highlight.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/IFrame/index.ts ================================================ import IFrame from './src/IFrame.vue' export { IFrame } ================================================ FILE: yshop-drink-vue3/src/components/IFrame/src/IFrame.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Icon/index.ts ================================================ import Icon from './src/Icon.vue' import IconSelect from './src/IconSelect.vue' export { Icon, IconSelect } ================================================ FILE: yshop-drink-vue3/src/components/Icon/src/Icon.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Icon/src/IconSelect.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Icon/src/data.ts ================================================ export const IconJson = { 'ep:': [ 'add-location', 'aim', 'alarm-clock', 'apple', 'arrow-down', 'arrow-down-bold', 'arrow-left', 'arrow-left-bold', 'arrow-right', 'arrow-right-bold', 'arrow-up', 'arrow-up-bold', 'avatar', 'back', 'baseball', 'basketball', 'bell', 'bell-filled', 'bicycle', 'bottom', 'bottom-left', 'bottom-right', 'bowl', 'box', 'briefcase', 'brush', 'brush-filled', 'burger', 'calendar', 'camera', 'camera-filled', 'caret-bottom', 'caret-left', 'caret-right', 'caret-top', 'cellphone', 'chat-dot-round', 'chat-dot-square', 'chat-line-round', 'chat-line-square', 'chat-round', 'chat-square', 'check', 'checked', 'cherry', 'chicken', 'circle-check', 'circle-check-filled', 'circle-close', 'circle-close-filled', 'circle-plus', 'circle-plus-filled', 'clock', 'close', 'close-bold', 'cloudy', 'coffee', 'coffee-cup', 'coin', 'cold-drink', 'collection', 'collection-tag', 'comment', 'compass', 'connection', 'coordinate', 'copy-document', 'cpu', 'credit-card', 'crop', 'd-arrow-left', 'd-arrow-right', 'd-caret', 'data-analysis', 'data-board', 'data-line', 'delete', 'delete-filled', 'delete-location', 'dessert', 'discount', 'dish', 'dish-dot', 'document', 'document-add', 'document-checked', 'document-copy', 'document-delete', 'document-remove', 'download', 'drizzling', 'edit', 'edit-pen', 'eleme', 'eleme-filled', 'expand', 'failed', 'female', 'files', 'film', 'filter', 'finished', 'first-aid-kit', 'flag', 'fold', 'folder', 'folder-add', 'folder-checked', 'folder-delete', 'folder-opened', 'folder-remove', 'food', 'football', 'fork-spoon', 'fries', 'full-screen', 'goblet', 'goblet-full', 'goblet-square', 'goblet-square-full', 'goods', 'goods-filled', 'grape', 'grid', 'guide', 'headset', 'help', 'help-filled', 'histogram', 'home-filled', 'hot-water', 'house', 'ice-cream', 'ice-cream-round', 'ice-cream-square', 'ice-drink', 'ice-tea', 'info-filled', 'iphone', 'key', 'knife-fork', 'lightning', 'link', 'list', 'loading', 'location', 'location-filled', 'location-information', 'lock', 'lollipop', 'magic-stick', 'magnet', 'male', 'management', 'map-location', 'medal', 'menu', 'message', 'message-box', 'mic', 'microphone', 'milk-tea', 'minus', 'money', 'monitor', 'moon', 'moon-night', 'more', 'more-filled', 'mostly-cloudy', 'mouse', 'mug', 'mute', 'mute-notification', 'no-smoking', 'notebook', 'notification', 'odometer', 'office-building', 'open', 'operation', 'opportunity', 'orange', 'paperclip', 'partly-cloudy', 'pear', 'phone', 'phone-filled', 'picture', 'picture-filled', 'picture-rounded', 'pie-chart', 'place', 'platform', 'plus', 'pointer', 'position', 'postcard', 'pouring', 'present', 'price-tag', 'printer', 'promotion', 'question-filled', 'rank', 'reading', 'reading-lamp', 'refresh', 'refresh-left', 'refresh-right', 'refrigerator', 'remove', 'remove-filled', 'right', 'scale-to-original', 'school', 'scissor', 'search', 'select', 'sell', 'semi-select', 'service', 'set-up', 'setting', 'share', 'ship', 'shop', 'shopping-bag', 'shopping-cart', 'shopping-cart-full', 'smoking', 'soccer', 'sold-out', 'sort', 'sort-down', 'sort-up', 'stamp', 'star', 'star-filled', 'stopwatch', 'success-filled', 'sugar', 'suitcase', 'sunny', 'sunrise', 'sunset', 'switch', 'switch-button', 'takeaway-box', 'ticket', 'tickets', 'timer', 'toilet-paper', 'tools', 'top', 'top-left', 'top-right', 'trend-charts', 'trophy', 'turn-off', 'umbrella', 'unlock', 'upload', 'upload-filled', 'user', 'user-filled', 'van', 'video-camera', 'video-camera-filled', 'video-pause', 'video-play', 'view', 'wallet', 'wallet-filled', 'warning', 'warning-filled', 'watch', 'watermelon', 'wind-power', 'zoom-in', 'zoom-out' ], 'fa:': [ '500px', 'address-book', 'address-book-o', 'address-card', 'address-card-o', 'adjust', 'adn', 'align-center', 'align-justify', 'align-left', 'amazon', 'ambulance', 'american-sign-language-interpreting', 'anchor', 'android', 'angellist', 'angle-double-left', 'angle-double-up', 'angle-down', 'angle-left', 'angle-up', 'apple', 'archive', 'area-chart', 'arrow-circle-left', 'arrow-circle-o-left', 'arrow-circle-o-up', 'arrow-circle-up', 'arrow-left', 'arrow-up', 'arrows', 'arrows-alt', 'arrows-h', 'arrows-v', 'assistive-listening-systems', 'asterisk', 'at', 'audio-description', 'automobile', 'backward', 'balance-scale', 'ban', 'bandcamp', 'bank', 'bar-chart', 'barcode', 'bars', 'bath', 'battery', 'battery-0', 'battery-1', 'battery-2', 'battery-3', 'bed', 'beer', 'behance', 'behance-square', 'bell', 'bell-o', 'bell-slash', 'bell-slash-o', 'bicycle', 'binoculars', 'birthday-cake', 'bitbucket', 'bitbucket-square', 'bitcoin', 'black-tie', 'blind', 'bluetooth', 'bluetooth-b', 'bold', 'bolt', 'bomb', 'book', 'bookmark', 'bookmark-o', 'braille', 'briefcase', 'bug', 'building', 'building-o', 'bullhorn', 'bullseye', 'bus', 'buysellads', 'cab', 'calculator', 'calendar', 'calendar-check-o', 'calendar-minus-o', 'calendar-o', 'calendar-plus-o', 'calendar-times-o', 'camera', 'camera-retro', 'caret-down', 'caret-left', 'caret-square-o-left', 'caret-square-o-up', 'caret-up', 'cart-arrow-down', 'cart-plus', 'cc', 'cc-amex', 'cc-diners-club', 'cc-discover', 'cc-jcb', 'cc-mastercard', 'cc-paypal', 'cc-stripe', 'cc-visa', 'certificate', 'chain', 'chain-broken', 'check', 'check-circle', 'check-circle-o', 'check-square', 'check-square-o', 'chevron-circle-left', 'chevron-circle-up', 'chevron-down', 'chevron-left', 'chevron-up', 'child', 'chrome', 'circle', 'circle-o', 'circle-o-notch', 'circle-thin', 'clipboard', 'clock-o', 'clone', 'close', 'cloud', 'cloud-download', 'cloud-upload', 'cny', 'code', 'code-fork', 'codepen', 'codiepie', 'coffee', 'cog', 'cogs', 'columns', 'comment', 'comment-o', 'commenting', 'commenting-o', 'comments', 'comments-o', 'compass', 'compress', 'connectdevelop', 'contao', 'copy', 'copyright', 'creative-commons', 'credit-card', 'credit-card-alt', 'crop', 'crosshairs', 'css3', 'cube', 'cubes', 'cut', 'cutlery', 'dashboard', 'dashcube', 'database', 'deaf', 'dedent', 'delicious', 'desktop', 'deviantart', 'diamond', 'digg', 'dollar', 'dot-circle-o', 'download', 'dribbble', 'drivers-license', 'drivers-license-o', 'dropbox', 'drupal', 'edge', 'edit', 'eercast', 'eject', 'ellipsis-h', 'ellipsis-v', 'empire', 'envelope', 'envelope-o', 'envelope-open', 'envelope-open-o', 'envelope-square', 'envira', 'eraser', 'etsy', 'eur', 'exchange', 'exclamation', 'exclamation-circle', 'exclamation-triangle', 'expand', 'expeditedssl', 'external-link', 'external-link-square', 'eye', 'eye-slash', 'eyedropper', 'fa', 'facebook', 'facebook-official', 'facebook-square', 'fast-backward', 'fax', 'feed', 'female', 'fighter-jet', 'file', 'file-archive-o', 'file-audio-o', 'file-code-o', 'file-excel-o', 'file-image-o', 'file-movie-o', 'file-o', 'file-pdf-o', 'file-powerpoint-o', 'file-text', 'file-text-o', 'file-word-o', 'film', 'filter', 'fire', 'fire-extinguisher', 'firefox', 'first-order', 'flag', 'flag-checkered', 'flag-o', 'flask', 'flickr', 'floppy-o', 'folder', 'folder-o', 'folder-open', 'folder-open-o', 'font', 'fonticons', 'fort-awesome', 'forumbee', 'foursquare', 'free-code-camp', 'frown-o', 'futbol-o', 'gamepad', 'gavel', 'gbp', 'genderless', 'get-pocket', 'gg', 'gg-circle', 'gift', 'git', 'git-square', 'github', 'github-alt', 'github-square', 'gitlab', 'gittip', 'glass', 'glide', 'glide-g', 'globe', 'google', 'google-plus', 'google-plus-circle', 'google-plus-square', 'google-wallet', 'graduation-cap', 'grav', 'group', 'h-square', 'hacker-news', 'hand-grab-o', 'hand-lizard-o', 'hand-o-left', 'hand-o-up', 'hand-paper-o', 'hand-peace-o', 'hand-pointer-o', 'hand-scissors-o', 'hand-spock-o', 'handshake-o', 'hashtag', 'hdd-o', 'header', 'headphones', 'heart', 'heart-o', 'heartbeat', 'history', 'home', 'hospital-o', 'hourglass', 'hourglass-1', 'hourglass-2', 'hourglass-3', 'hourglass-o', 'houzz', 'html5', 'i-cursor', 'id-badge', 'ils', 'image', 'imdb', 'inbox', 'indent', 'industry', 'info', 'info-circle', 'inr', 'instagram', 'internet-explorer', 'intersex', 'ioxhost', 'italic', 'joomla', 'jsfiddle', 'key', 'keyboard-o', 'krw', 'language', 'laptop', 'lastfm', 'lastfm-square', 'leaf', 'leanpub', 'lemon-o', 'level-up', 'life-bouy', 'lightbulb-o', 'line-chart', 'linkedin', 'linkedin-square', 'linode', 'linux', 'list', 'list-alt', 'list-ol', 'list-ul', 'location-arrow', 'lock', 'long-arrow-left', 'long-arrow-up', 'low-vision', 'magic', 'magnet', 'mail-forward', 'mail-reply', 'mail-reply-all', 'male', 'map', 'map-marker', 'map-o', 'map-pin', 'map-signs', 'mars', 'mars-double', 'mars-stroke', 'mars-stroke-h', 'mars-stroke-v', 'maxcdn', 'meanpath', 'medium', 'medkit', 'meetup', 'meh-o', 'mercury', 'microchip', 'microphone', 'microphone-slash', 'minus', 'minus-circle', 'minus-square', 'minus-square-o', 'mixcloud', 'mobile', 'modx', 'money', 'moon-o', 'motorcycle', 'mouse-pointer', 'music', 'neuter', 'newspaper-o', 'object-group', 'object-ungroup', 'odnoklassniki', 'odnoklassniki-square', 'opencart', 'openid', 'opera', 'optin-monster', 'pagelines', 'paint-brush', 'paper-plane', 'paper-plane-o', 'paperclip', 'paragraph', 'pause', 'pause-circle', 'pause-circle-o', 'paw', 'paypal', 'pencil', 'pencil-square', 'percent', 'phone', 'phone-square', 'pie-chart', 'pied-piper', 'pied-piper-alt', 'pied-piper-pp', 'pinterest', 'pinterest-p', 'pinterest-square', 'plane', 'play', 'play-circle', 'play-circle-o', 'plug', 'plus', 'plus-circle', 'plus-square', 'plus-square-o', 'podcast', 'power-off', 'print', 'product-hunt', 'puzzle-piece', 'qq', 'qrcode', 'question', 'question-circle', 'question-circle-o', 'quora', 'quote-left', 'quote-right', 'ra', 'random', 'ravelry', 'recycle', 'reddit', 'reddit-alien', 'reddit-square', 'refresh', 'registered', 'renren', 'repeat', 'retweet', 'road', 'rocket', 'rotate-left', 'rouble', 'rss-square', 'safari', 'scribd', 'search', 'search-minus', 'search-plus', 'sellsy', 'server', 'share-alt', 'share-alt-square', 'share-square', 'share-square-o', 'shield', 'ship', 'shirtsinbulk', 'shopping-bag', 'shopping-basket', 'shopping-cart', 'shower', 'sign-in', 'sign-language', 'sign-out', 'signal', 'simplybuilt', 'sitemap', 'skyatlas', 'skype', 'slack', 'sliders', 'slideshare', 'smile-o', 'snapchat', 'snapchat-ghost', 'snapchat-square', 'snowflake-o', 'sort', 'sort-alpha-asc', 'sort-alpha-desc', 'sort-amount-asc', 'sort-amount-desc', 'sort-asc', 'sort-numeric-asc', 'sort-numeric-desc', 'soundcloud', 'space-shuttle', 'spinner', 'spoon', 'spotify', 'square', 'square-o', 'stack-exchange', 'stack-overflow', 'star', 'star-half', 'star-half-empty', 'star-o', 'steam', 'steam-square', 'step-backward', 'stethoscope', 'sticky-note', 'sticky-note-o', 'stop', 'stop-circle', 'stop-circle-o', 'street-view', 'strikethrough', 'stumbleupon', 'stumbleupon-circle', 'subscript', 'subway', 'suitcase', 'sun-o', 'superpowers', 'superscript', 'table', 'tablet', 'tag', 'tags', 'tasks', 'telegram', 'television', 'tencent-weibo', 'terminal', 'text-height', 'text-width', 'th', 'th-large', 'th-list', 'themeisle', 'thermometer', 'thermometer-0', 'thermometer-1', 'thermometer-2', 'thermometer-3', 'thumb-tack', 'thumbs-down', 'thumbs-o-up', 'thumbs-up', 'ticket', 'times-circle', 'times-circle-o', 'times-rectangle', 'times-rectangle-o', 'tint', 'toggle-off', 'toggle-on', 'trademark', 'train', 'transgender-alt', 'trash', 'trash-o', 'tree', 'trello', 'tripadvisor', 'trophy', 'truck', 'try', 'tty', 'tumblr', 'tumblr-square', 'twitch', 'twitter', 'twitter-square', 'umbrella', 'underline', 'universal-access', 'unlock', 'unlock-alt', 'upload', 'usb', 'user', 'user-circle', 'user-circle-o', 'user-md', 'user-o', 'user-plus', 'user-secret', 'user-times', 'venus', 'venus-double', 'venus-mars', 'viacoin', 'viadeo', 'viadeo-square', 'video-camera', 'vimeo', 'vimeo-square', 'vine', 'vk', 'volume-control-phone', 'volume-down', 'volume-off', 'volume-up', 'wechat', 'weibo', 'whatsapp', 'wheelchair', 'wheelchair-alt', 'wifi', 'wikipedia-w', 'window-maximize', 'window-minimize', 'window-restore', 'windows', 'wordpress', 'wpbeginner', 'wpexplorer', 'wpforms', 'wrench', 'xing', 'xing-square', 'y-combinator', 'yahoo', 'yelp', 'yoast', 'youtube', 'youtube-play', 'youtube-square' ], 'fa-solid:': [ 'abacus', 'ad', 'address-book', 'address-card', 'adjust', 'air-freshener', 'align-center', 'align-justify', 'align-left', 'align-right', 'allergies', 'ambulance', 'american-sign-language-interpreting', 'anchor', 'angle-double-down', 'angle-double-left', 'angle-double-right', 'angle-double-up', 'angle-down', 'angle-left', 'angle-right', 'angle-up', 'angry', 'ankh', 'apple-alt', 'archive', 'archway', 'arrow-alt-circle-down', 'arrow-alt-circle-left', 'arrow-alt-circle-right', 'arrow-alt-circle-up', 'arrow-circle-down', 'arrow-circle-left', 'arrow-circle-right', 'arrow-circle-up', 'arrow-down', 'arrow-left', 'arrow-right', 'arrow-up', 'arrows-alt', 'arrows-alt-h', 'arrows-alt-v', 'assistive-listening-systems', 'asterisk', 'at', 'atlas', 'atom', 'audio-description', 'award', 'baby', 'baby-carriage', 'backspace', 'backward', 'bacon', 'bacteria', 'bacterium', 'bahai', 'balance-scale', 'balance-scale-left', 'balance-scale-right', 'ban', 'band-aid', 'barcode', 'bars', 'baseball-ball', 'basketball-ball', 'bath', 'battery-empty', 'battery-full', 'battery-half', 'battery-quarter', 'battery-three-quarters', 'bed', 'beer', 'bell', 'bell-slash', 'bezier-curve', 'bible', 'bicycle', 'biking', 'binoculars', 'biohazard', 'birthday-cake', 'blender', 'blender-phone', 'blind', 'blog', 'bold', 'bolt', 'bomb', 'bone', 'bong', 'book', 'book-dead', 'book-medical', 'book-open', 'book-reader', 'bookmark', 'border-all', 'border-none', 'border-style', 'bowling-ball', 'box', 'box-open', 'box-tissue', 'boxes', 'braille', 'brain', 'bread-slice', 'briefcase', 'briefcase-medical', 'broadcast-tower', 'broom', 'brush', 'bug', 'building', 'bullhorn', 'bullseye', 'burn', 'bus', 'bus-alt', 'business-time', 'calculator', 'calculator-alt', 'calendar', 'calendar-alt', 'calendar-check', 'calendar-day', 'calendar-minus', 'calendar-plus', 'calendar-times', 'calendar-week', 'camera', 'camera-retro', 'campground', 'candy-cane', 'cannabis', 'capsules', 'car', 'car-alt', 'car-battery', 'car-crash', 'car-side', 'caravan', 'caret-down', 'caret-left', 'caret-right', 'caret-square-down', 'caret-square-left', 'caret-square-right', 'caret-square-up', 'caret-up', 'carrot', 'cart-arrow-down', 'cart-plus', 'cash-register', 'cat', 'certificate', 'chair', 'chalkboard', 'chalkboard-teacher', 'charging-station', 'chart-area', 'chart-bar', 'chart-line', 'chart-pie', 'check', 'check-circle', 'check-double', 'check-square', 'cheese', 'chess', 'chess-bishop', 'chess-board', 'chess-king', 'chess-knight', 'chess-pawn', 'chess-queen', 'chess-rook', 'chevron-circle-down', 'chevron-circle-left', 'chevron-circle-right', 'chevron-circle-up', 'chevron-down', 'chevron-left', 'chevron-right', 'chevron-up', 'child', 'church', 'circle', 'circle-notch', 'city', 'clinic-medical', 'clipboard', 'clipboard-check', 'clipboard-list', 'clock', 'clone', 'closed-captioning', 'cloud', 'cloud-download-alt', 'cloud-meatball', 'cloud-moon', 'cloud-moon-rain', 'cloud-rain', 'cloud-showers-heavy', 'cloud-sun', 'cloud-sun-rain', 'cloud-upload-alt', 'cocktail', 'code', 'code-branch', 'coffee', 'cog', 'cogs', 'coins', 'columns', 'comment', 'comment-alt', 'comment-dollar', 'comment-dots', 'comment-medical', 'comment-slash', 'comments', 'comments-dollar', 'compact-disc', 'compass', 'compress', 'compress-alt', 'compress-arrows-alt', 'concierge-bell', 'cookie', 'cookie-bite', 'copy', 'copyright', 'couch', 'credit-card', 'crop', 'crop-alt', 'cross', 'crosshairs', 'crow', 'crown', 'crutch', 'cube', 'cubes', 'cut', 'database', 'deaf', 'democrat', 'desktop', 'dharmachakra', 'diagnoses', 'dice', 'dice-d20', 'dice-d6', 'dice-five', 'dice-four', 'dice-one', 'dice-six', 'dice-three', 'dice-two', 'digital-tachograph', 'directions', 'disease', 'divide', 'dizzy', 'dna', 'dog', 'dollar-sign', 'dolly', 'dolly-flatbed', 'donate', 'door-closed', 'door-open', 'dot-circle', 'dove', 'download', 'drafting-compass', 'dragon', 'draw-polygon', 'drum', 'drum-steelpan', 'drumstick-bite', 'dumbbell', 'dumpster', 'dumpster-fire', 'dungeon', 'edit', 'egg', 'eject', 'ellipsis-h', 'ellipsis-v', 'empty-set', 'envelope', 'envelope-open', 'envelope-open-text', 'envelope-square', 'equals', 'eraser', 'ethernet', 'euro-sign', 'exchange-alt', 'exclamation', 'exclamation-circle', 'exclamation-triangle', 'expand', 'expand-alt', 'expand-arrows-alt', 'external-link-alt', 'external-link-square-alt', 'eye', 'eye-dropper', 'eye-slash', 'fan', 'fast-backward', 'fast-forward', 'faucet', 'fax', 'feather', 'feather-alt', 'female', 'fighter-jet', 'file', 'file-alt', 'file-archive', 'file-audio', 'file-code', 'file-contract', 'file-csv', 'file-download', 'file-excel', 'file-export', 'file-image', 'file-import', 'file-invoice', 'file-invoice-dollar', 'file-medical', 'file-medical-alt', 'file-pdf', 'file-powerpoint', 'file-prescription', 'file-signature', 'file-upload', 'file-video', 'file-word', 'fill', 'fill-drip', 'film', 'filter', 'fingerprint', 'fire', 'fire-alt', 'fire-extinguisher', 'first-aid', 'fish', 'fist-raised', 'flag', 'flag-checkered', 'flag-usa', 'flask', 'flushed', 'folder', 'folder-minus', 'folder-open', 'folder-plus', 'font', 'football-ball', 'forward', 'frog', 'frown', 'frown-open', 'function', 'funnel-dollar', 'futbol', 'gamepad', 'gas-pump', 'gavel', 'gem', 'genderless', 'ghost', 'gift', 'gifts', 'glass-cheers', 'glass-martini', 'glass-martini-alt', 'glass-whiskey', 'glasses', 'globe', 'globe-africa', 'globe-americas', 'globe-asia', 'globe-europe', 'golf-ball', 'gopuram', 'graduation-cap', 'greater-than', 'greater-than-equal', 'grimace', 'grin', 'grin-alt', 'grin-beam', 'grin-beam-sweat', 'grin-hearts', 'grin-squint', 'grin-squint-tears', 'grin-stars', 'grin-tears', 'grin-tongue', 'grin-tongue-squint', 'grin-tongue-wink', 'grin-wink', 'grip-horizontal', 'grip-lines', 'grip-lines-vertical', 'grip-vertical', 'guitar', 'h-square', 'hamburger', 'hammer', 'hamsa', 'hand-holding', 'hand-holding-heart', 'hand-holding-medical', 'hand-holding-usd', 'hand-holding-water', 'hand-lizard', 'hand-middle-finger', 'hand-paper', 'hand-peace', 'hand-point-down', 'hand-point-left', 'hand-point-right', 'hand-point-up', 'hand-pointer', 'hand-rock', 'hand-scissors', 'hand-sparkles', 'hand-spock', 'hands', 'hands-helping', 'hands-wash', 'handshake', 'handshake-alt-slash', 'handshake-slash', 'hanukiah', 'hard-hat', 'hashtag', 'hat-cowboy', 'hat-cowboy-side', 'hat-wizard', 'hdd', 'head-side-cough', 'head-side-cough-slash', 'head-side-mask', 'head-side-virus', 'heading', 'headphones', 'headphones-alt', 'headset', 'heart', 'heart-broken', 'heartbeat', 'helicopter', 'highlighter', 'hiking', 'hippo', 'history', 'hockey-puck', 'holly-berry', 'home', 'horse', 'horse-head', 'hospital', 'hospital-alt', 'hospital-symbol', 'hospital-user', 'hot-tub', 'hotdog', 'hotel', 'hourglass', 'hourglass-end', 'hourglass-half', 'hourglass-start', 'house-damage', 'house-user', 'hryvnia', 'i-cursor', 'ice-cream', 'icicles', 'icons', 'id-badge', 'id-card', 'id-card-alt', 'igloo', 'image', 'images', 'inbox', 'indent', 'industry', 'infinity', 'info', 'info-circle', 'integral', 'intersection', 'italic', 'jedi', 'joint', 'journal-whills', 'kaaba', 'key', 'keyboard', 'khanda', 'kiss', 'kiss-beam', 'kiss-wink-heart', 'kiwi-bird', 'lambda', 'landmark', 'language', 'laptop', 'laptop-code', 'laptop-house', 'laptop-medical', 'laugh', 'laugh-beam', 'laugh-squint', 'laugh-wink', 'layer-group', 'leaf', 'lemon', 'less-than', 'less-than-equal', 'level-down-alt', 'level-up-alt', 'life-ring', 'lightbulb', 'link', 'lira-sign', 'list', 'list-alt', 'list-ol', 'list-ul', 'location-arrow', 'lock', 'lock-open', 'long-arrow-alt-down', 'long-arrow-alt-left', 'long-arrow-alt-right', 'long-arrow-alt-up', 'low-vision', 'luggage-cart', 'lungs', 'lungs-virus', 'magic', 'magnet', 'mail-bulk', 'male', 'map', 'map-marked', 'map-marked-alt', 'map-marker', 'map-marker-alt', 'map-pin', 'map-signs', 'marker', 'mars', 'mars-double', 'mars-stroke', 'mars-stroke-h', 'mars-stroke-v', 'mask', 'medal', 'medkit', 'meh', 'meh-blank', 'meh-rolling-eyes', 'memory', 'menorah', 'mercury', 'meteor', 'microchip', 'microphone', 'microphone-alt', 'microphone-alt-slash', 'microphone-slash', 'microscope', 'minus', 'minus-circle', 'minus-square', 'mitten', 'mobile', 'mobile-alt', 'money-bill', 'money-bill-alt', 'money-bill-wave', 'money-bill-wave-alt', 'money-check', 'money-check-alt', 'monument', 'moon', 'mortar-pestle', 'mosque', 'motorcycle', 'mountain', 'mouse', 'mouse-pointer', 'mug-hot', 'music', 'network-wired', 'neuter', 'newspaper', 'not-equal', 'notes-medical', 'object-group', 'object-ungroup', 'oil-can', 'om', 'omega', 'otter', 'outdent', 'pager', 'paint-brush', 'paint-roller', 'palette', 'pallet', 'paper-plane', 'paperclip', 'parachute-box', 'paragraph', 'parking', 'passport', 'pastafarianism', 'paste', 'pause', 'pause-circle', 'paw', 'peace', 'pen', 'pen-alt', 'pen-fancy', 'pen-nib', 'pen-square', 'pencil-alt', 'pencil-ruler', 'people-arrows', 'people-carry', 'pepper-hot', 'percent', 'percentage', 'person-booth', 'phone', 'phone-alt', 'phone-slash', 'phone-square', 'phone-square-alt', 'phone-volume', 'photo-video', 'pi', 'piggy-bank', 'pills', 'pizza-slice', 'place-of-worship', 'plane', 'plane-arrival', 'plane-departure', 'plane-slash', 'play', 'play-circle', 'plug', 'plus', 'plus-circle', 'plus-square', 'podcast', 'poll', 'poll-h', 'poo', 'poo-storm', 'poop', 'portrait', 'pound-sign', 'power-off', 'pray', 'praying-hands', 'prescription', 'prescription-bottle', 'prescription-bottle-alt', 'print', 'procedures', 'project-diagram', 'pump-medical', 'pump-soap', 'puzzle-piece', 'qrcode', 'question', 'question-circle', 'quidditch', 'quote-left', 'quote-right', 'quran', 'radiation', 'radiation-alt', 'rainbow', 'random', 'receipt', 'record-vinyl', 'recycle', 'redo', 'redo-alt', 'registered', 'remove-format', 'reply', 'reply-all', 'republican', 'restroom', 'retweet', 'ribbon', 'ring', 'road', 'robot', 'rocket', 'route', 'rss', 'rss-square', 'ruble-sign', 'ruler', 'ruler-combined', 'ruler-horizontal', 'ruler-vertical', 'running', 'rupee-sign', 'sad-cry', 'sad-tear', 'satellite', 'satellite-dish', 'save', 'school', 'screwdriver', 'scroll', 'sd-card', 'search', 'search-dollar', 'search-location', 'search-minus', 'search-plus', 'seedling', 'server', 'shapes', 'share', 'share-alt', 'share-alt-square', 'share-square', 'shekel-sign', 'shield-alt', 'shield-virus', 'ship', 'shipping-fast', 'shoe-prints', 'shopping-bag', 'shopping-basket', 'shopping-cart', 'shower', 'shuttle-van', 'sigma', 'sign', 'sign-in-alt', 'sign-language', 'sign-out-alt', 'signal', 'signal-alt', 'signal-alt-slash', 'signal-slash', 'signature', 'sim-card', 'sink', 'sitemap', 'skating', 'skiing', 'skiing-nordic', 'skull', 'skull-crossbones', 'slash', 'sleigh', 'sliders-h', 'smile', 'smile-beam', 'smile-wink', 'smog', 'smoking', 'smoking-ban', 'sms', 'snowboarding', 'snowflake', 'snowman', 'snowplow', 'soap', 'socks', 'solar-panel', 'sort', 'sort-alpha-down', 'sort-alpha-down-alt', 'sort-alpha-up', 'sort-alpha-up-alt', 'sort-amount-down', 'sort-amount-down-alt', 'sort-amount-up', 'sort-amount-up-alt', 'sort-down', 'sort-numeric-down', 'sort-numeric-down-alt', 'sort-numeric-up', 'sort-numeric-up-alt', 'sort-up', 'spa', 'space-shuttle', 'spell-check', 'spider', 'spinner', 'splotch', 'spray-can', 'square', 'square-full', 'square-root', 'square-root-alt', 'stamp', 'star', 'star-and-crescent', 'star-half', 'star-half-alt', 'star-of-david', 'star-of-life', 'step-backward', 'step-forward', 'stethoscope', 'sticky-note', 'stop', 'stop-circle', 'stopwatch', 'stopwatch-20', 'store', 'store-alt', 'store-alt-slash', 'store-slash', 'stream', 'street-view', 'strikethrough', 'stroopwafel', 'subscript', 'subway', 'suitcase', 'suitcase-rolling', 'sun', 'superscript', 'surprise', 'swatchbook', 'swimmer', 'swimming-pool', 'synagogue', 'sync', 'sync-alt', 'syringe', 'table', 'table-tennis', 'tablet', 'tablet-alt', 'tablets', 'tachometer-alt', 'tag', 'tags', 'tally', 'tape', 'tasks', 'taxi', 'teeth', 'teeth-open', 'temperature-high', 'temperature-low', 'tenge', 'terminal', 'text-height', 'text-width', 'th', 'th-large', 'th-list', 'theater-masks', 'thermometer', 'thermometer-empty', 'thermometer-full', 'thermometer-half', 'thermometer-quarter', 'thermometer-three-quarters', 'theta', 'thumbs-down', 'thumbs-up', 'thumbtack', 'ticket-alt', 'tilde', 'times', 'times-circle', 'tint', 'tint-slash', 'tired', 'toggle-off', 'toggle-on', 'toilet', 'toilet-paper', 'toilet-paper-slash', 'toolbox', 'tools', 'tooth', 'torah', 'torii-gate', 'tractor', 'trademark', 'traffic-light', 'trailer', 'train', 'tram', 'transgender', 'transgender-alt', 'trash', 'trash-alt', 'trash-restore', 'trash-restore-alt', 'tree', 'trophy', 'truck', 'truck-loading', 'truck-monster', 'truck-moving', 'truck-pickup', 'tshirt', 'tty', 'tv', 'umbrella', 'umbrella-beach', 'underline', 'undo', 'undo-alt', 'union', 'universal-access', 'university', 'unlink', 'unlock', 'unlock-alt', 'upload', 'user', 'user-alt', 'user-alt-slash', 'user-astronaut', 'user-check', 'user-circle', 'user-clock', 'user-cog', 'user-edit', 'user-friends', 'user-graduate', 'user-injured', 'user-lock', 'user-md', 'user-minus', 'user-ninja', 'user-nurse', 'user-plus', 'user-secret', 'user-shield', 'user-slash', 'user-tag', 'user-tie', 'user-times', 'users', 'users-cog', 'users-slash', 'utensil-spoon', 'utensils', 'value-absolute', 'vector-square', 'venus', 'venus-double', 'venus-mars', 'vest', 'vest-patches', 'vial', 'vials', 'video', 'video-slash', 'vihara', 'virus', 'virus-slash', 'viruses', 'voicemail', 'volleyball-ball', 'volume', 'volume-down', 'volume-mute', 'volume-off', 'volume-slash', 'volume-up', 'vote-yea', 'vr-cardboard', 'walking', 'wallet', 'warehouse', 'water', 'wave-square', 'weight', 'weight-hanging', 'wheelchair', 'wifi', 'wifi-slash', 'wind', 'window-close', 'window-maximize', 'window-minimize', 'window-restore', 'wine-bottle', 'wine-glass', 'wine-glass-alt', 'won-sign', 'wrench', 'x-ray', 'yen-sign', 'yin-yang' ] } ================================================ FILE: yshop-drink-vue3/src/components/ImageViewer/index.ts ================================================ import ImageViewer from './src/ImageViewer.vue' import { isClient } from '@/utils/is' import { createVNode, render, VNode } from 'vue' import { ImageViewerProps } from './src/types' let instance: Nullable = null export function createImageViewer(options: ImageViewerProps) { if (!isClient) return const { urlList, initialIndex = 0, infinite = true, hideOnClickModal = false, teleported = false, zIndex = 2000, show = true } = options const propsData: Partial = {} const container = document.createElement('div') propsData.urlList = urlList propsData.initialIndex = initialIndex propsData.infinite = infinite propsData.hideOnClickModal = hideOnClickModal propsData.teleported = teleported propsData.zIndex = zIndex propsData.show = show document.body.appendChild(container) instance = createVNode(ImageViewer, propsData) render(instance, container) } ================================================ FILE: yshop-drink-vue3/src/components/ImageViewer/src/ImageViewer.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/ImageViewer/src/types.ts ================================================ export interface ImageViewerProps { urlList?: string[] zIndex?: number initialIndex?: number infinite?: boolean hideOnClickModal?: boolean teleported?: boolean show?: boolean } ================================================ FILE: yshop-drink-vue3/src/components/Infotip/index.ts ================================================ import Infotip from './src/Infotip.vue' export { Infotip } ================================================ FILE: yshop-drink-vue3/src/components/Infotip/src/Infotip.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/InputPassword/index.ts ================================================ import InputPassword from './src/InputPassword.vue' export { InputPassword } ================================================ FILE: yshop-drink-vue3/src/components/InputPassword/src/InputPassword.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/InputWithColor/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/MagicCubeEditor/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/MagicCubeEditor/util.ts ================================================ // 坐标点 export interface Point { x: number y: number } // 矩形 export interface Rect { // 左上角 X 轴坐标 left: number // 左上角 Y 轴坐标 top: number // 右下角 X 轴坐标 right: number // 右下角 Y 轴坐标 bottom: number // 矩形宽度 width: number // 矩形高度 height: number } /** * 判断两个矩形是否重叠 * @param a 矩形 A * @param b 矩形 B */ export const isOverlap = (a: Rect, b: Rect): boolean => { return ( a.left < b.left + b.width && a.left + a.width > b.left && a.top < b.top + b.height && a.height + a.top > b.top ) } /** * 检查坐标点是否在矩形内 * @param hotArea 矩形 * @param point 坐标 */ export const isContains = (hotArea: Rect, point: Point): boolean => { return ( point.x >= hotArea.left && point.x < hotArea.right && point.y >= hotArea.top && point.y < hotArea.bottom ) } /** * 在两个坐标点中间,创建一个矩形 * * 存在以下情况: * 1. 两个坐标点是同一个位置,只占一个位置的正方形,宽高都为 1 * 2. X 轴坐标相同,只占一行的矩形,高度为 1 * 3. Y 轴坐标相同,只占一列的矩形,宽度为 1 * 4. 多行多列的矩形 * * @param a 坐标点一 * @param b 坐标点二 */ export const createRect = (a: Point, b: Point): Rect => { // 计算矩形的范围 const [left, left2] = [a.x, b.x].sort() const [top, top2] = [a.y, b.y].sort() const right = left2 + 1 const bottom = top2 + 1 const height = bottom - top const width = right - left return { left, right, top, bottom, height, width } } ================================================ FILE: yshop-drink-vue3/src/components/Materials/index.ts ================================================ /* * @Author: Gaoxs * @Date: 2023-05-21 23:36:03 * @LastEditors: Gaoxs * @Description: */ import Materials from './src/Materials.vue' import EditorMaterials from './src/EditorMaterials.vue' export { Materials, EditorMaterials } ================================================ FILE: yshop-drink-vue3/src/components/Materials/src/Materials.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Materials/src/editorMaterials.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/OperateLogV2/index.ts ================================================ import OperateLogV2 from './src/OperateLogV2.vue' export { OperateLogV2 } ================================================ FILE: yshop-drink-vue3/src/components/OperateLogV2/src/OperateLogV2.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Pagination/index.vue ================================================ ,并使用 ts 重写 --> ================================================ FILE: yshop-drink-vue3/src/components/Qrcode/index.ts ================================================ import Qrcode from './src/Qrcode.vue' export { Qrcode } ================================================ FILE: yshop-drink-vue3/src/components/Qrcode/src/Qrcode.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/RouterSearch/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Search/index.ts ================================================ import Search from './src/Search.vue' export { Search } ================================================ FILE: yshop-drink-vue3/src/components/Search/src/Search.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/ShortcutDateRangePicker/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/SimpleProcessDesigner/src/addNode.vue ================================================ /* stylelint-disable order/properties-order */ ================================================ FILE: yshop-drink-vue3/src/components/SimpleProcessDesigner/src/nodeWrap.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/SimpleProcessDesigner/src/util.ts ================================================ /** * todo */ export const arrToStr = (arr?: [{ name: string }]) => { if (arr) { return arr .map((item) => { return item.name }) .toString() } } export const setApproverStr = (nodeConfig: any) => { if (nodeConfig.settype == 1) { if (nodeConfig.nodeUserList.length == 1) { return nodeConfig.nodeUserList[0].name } else if (nodeConfig.nodeUserList.length > 1) { if (nodeConfig.examineMode == 1) { return arrToStr(nodeConfig.nodeUserList) } else if (nodeConfig.examineMode == 2) { return nodeConfig.nodeUserList.length + '人会签' } } } else if (nodeConfig.settype == 2) { const level = nodeConfig.directorLevel == 1 ? '直接主管' : '第' + nodeConfig.directorLevel + '级主管' if (nodeConfig.examineMode == 1) { return level } else if (nodeConfig.examineMode == 2) { return level + '会签' } } else if (nodeConfig.settype == 4) { if (nodeConfig.selectRange == 1) { return '发起人自选' } else { if (nodeConfig.nodeUserList.length > 0) { if (nodeConfig.selectRange == 2) { return '发起人自选' } else { return '发起人从' + nodeConfig.nodeUserList[0].name + '中自选' } } else { return '' } } } else if (nodeConfig.settype == 5) { return '发起人自己' } else if (nodeConfig.settype == 7) { return '从直接主管到通讯录中级别最高的第' + nodeConfig.examineEndDirectorLevel + '个层级主管' } } export const copyerStr = (nodeConfig: any) => { if (nodeConfig.nodeUserList.length != 0) { return arrToStr(nodeConfig.nodeUserList) } else { if (nodeConfig.ccSelfSelectFlag == 1) { return '发起人自选' } } } export const conditionStr = (nodeConfig, index) => { const { conditionList, nodeUserList } = nodeConfig.conditionNodes[index] if (conditionList.length == 0) { return index == nodeConfig.conditionNodes.length - 1 && nodeConfig.conditionNodes[0].conditionList.length != 0 ? '其他条件进入此流程' : '请设置条件' } else { let str = '' for (let i = 0; i < conditionList.length; i++) { const { columnId, columnType, showType, showName, optType, zdy1, opt1, zdy2, opt2, fixedDownBoxValue } = conditionList[i] if (columnId == 0) { if (nodeUserList.length != 0) { str += '发起人属于:' str += nodeUserList .map((item) => { return item.name }) .join('或') + ' 并且 ' } } if (columnType == 'String' && showType == '3') { if (zdy1) { str += showName + '属于:' + dealStr(zdy1, JSON.parse(fixedDownBoxValue)) + ' 并且 ' } } if (columnType == 'Double') { if (optType != 6 && zdy1) { const optTypeStr = ['', '<', '>', '≤', '=', '≥'][optType] str += `${showName} ${optTypeStr} ${zdy1} 并且 ` } else if (optType == 6 && zdy1 && zdy2) { str += `${zdy1} ${opt1} ${showName} ${opt2} ${zdy2} 并且 ` } } } return str ? str.substring(0, str.length - 4) : '请设置条件' } } export const dealStr = (str: string, obj) => { const arr = [] const list = str.split(',') for (const elem in obj) { list.map((item) => { if (item == elem) { arr.push(obj[elem].value) } }) } return arr.join('或') } export const removeEle = (arr, elem, key = 'id') => { let includesIndex arr.map((item, index) => { if (item[key] == elem[key]) { includesIndex = index } }) arr.splice(includesIndex, 1) } export const bgColors = ['87, 106, 149', '255, 148, 62', '50, 150, 250'] export const placeholderList = ['发起人', '审核人', '抄送人'] export const setTypes = [ { value: 1, label: '指定成员' }, { value: 2, label: '主管' }, { value: 4, label: '发起人自选' }, { value: 5, label: '发起人自己' }, { value: 7, label: '连续多级主管' } ] export const selectModes = [ { value: 1, label: '选一个人' }, { value: 2, label: '选多个人' } ] export const selectRanges = [ { value: 1, label: '全公司' }, { value: 2, label: '指定成员' }, { value: 3, label: '指定角色' } ] export const optTypes = [ { value: '1', label: '小于' }, { value: '2', label: '大于' }, { value: '3', label: '小于等于' }, { value: '4', label: '等于' }, { value: '5', label: '大于等于' }, { value: '6', label: '介于两个数之间' } ] ================================================ FILE: yshop-drink-vue3/src/components/SimpleProcessDesigner/theme/workflow.css ================================================ .clearfix { zoom: 1 } .clearfix:after, .clearfix:before { content: ""; display: table } .clearfix:after { clear: both } @font-face { font-family: anticon; font-display: fallback; src: url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.eot"); src: url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.woff") format("woff"), url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.ttf") format("truetype"), url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.svg#iconfont") format("svg") } .anticon { display: inline-block; font-style: normal; vertical-align: baseline; text-align: center; text-transform: none; line-height: 1; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale } .anticon:before { display: block; font-family: anticon!important } .anticon-close:before { content: "\E633" } .anticon-right:before { content: "\E61F" } .anticon-exclamation-circle{ color: rgb(242, 86, 67) } .anticon-exclamation-circle:before { content: "\E62C" } .anticon-left:before { content: "\E620" } .anticon-close-circle:before { content: "\E62E" } .ant-btn { line-height: 1.5; display: inline-block; font-weight: 400; text-align: center; touch-action: manipulation; cursor: pointer; background-image: none; border: 1px solid transparent; white-space: nowrap; padding: 0 15px; font-size: 14px; border-radius: 4px; height: 32px; user-select: none; transition: all .3s cubic-bezier(.645, .045, .355, 1); position: relative; color: rgba(0, 0, 0, .65); background-color: #fff; border-color: #d9d9d9 } .ant-btn>.anticon { line-height: 1 } .ant-btn, .ant-btn:active, .ant-btn:focus { outline: 0 } .ant-btn>a:only-child { color: currentColor } .ant-btn>a:only-child:after { content: ""; position: absolute; top: 0; left: 0; bottom: 0; right: 0; background: transparent } .ant-btn:focus, .ant-btn:hover { color: #40a9ff; background-color: #fff; border-color: #40a9ff } .ant-btn:focus>a:only-child, .ant-btn:hover>a:only-child { color: currentColor } .ant-btn:focus>a:only-child:after, .ant-btn:hover>a:only-child:after { content: ""; position: absolute; top: 0; left: 0; bottom: 0; right: 0; background: transparent } .ant-btn.active, .ant-btn:active { color: #096dd9; background-color: #fff; border-color: #096dd9 } .ant-btn.active>a:only-child, .ant-btn:active>a:only-child { color: currentColor } .ant-btn.active>a:only-child:after, .ant-btn:active>a:only-child:after { content: ""; position: absolute; top: 0; left: 0; bottom: 0; right: 0; background: transparent } .ant-btn.active, .ant-btn:active, .ant-btn:focus, .ant-btn:hover { background: #fff; text-decoration: none } .ant-btn>i, .ant-btn>span { pointer-events: none } .ant-btn:before { position: absolute; top: -1px; left: -1px; bottom: -1px; right: -1px; background: #fff; opacity: .35; content: ""; border-radius: inherit; z-index: 1; transition: opacity .2s; pointer-events: none; display: none } .ant-btn .anticon { transition: margin-left .3s cubic-bezier(.645, .045, .355, 1) } .ant-btn:active>span, .ant-btn:focus>span { position: relative } .ant-btn>.anticon+span, .ant-btn>span+.anticon { margin-left: 8px } .ant-input { font-family: Chinese Quote, -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif; font-variant: tabular-nums; box-sizing: border-box; margin: 0; padding: 0; list-style: none; position: relative; display: inline-block; padding: 4px 11px; width: 100%; height: 32px; font-size: 14px; line-height: 1.5; color: rgba(0, 0, 0, .65); background-color: #fff; background-image: none; border: 1px solid #d9d9d9; border-radius: 4px; transition: all .3s } .ant-input::-moz-placeholder { color: #bfbfbf; opacity: 1 } .ant-input:-ms-input-placeholder { color: #bfbfbf } .ant-input::-webkit-input-placeholder { color: #bfbfbf } .ant-input:focus, .ant-input:hover { border-color: #40a9ff; border-right-width: 1px!important } .ant-input:focus { outline: 0; box-shadow: 0 0 0 2px rgba(24, 144, 255, .2) } textarea.ant-input { max-width: 100%; height: auto; vertical-align: bottom; transition: all .3s, height 0s; min-height: 32px } a, abbr, acronym, address, applet, article, aside, audio, b, big, blockquote, body, canvas, caption, center, cite, code, dd, del, details, dfn, div, dl, dt, em, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, html, i, iframe, img, ins, kbd, label, legend, li, mark, menu, nav, object, ol, p, pre, q, s, samp, section, small, span, strike, strong, sub, summary, sup, table, tbody, td, tfoot, th, thead, time, tr, tt, u, ul, var, video { margin: 0; padding: 0; border: 0; outline: 0; font-size: 100%; font: inherit; vertical-align: baseline } *, :after, :before { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box } html { font-family: sans-serif; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100% } body, html { font-size: 14px } body { font-family: Microsoft Yahei, Lucida Grande, Lucida Sans Unicode, Helvetica, Arial, Verdana, sans-serif; line-height: 1.6; background-color: #fff; position: static!important; -webkit-tap-highlight-color: rgba(0, 0, 0, 0) } ol, ul { list-style-type: none } b, strong { font-weight: 700 } img { border: 0 } button, input, select, textarea { font-family: inherit; font-size: 100%; margin: 0 } textarea { overflow: auto; vertical-align: top; -webkit-appearance: none } button, input { line-height: normal } button, select { text-transform: none } button, html input[type=button], input[type=reset], input[type=submit] { -webkit-appearance: button; cursor: pointer } input[type=search] { -webkit-appearance: textfield; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; box-sizing: content-box } input[type=search]::-webkit-search-cancel-button, input[type=search]::-webkit-search-decoration { -webkit-appearance: none } button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0 } table { width: 100%; border-spacing: 0; border-collapse: collapse } table, td, th { border: 0 } td, th { padding: 0; vertical-align: top } th { font-weight: 700; text-align: left } thead th { white-space: nowrap } a { text-decoration: none; cursor: pointer; color: #3296fa } a:active, a:hover { outline: 0; color: #3296fa } small { font-size: 80% } body, html { font-size: 12px!important; color: #191f25!important; background: #f6f6f6!important } .wrap { display: -webkit-box; display: -ms-flexbox; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -ms-flex-direction: column; flex-direction: column; height: 100% } @font-face { font-family: IconFont; src: url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.eot"); src: url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.woff") format("woff"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.ttf") format("truetype"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.svg#IconFont") format("svg") } .iconfont { font-family: IconFont!important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; -webkit-text-stroke-width: .2px; -moz-osx-font-smoothing: grayscale } .fd-nav { position: fixed; top: 0; left: 0; right: 0; z-index: 997; width: 100%; height: 60px; font-size: 14px; color: #fff; background: #3296fa; display: flex; align-items: center } .fd-nav>* { flex: 1; width: 100% } .fd-nav .fd-nav-left { display: -webkit-box; display: flex; align-items: center } .fd-nav .fd-nav-center { flex: none; width: 600px; text-align: center } .fd-nav .fd-nav-right { display: flex; align-items: center; justify-content: flex-end; text-align: right } .fd-nav .fd-nav-back { display: inline-block; width: 60px; height: 60px; font-size: 22px; border-right: 1px solid #1583f2; text-align: center; cursor: pointer } .fd-nav .fd-nav-back:hover { background: #5af } .fd-nav .fd-nav-back:active { background: #1583f2 } .fd-nav .fd-nav-back .anticon { line-height: 60px } .fd-nav .fd-nav-title { width: 0; flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; padding: 0 15px } .fd-nav a { color: #fff; margin-left: 12px } .fd-nav .button-publish { min-width: 80px; margin-left: 4px; margin-right: 15px; color: #3296fa; border-color: #fff } .fd-nav .button-publish.ant-btn:focus, .fd-nav .button-publish.ant-btn:hover { color: #3296fa; border-color: #fff; box-shadow: 0 10px 20px 0 rgba(0, 0, 0, .3) } .fd-nav .button-publish.ant-btn:active { color: #3296fa; background: #d6eaff; box-shadow: none } .fd-nav .button-preview { min-width: 80px; margin-left: 16px; margin-right: 4px; color: #fff; border-color: #fff; background: transparent } .fd-nav .button-preview.ant-btn:focus, .fd-nav .button-preview.ant-btn:hover { color: #fff; border-color: #fff; background: #59acfc } .fd-nav .button-preview.ant-btn:active { color: #fff; border-color: #fff; background: #2186ef } .fd-nav-content { position: fixed; top: 60px; left: 0; right: 0; bottom: 0; z-index: 1; overflow-x: hidden; overflow-y: auto; padding-bottom: 30px } .error-modal-desc { font-size: 13px; color: rgba(25, 31, 37, .56); line-height: 22px; margin-bottom: 14px } .error-modal-list { height: 200px; overflow-y: auto; margin-right: -25px; padding-right: 25px } .error-modal-item { padding: 10px 20px; line-height: 21px; background: #f6f6f6; display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; border-radius: 4px } .error-modal-item-label { flex: none; font-size: 15px; color: rgba(25, 31, 37, .56); padding-right: 10px } .error-modal-item-content { text-align: right; flex: 1; font-size: 13px; color: #191f25 } #body.blur { -webkit-filter: blur(3px); filter: blur(3px) } .zoom { display: flex; position: fixed; -webkit-box-align: center; -ms-flex-align: center; align-items: center; -webkit-box-pack: justify; -ms-flex-pack: justify; justify-content: space-between; height: 40px; width: 125px; right: 40px; margin-top: 30px; z-index: 10 } .zoom .zoom-in, .zoom .zoom-out { width: 30px; height: 30px; background: #fff; color: #c1c1cd; cursor: pointer; background-size: 100%; background-repeat: no-repeat } .zoom .zoom-out { background-image: url(https://gw.alicdn.com/tfs/TB1s0qhBHGYBuNjy0FoXXciBFXa-90-90.png) } .zoom .zoom-out.disabled { opacity: .5 } .zoom .zoom-in { background-image: url(https://gw.alicdn.com/tfs/TB1UIgJBTtYBeNjy1XdXXXXyVXa-90-90.png) } .zoom .zoom-in.disabled { opacity: .5 } .auto-judge:hover .editable-title, .node-wrap-box:hover .editable-title { border-bottom: 1px dashed #fff } .auto-judge:hover .editable-title.editing, .node-wrap-box:hover .editable-title.editing { text-decoration: none; border: 1px solid #d9d9d9 } .auto-judge:hover .editable-title { border-color: #15bc83 } .editable-title { line-height: 15px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; border-bottom: 1px dashed transparent } .editable-title:before { content: ""; position: absolute; top: 0; left: 0; bottom: 0; right: 40px } .editable-title:hover { border-bottom: 1px dashed #fff } .editable-title-input { flex: none; height: 18px; padding-left: 4px; text-indent: 0; font-size: 12px; line-height: 18px; z-index: 1 } .editable-title-input:hover { text-decoration: none } .ant-btn { position: relative } .node-wrap-box { display: -webkit-inline-box; display: -ms-inline-flexbox; display: inline-flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -ms-flex-direction: column; flex-direction: column; position: relative; width: 220px; min-height: 72px; -ms-flex-negative: 0; flex-shrink: 0; background: #fff; border-radius: 4px; cursor: pointer } .node-wrap-box:after { pointer-events: none; content: ""; position: absolute; top: 0; bottom: 0; left: 0; right: 0; z-index: 2; border-radius: 4px; border: 1px solid transparent; transition: all .1s cubic-bezier(.645, .045, .355, 1); box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1) } .node-wrap-box.active:after, .node-wrap-box:active:after, .node-wrap-box:hover:after { border: 1px solid #3296fa; box-shadow: 0 0 6px 0 rgba(50, 150, 250, .3) } .node-wrap-box.active .close, .node-wrap-box:active .close, .node-wrap-box:hover .close { display: block } .node-wrap-box.error:after { border: 1px solid #f25643; box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1) } .node-wrap-box .title { position: relative; display: flex; align-items: center; padding-left: 16px; padding-right: 30px; width: 100%; height: 24px; line-height: 24px; font-size: 12px; color: #fff; text-align: left; background: #576a95; border-radius: 4px 4px 0 0 } .node-wrap-box .title .iconfont { font-size: 12px; margin-right: 5px } .node-wrap-box .placeholder { color: #bfbfbf } .node-wrap-box .close { display: none; position: absolute; right: 10px; top: 50%; transform: translateY(-50%); width: 20px; height: 20px; font-size: 14px; color: #fff; border-radius: 50%; text-align: center; line-height: 20px } .node-wrap-box .content { position: relative; font-size: 14px; padding: 16px; padding-right: 30px } .node-wrap-box .content .text { overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical } .node-wrap-box .content .arrow { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); width: 20px; height: 14px; font-size: 14px; color: #979797 } .start-node.node-wrap-box .content .text { display: block; white-space: nowrap } .node-wrap-box:before { content: ""; position: absolute; top: -12px; left: 50%; -webkit-transform: translateX(-50%); transform: translateX(-50%); width: 0; height: 4px; border-style: solid; border-width: 8px 6px 4px; border-color: #cacaca transparent transparent; background: #f5f5f7 } .node-wrap-box.start-node:before { content: none } .top-left-cover-line { left: -1px } .top-left-cover-line, .top-right-cover-line { position: absolute; height: 8px; width: 50%; background-color: #f5f5f7; top: -4px } .top-right-cover-line { right: -1px } .bottom-left-cover-line { left: -1px } .bottom-left-cover-line, .bottom-right-cover-line { position: absolute; height: 8px; width: 50%; background-color: #f5f5f7; bottom: -4px } .bottom-right-cover-line { right: -1px } .dingflow-design { width: 100%; background-color: #f5f5f7; overflow: auto; position: absolute; bottom: 0; left: 0; right: 0; top: 0 } .dingflow-design .box-scale { transform: scale(1); display: inline-block; position: relative; width: 100%; padding: 54.5px 0; -webkit-box-align: start; -ms-flex-align: start; align-items: flex-start; -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; -ms-flex-wrap: wrap; flex-wrap: wrap; min-width: -webkit-min-content; min-width: -moz-min-content; min-width: min-content; background-color: #f5f5f7; transform-origin: 50% 0px 0px; } .dingflow-design .node-wrap { flex-direction: column; -webkit-box-pack: start; -ms-flex-pack: start; justify-content: flex-start; -webkit-box-align: center; -ms-flex-align: center; align-items: center; -ms-flex-wrap: wrap; flex-wrap: wrap; -webkit-box-flex: 1; -ms-flex-positive: 1; padding: 0 50px; position: relative } .dingflow-design .branch-wrap, .dingflow-design .node-wrap { display: inline-flex; width: 100% } .dingflow-design .branch-box-wrap { display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -ms-flex-direction: column; flex-direction: column; -ms-flex-wrap: wrap; flex-wrap: wrap; -webkit-box-align: center; -ms-flex-align: center; align-items: center; min-height: 270px; width: 100%; -ms-flex-negative: 0; flex-shrink: 0 } .dingflow-design .branch-box { display: flex; overflow: visible; min-height: 180px; height: auto; border-bottom: 2px solid #ccc; border-top: 2px solid #ccc; position: relative; margin-top: 15px } .dingflow-design .branch-box .col-box { background: #f5f5f7 } .dingflow-design .branch-box .col-box:before { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 0; margin: auto; width: 2px; height: 100%; background-color: #cacaca } .dingflow-design .add-branch { border: none; outline: none; user-select: none; justify-content: center; font-size: 12px; padding: 0 10px; height: 30px; line-height: 30px; border-radius: 15px; color: #3296fa; background: #fff; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .1); position: absolute; top: -16px; left: 50%; transform: translateX(-50%); transform-origin: center center; cursor: pointer; z-index: 1; display: inline-flex; align-items: center; -webkit-transition: all .3s cubic-bezier(.645, .045, .355, 1); transition: all .3s cubic-bezier(.645, .045, .355, 1) } .dingflow-design .add-branch:hover { transform: translateX(-50%) scale(1.1); box-shadow: 0 8px 16px 0 rgba(0, 0, 0, .1) } .dingflow-design .add-branch:active { transform: translateX(-50%); box-shadow: none } .dingflow-design .col-box { display: inline-flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; flex-direction: column; -webkit-box-align: center; align-items: center; position: relative } .dingflow-design .condition-node { min-height: 220px } .dingflow-design .condition-node, .dingflow-design .condition-node-box { display: inline-flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; flex-direction: column; -webkit-box-flex: 1 } .dingflow-design .condition-node-box { padding-top: 30px; padding-right: 50px; padding-left: 50px; -webkit-box-pack: center; justify-content: center; -webkit-box-align: center; align-items: center; flex-grow: 1; position: relative } .dingflow-design .condition-node-box:before { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; margin: auto; width: 2px; height: 100%; background-color: #cacaca } .dingflow-design .auto-judge { position: relative; width: 220px; min-height: 72px; background: #fff; border-radius: 4px; padding: 14px 19px; cursor: pointer } .dingflow-design .auto-judge:after { pointer-events: none; content: ""; position: absolute; top: 0; bottom: 0; left: 0; right: 0; z-index: 2; border-radius: 4px; border: 1px solid transparent; transition: all .1s cubic-bezier(.645, .045, .355, 1); box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1) } .dingflow-design .auto-judge.active:after, .dingflow-design .auto-judge:active:after, .dingflow-design .auto-judge:hover:after { border: 1px solid #3296fa; box-shadow: 0 0 6px 0 rgba(50, 150, 250, .3) } .dingflow-design .auto-judge.active .close, .dingflow-design .auto-judge:active .close, .dingflow-design .auto-judge:hover .close { display: block } .dingflow-design .auto-judge.error:after { border: 1px solid #f25643; box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1) } .dingflow-design .auto-judge .title-wrapper { position: relative; font-size: 12px; color: #15bc83; text-align: left; line-height: 16px } .dingflow-design .auto-judge .title-wrapper .editable-title { display: inline-block; max-width: 120px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis } .dingflow-design .auto-judge .title-wrapper .priority-title { display: inline-block; float: right; margin-right: 10px; color: rgba(25, 31, 37, .56) } .dingflow-design .auto-judge .placeholder { color: #bfbfbf } .dingflow-design .auto-judge .close { display: none; position: absolute; right: -10px; top: -10px; width: 20px; height: 20px; font-size: 14px; color: rgba(0, 0, 0, .25); border-radius: 50%; text-align: center; line-height: 20px; z-index: 2 } .dingflow-design .auto-judge .content { font-size: 14px; color: #191f25; text-align: left; margin-top: 6px; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical } .dingflow-design .auto-judge .sort-left, .dingflow-design .auto-judge .sort-right { position: absolute; top: 0; bottom: 0; display: none; z-index: 1 } .dingflow-design .auto-judge .sort-left { left: 0; border-right: 1px solid #f6f6f6 } .dingflow-design .auto-judge .sort-right { right: 0; border-left: 1px solid #f6f6f6 } .dingflow-design .auto-judge:hover .sort-left, .dingflow-design .auto-judge:hover .sort-right { display: flex; align-items: center } .dingflow-design .auto-judge .sort-left:hover, .dingflow-design .auto-judge .sort-right:hover { background: #efefef } .dingflow-design .end-node { border-radius: 50%; font-size: 14px; color: rgba(25, 31, 37, .4); text-align: left } .dingflow-design .end-node .end-node-circle { width: 10px; height: 10px; margin: auto; border-radius: 50%; background: #dbdcdc } .dingflow-design .end-node .end-node-text { margin-top: 5px; text-align: center } .approval-setting { border-radius: 2px; margin: 20px 0; position: relative; background: #fff } .ant-btn { position: relative } ================================================ FILE: yshop-drink-vue3/src/components/Sticky/index.ts ================================================ import Sticky from './src/Sticky.vue' export { Sticky } ================================================ FILE: yshop-drink-vue3/src/components/Sticky/src/Sticky.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/SummaryCard/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Table/index.ts ================================================ import Table from './src/Table.vue' import { ElTable } from 'element-plus' import { TableSetPropsType } from '@/types/table' import TableSelectForm from './src/TableSelectForm.vue' export interface TableExpose { setProps: (props: Recordable) => void setColumn: (columnProps: TableSetPropsType[]) => void selections: Recordable[] elTableRef: ComponentRef } export { Table, TableSelectForm } ================================================ FILE: yshop-drink-vue3/src/components/Table/src/Table.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Table/src/TableSelectForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Table/src/helper.ts ================================================ export const setIndex = (reserveIndex: boolean, index: number, size: number, current: number) => { const newIndex = index + 1 if (reserveIndex) { return size * (current - 1) + newIndex } else { return newIndex } } ================================================ FILE: yshop-drink-vue3/src/components/Table/src/types.ts ================================================ import { Pagination, TableColumn } from '@/types/table' export type TableProps = { pageSize?: number currentPage?: number // 是否多选 selection?: boolean // 是否所有的超出隐藏,优先级低于schema中的showOverflowTooltip, showOverflowTooltip?: boolean // 表头 columns?: TableColumn[] // 是否展示分页 pagination?: Pagination | undefined // 仅对 type=selection 的列有效,类型为 Boolean,为 true 则会在数据更新之后保留之前选中的数据(需指定 row-key) reserveSelection?: boolean // 加载状态 loading?: boolean // 是否叠加索引 reserveIndex?: boolean // 对齐方式 align?: 'left' | 'center' | 'right' // 表头对齐方式 headerAlign?: 'left' | 'center' | 'right' data?: Recordable expand?: boolean } & Recordable ================================================ FILE: yshop-drink-vue3/src/components/Tooltip/index.ts ================================================ import Tooltip from './src/Tooltip.vue' export { Tooltip } ================================================ FILE: yshop-drink-vue3/src/components/Tooltip/src/Tooltip.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/UploadFile/index.ts ================================================ import UploadImg from './src/UploadImg.vue' import UploadImgs from './src/UploadImgs.vue' import UploadFile from './src/UploadFile.vue' export { UploadImg, UploadImgs, UploadFile } ================================================ FILE: yshop-drink-vue3/src/components/UploadFile/src/UploadFile.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/UploadFile/src/UploadImg.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/UploadFile/src/UploadImgs.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/UploadFile/src/useUpload.ts ================================================ import * as FileApi from '@/api/infra/file' import CryptoJS from 'crypto-js' import { UploadRawFile, UploadRequestOptions } from 'element-plus/es/components/upload/src/upload' import axios from 'axios' export const useUpload = () => { // 后端上传地址 const uploadUrl = import.meta.env.VITE_UPLOAD_URL // 是否使用前端直连上传 const isClientUpload = UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE // 重写ElUpload上传方法 const httpRequest = async (options: UploadRequestOptions) => { // 模式一:前端上传 if (isClientUpload) { // 1.1 生成文件名称 const fileName = await generateFileName(options.file) // 1.2 获取文件预签名地址 const presignedInfo = await FileApi.getFilePresignedUrl(fileName) // 1.3 上传文件(不能使用 ElUpload 的 ajaxUpload 方法的原因:其使用的是 FormData 上传,Minio 不支持) return axios.put(presignedInfo.uploadUrl, options.file, { headers: { 'Content-Type': options.file.type, } }).then(() => { // 1.4. 记录文件信息到后端(异步) createFile(presignedInfo, fileName, options.file) // 通知成功,数据格式保持与后端上传的返回结果一致 return { data: presignedInfo.url } }) } else { // 模式二:后端上传 // 重写 el-upload httpRequest 文件上传成功会走成功的钩子,失败走失败的钩子 return new Promise((resolve, reject) => { FileApi.updateFile({ file: options.file }) .then((res) => { if (res.code === 0) { resolve(res) } else { reject(res) } }) .catch((res) => { reject(res) }) }) } } return { uploadUrl, httpRequest } } /** * 创建文件信息 * @param vo 文件预签名信息 * @param name 文件名称 * @param file 文件 */ function createFile(vo: FileApi.FilePresignedUrlRespVO, name: string, file: UploadRawFile) { const fileVo = { configId: vo.configId, url: vo.url, path: name, name: file.name, type: file.type, size: file.size } FileApi.createFile(fileVo) return fileVo } /** * 生成文件名称(使用算法SHA256) * @param file 要上传的文件 */ async function generateFileName(file: UploadRawFile) { // 读取文件内容 const data = await file.arrayBuffer() const wordArray = CryptoJS.lib.WordArray.create(data) // 计算SHA256 const sha256 = CryptoJS.SHA256(wordArray).toString() // 拼接后缀 const ext = file.name.substring(file.name.lastIndexOf('.')) return `${sha256}${ext}` } /** * 上传类型 */ enum UPLOAD_TYPE { // 客户端直接上传(只支持S3服务) CLIENT = 'client', // 客户端发送到后端上传 SERVER = 'server' } ================================================ FILE: yshop-drink-vue3/src/components/Verifition/index.ts ================================================ import Verify from './src/Verify.vue' export { Verify } ================================================ FILE: yshop-drink-vue3/src/components/Verifition/src/Verify/VerifyPoints.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Verifition/src/Verify/VerifySlide.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Verifition/src/Verify/index.ts ================================================ import VerifySlide from './VerifySlide.vue' import VerifyPoints from './VerifyPoints.vue' export { VerifySlide, VerifyPoints } ================================================ FILE: yshop-drink-vue3/src/components/Verifition/src/Verify.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/Verifition/src/utils/ase.ts ================================================ import CryptoJS from 'crypto-js' /** * @word 要加密的内容 * @keyWord String 服务器随机返回的关键字 * */ export function aesEncrypt(word, keyWord = 'XwKsGlMcdPMEhR1B') { const key = CryptoJS.enc.Utf8.parse(keyWord) const srcs = CryptoJS.enc.Utf8.parse(word) const encrypted = CryptoJS.AES.encrypt(srcs, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }) return encrypted.toString() } ================================================ FILE: yshop-drink-vue3/src/components/Verifition/src/utils/util.ts ================================================ export function resetSize(vm) { let img_width, img_height, bar_width, bar_height //图片的宽度、高度,移动条的宽度、高度 const EmployeeWindow = window as any const parentWidth = vm.$el.parentNode.offsetWidth || EmployeeWindow.offsetWidth const parentHeight = vm.$el.parentNode.offsetHeight || EmployeeWindow.offsetHeight if (vm.imgSize.width.indexOf('%') != -1) { img_width = (parseInt(vm.imgSize.width) / 100) * parentWidth + 'px' } else { img_width = vm.imgSize.width } if (vm.imgSize.height.indexOf('%') != -1) { img_height = (parseInt(vm.imgSize.height) / 100) * parentHeight + 'px' } else { img_height = vm.imgSize.height } if (vm.barSize.width.indexOf('%') != -1) { bar_width = (parseInt(vm.barSize.width) / 100) * parentWidth + 'px' } else { bar_width = vm.barSize.width } if (vm.barSize.height.indexOf('%') != -1) { bar_height = (parseInt(vm.barSize.height) / 100) * parentHeight + 'px' } else { bar_height = vm.barSize.height } return { imgWidth: img_width, imgHeight: img_height, barWidth: bar_width, barHeight: bar_height } } export const _code_chars = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' ] export const _code_color1 = ['#fffff0', '#f0ffff', '#f0fff0', '#fff0f0'] export const _code_color2 = ['#FF0033', '#006699', '#993366', '#FF9900', '#66CC66', '#FF33CC'] ================================================ FILE: yshop-drink-vue3/src/components/VerticalButtonGroup/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/XButton/index.ts ================================================ import XButton from './src/XButton.vue' import XTextButton from './src/XTextButton.vue' export { XButton, XTextButton } ================================================ FILE: yshop-drink-vue3/src/components/XButton/src/XButton.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/XButton/src/XTextButton.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/components/index.ts ================================================ import type { App } from 'vue' import { Icon } from './Icon' export const setupGlobCom = (app: App): void => { app.component('Icon', Icon) } ================================================ FILE: yshop-drink-vue3/src/config/axios/config.ts ================================================ const config: { base_url: string result_code: number | string default_headers: AxiosHeaders request_timeout: number } = { /** * api请求基础路径 */ base_url: import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL, /** * 接口成功返回状态码 */ result_code: 200, /** * 接口请求超时时间 */ request_timeout: 30000, /** * 默认接口请求类型 * 可选值:application/x-www-form-urlencoded multipart/form-data */ default_headers: 'application/json' } export { config } ================================================ FILE: yshop-drink-vue3/src/config/axios/errorCode.ts ================================================ export default { '401': '认证失败,无法访问系统资源', '403': '当前操作没有权限', '404': '访问资源不存在', default: '系统未知错误,请反馈给管理员' } ================================================ FILE: yshop-drink-vue3/src/config/axios/index.ts ================================================ import { service } from './service' import { config } from './config' const { default_headers } = config const request = (option: any) => { const { url, method, params, data, headersType, responseType, ...config } = option return service({ url: url, method, params, data, ...config, responseType: responseType, headers: { 'Content-Type': headersType || default_headers } }) } export default { get: async (option: any) => { const res = await request({ method: 'GET', ...option }) return res.data as unknown as T }, post: async (option: any) => { const res = await request({ method: 'POST', ...option }) return res.data as unknown as T }, postOriginal: async (option: any) => { const res = await request({ method: 'POST', ...option }) return res }, delete: async (option: any) => { const res = await request({ method: 'DELETE', ...option }) return res.data as unknown as T }, put: async (option: any) => { const res = await request({ method: 'PUT', ...option }) return res.data as unknown as T }, download: async (option: any) => { const res = await request({ method: 'GET', responseType: 'blob', ...option }) return res as unknown as Promise }, upload: async (option: any) => { option.headersType = 'multipart/form-data' const res = await request({ method: 'POST', ...option }) return res as unknown as Promise } } ================================================ FILE: yshop-drink-vue3/src/config/axios/service.ts ================================================ import axios, { AxiosError, AxiosInstance, AxiosRequestHeaders, AxiosResponse, InternalAxiosRequestConfig } from 'axios' import { ElMessage, ElMessageBox, ElNotification } from 'element-plus' import qs from 'qs' import { config } from '@/config/axios/config' import { getAccessToken, getRefreshToken, getTenantId, removeToken, setToken } from '@/utils/auth' import errorCode from './errorCode' import { resetRouter } from '@/router' import { deleteUserCache } from '@/hooks/web/useCache' const tenantEnable = import.meta.env.VITE_APP_TENANT_ENABLE const { result_code, base_url, request_timeout } = config // 需要忽略的提示。忽略后,自动 Promise.reject('error') const ignoreMsgs = [ '无效的刷新令牌', // 刷新令牌被删除时,不用提示 '刷新令牌已过期' // 使用刷新令牌,刷新获取新的访问令牌时,结果因为过期失败,此时需要忽略。否则,会导致继续 401,无法跳转到登出界面 ] // 是否显示重新登录 export const isRelogin = { show: false } // Axios 无感知刷新令牌,参考 https://www.dashingdog.cn/article/11 与 https://segmentfault.com/a/1190000020210980 实现 // 请求队列 let requestList: any[] = [] // 是否正在刷新中 let isRefreshToken = false // 请求白名单,无须token的接口 const whiteList: string[] = ['/login', '/refresh-token'] // 创建axios实例 const service: AxiosInstance = axios.create({ baseURL: base_url, // api 的 base_url timeout: request_timeout, // 请求超时时间 withCredentials: false // 禁用 Cookie 等信息 }) // request拦截器 service.interceptors.request.use( (config: InternalAxiosRequestConfig) => { // 是否需要设置 token let isToken = (config!.headers || {}).isToken === false whiteList.some((v) => { if (config.url) { config.url.indexOf(v) > -1 return (isToken = false) } }) if (getAccessToken() && !isToken) { ;(config as Recordable).headers.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token } // 设置租户 if (tenantEnable && tenantEnable === 'true') { const tenantId = getTenantId() if (tenantId) (config as Recordable).headers['tenant-id'] = tenantId } const params = config.params || {} const data = config.data || false if ( config.method?.toUpperCase() === 'POST' && (config.headers as AxiosRequestHeaders)['Content-Type'] === 'application/x-www-form-urlencoded' ) { config.data = qs.stringify(data) } // get参数编码 if (config.method?.toUpperCase() === 'GET' && params) { config.params = {} const paramsStr = qs.stringify(params, { allowDots: true }) if (paramsStr) { config.url = config.url + '?' + paramsStr } } return config }, (error: AxiosError) => { // Do something with request error console.log(error) // for debug Promise.reject(error) } ) // response 拦截器 service.interceptors.response.use( async (response: AxiosResponse) => { let { data } = response const config = response.config if (!data) { // 返回“[HTTP]请求没有返回值”; throw new Error() } const { t } = useI18n() // 未设置状态码则默认成功状态 // 二进制数据则直接返回,例如说 Excel 导出 if ( response.request.responseType === 'blob' || response.request.responseType === 'arraybuffer' ) { // 注意:如果导出的响应为 json,说明可能失败了,不直接返回进行下载 if (response.data.type !== 'application/json') { return response.data } data = await new Response(response.data).json() } const code = data.code || result_code // 获取错误信息 const msg = data.msg || errorCode[code] || errorCode['default'] if (ignoreMsgs.indexOf(msg) !== -1) { // 如果是忽略的错误码,直接返回 msg 异常 return Promise.reject(msg) } else if (code === 401) { // 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了 if (!isRefreshToken) { isRefreshToken = true // 1. 如果获取不到刷新令牌,则只能执行登出操作 if (!getRefreshToken()) { return handleAuthorized() } // 2. 进行刷新访问令牌 try { const refreshTokenRes = await refreshToken() // 2.1 刷新成功,则回放队列的请求 + 当前请求 setToken((await refreshTokenRes).data.data) config.headers!.Authorization = 'Bearer ' + getAccessToken() requestList.forEach((cb: any) => { cb() }) requestList = [] return service(config) } catch (e) { // 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。 // 2.2 刷新失败,只回放队列的请求 requestList.forEach((cb: any) => { cb() }) // 提示是否要登出。即不回放当前请求!不然会形成递归 return handleAuthorized() } finally { requestList = [] isRefreshToken = false } } else { // 添加到队列,等待刷新获取到新的令牌 return new Promise((resolve) => { requestList.push(() => { config.headers!.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改 resolve(service(config)) }) }) } } else if (code === 500) { ElMessage.error(t('sys.api.errMsg500')) return Promise.reject(new Error(msg)) } else if (code === 901) { ElMessage.error({ offset: 300, dangerouslyUseHTMLString: true, message: '
    ' + t('sys.api.errMsg901') + '
    ' + '
     
    ' + '
    https://www.yixiang.co/
    ' + '
     
    ' + '
    ' }) return Promise.reject(new Error(msg)) } else if (code !== 200) { if (msg === '无效的刷新令牌') { // hard coding:忽略这个提示,直接登出 console.log(msg) } else { ElNotification.error({ title: msg }) } return Promise.reject('error') } else { return data } }, (error: AxiosError) => { console.log('err' + error) // for debug let { message } = error const { t } = useI18n() if (message === 'Network Error') { message = t('sys.api.errorMessage') } else if (message.includes('timeout')) { message = t('sys.api.apiTimeoutMessage') } else if (message.includes('Request failed with status code')) { message = t('sys.api.apiRequestFailed') + message.substr(message.length - 3) } ElMessage.error(message) return Promise.reject(error) } ) const refreshToken = async () => { axios.defaults.headers.common['tenant-id'] = getTenantId() return await axios.post(base_url + '/system/auth/refresh-token?refreshToken=' + getRefreshToken()) } const handleAuthorized = () => { const { t } = useI18n() if (!isRelogin.show) { // 如果已经到重新登录页面则不进行弹窗提示 if (window.location.href.includes('login?redirect=')) { return } isRelogin.show = true ElMessageBox.confirm(t('sys.api.timeoutMessage'), t('common.confirmTitle'), { showCancelButton: false, closeOnClickModal: false, showClose: false, confirmButtonText: t('login.relogin'), type: 'warning' }).then(() => { resetRouter() // 重置静态路由表 deleteUserCache() // 删除用户缓存 removeToken() isRelogin.show = false // 干掉token后再走一次路由让它过router.beforeEach的校验 window.location.href = window.location.href }) } return Promise.reject(t('sys.api.timeoutMessage')) } export { service } ================================================ FILE: yshop-drink-vue3/src/directives/index.ts ================================================ import type { App } from 'vue' import { hasRole } from './permission/hasRole' import { hasPermi } from './permission/hasPermi' /** * 导出指令:v-xxx * @methods hasRole 用户权限,用法: v-hasRole * @methods hasPermi 按钮权限,用法: v-hasPermi */ export const setupAuth = (app: App) => { hasRole(app) hasPermi(app) } ================================================ FILE: yshop-drink-vue3/src/directives/permission/hasPermi.ts ================================================ import type { App } from 'vue' import { CACHE_KEY, useCache } from '@/hooks/web/useCache' const { t } = useI18n() // 国际化 export function hasPermi(app: App) { app.directive('hasPermi', (el, binding) => { const { wsCache } = useCache() const { value } = binding const all_permission = '*:*:*' const permissions = wsCache.get(CACHE_KEY.USER).permissions if (value && value instanceof Array && value.length > 0) { const permissionFlag = value const hasPermissions = permissions.some((permission: string) => { return all_permission === permission || permissionFlag.includes(permission) }) if (!hasPermissions) { el.parentNode && el.parentNode.removeChild(el) } } else { throw new Error(t('permission.hasPermission')) } }) } ================================================ FILE: yshop-drink-vue3/src/directives/permission/hasRole.ts ================================================ import type { App } from 'vue' import { CACHE_KEY, useCache } from '@/hooks/web/useCache' const { t } = useI18n() // 国际化 export function hasRole(app: App) { app.directive('hasRole', (el, binding) => { const { wsCache } = useCache() const { value } = binding const super_admin = 'admin' const roles = wsCache.get(CACHE_KEY.USER).roles if (value && value instanceof Array && value.length > 0) { const roleFlag = value const hasRole = roles.some((role: string) => { return super_admin === role || roleFlag.includes(role) }) if (!hasRole) { el.parentNode && el.parentNode.removeChild(el) } } else { throw new Error(t('permission.hasRole')) } }) } ================================================ FILE: yshop-drink-vue3/src/hooks/event/useScrollTo.ts ================================================ export interface ScrollToParams { el: HTMLElement to: number position: string duration?: number callback?: () => void } const easeInOutQuad = (t: number, b: number, c: number, d: number) => { t /= d / 2 if (t < 1) { return (c / 2) * t * t + b } t-- return (-c / 2) * (t * (t - 2) - 1) + b } const move = (el: HTMLElement, position: string, amount: number) => { el[position] = amount } export function useScrollTo({ el, position = 'scrollLeft', to, duration = 500, callback }: ScrollToParams) { const isActiveRef = ref(false) const start = el[position] const change = to - start const increment = 20 let currentTime = 0 function animateScroll() { if (!unref(isActiveRef)) { return } currentTime += increment const val = easeInOutQuad(currentTime, start, change, duration) move(el, position, val) if (currentTime < duration && unref(isActiveRef)) { requestAnimationFrame(animateScroll) } else { if (callback) { callback() } } } function run() { isActiveRef.value = true animateScroll() } function stop() { isActiveRef.value = false } return { start: run, stop } } ================================================ FILE: yshop-drink-vue3/src/hooks/web/useCache.ts ================================================ /** * 配置浏览器本地存储的方式,可直接存储对象数组。 */ import WebStorageCache from 'web-storage-cache' type CacheType = 'localStorage' | 'sessionStorage' export const CACHE_KEY = { // 用户相关 ROLE_ROUTERS: 'roleRouters', USER: 'user', // 系统设置 IS_DARK: 'isDark', LANG: 'lang', THEME: 'theme', LAYOUT: 'layout', DICT_CACHE: 'dictCache', // 登录表单 LoginForm: 'loginForm', TenantId: 'tenantId' } export const useCache = (type: CacheType = 'localStorage') => { const wsCache: WebStorageCache = new WebStorageCache({ storage: type }) return { wsCache } } export const deleteUserCache = () => { const { wsCache } = useCache() wsCache.delete(CACHE_KEY.USER) wsCache.delete(CACHE_KEY.ROLE_ROUTERS) // 注意,不要清理 LoginForm 登录表单 } ================================================ FILE: yshop-drink-vue3/src/hooks/web/useConfigGlobal.ts ================================================ import { ConfigGlobalTypes } from '@/types/configGlobal' export const useConfigGlobal = () => { const configGlobal = inject('configGlobal', {}) as ConfigGlobalTypes return { configGlobal } } ================================================ FILE: yshop-drink-vue3/src/hooks/web/useCrudSchemas.ts ================================================ import { reactive } from 'vue' import { AxiosPromise } from 'axios' import { findIndex } from '@/utils' import { eachTree, filter, treeMap } from '@/utils/tree' import { getBoolDictOptions, getDictOptions, getIntDictOptions } from '@/utils/dict' import { FormSchema } from '@/types/form' import { TableColumn } from '@/types/table' import { DescriptionsSchema } from '@/types/descriptions' import { ComponentOptions, ComponentProps } from '@/types/components' import { DictTag } from '@/components/DictTag' import { cloneDeep, merge } from 'lodash-es' export type CrudSchema = Omit & { isSearch?: boolean // 是否在查询显示 search?: CrudSearchParams // 查询的详细配置 isTable?: boolean // 是否在列表显示 table?: CrudTableParams // 列表的详细配置 isForm?: boolean // 是否在表单显示 form?: CrudFormParams // 表单的详细配置 isDetail?: boolean // 是否在详情显示 detail?: CrudDescriptionsParams // 详情的详细配置 children?: CrudSchema[] dictType?: string // 字典类型 dictClass?: 'string' | 'number' | 'boolean' // 字典数据类型 string | number | boolean } type CrudSearchParams = { // 是否显示在查询项 show?: boolean // 接口 api?: () => Promise // 搜索字段 field?: string } & Omit type CrudTableParams = { // 是否显示表头 show?: boolean // 列宽配置 width?: number | string // 列是否固定在左侧或者右侧 fixed?: 'left' | 'right' } & Omit type CrudFormParams = { // 是否显示表单项 show?: boolean // 接口 api?: () => Promise } & Omit type CrudDescriptionsParams = { // 是否显示表单项 show?: boolean } & Omit interface AllSchemas { searchSchema: FormSchema[] tableColumns: TableColumn[] formSchema: FormSchema[] detailSchema: DescriptionsSchema[] } const { t } = useI18n() // 过滤所有结构 export const useCrudSchemas = ( crudSchema: CrudSchema[] ): { allSchemas: AllSchemas } => { // 所有结构数据 const allSchemas = reactive({ searchSchema: [], tableColumns: [], formSchema: [], detailSchema: [] }) const searchSchema = filterSearchSchema(crudSchema, allSchemas) allSchemas.searchSchema = searchSchema || [] const tableColumns = filterTableSchema(crudSchema) allSchemas.tableColumns = tableColumns || [] const formSchema = filterFormSchema(crudSchema, allSchemas) allSchemas.formSchema = formSchema const detailSchema = filterDescriptionsSchema(crudSchema) allSchemas.detailSchema = detailSchema return { allSchemas } } // 过滤 Search 结构 const filterSearchSchema = (crudSchema: CrudSchema[], allSchemas: AllSchemas): FormSchema[] => { const searchSchema: FormSchema[] = [] // 获取字典列表队列 const searchRequestTask: Array<() => Promise> = [] eachTree(crudSchema, (schemaItem: CrudSchema) => { // 判断是否显示 if (schemaItem?.isSearch || schemaItem.search?.show) { let component = schemaItem?.search?.component || 'Input' const options: ComponentOptions[] = [] let comonentProps: ComponentProps = {} if (schemaItem.dictType) { const allOptions: ComponentOptions = { label: '全部', value: '' } options.push(allOptions) getDictOptions(schemaItem.dictType).forEach((dict) => { options.push(dict) }) comonentProps = { options: options } if (!schemaItem.search?.component) component = 'Select' } // updated by AKing: 解决了当使用默认的dict选项时,form中事件不能触发的问题 const searchSchemaItem = merge( { // 默认为 input component, ...schemaItem.search, field: schemaItem.field, label: schemaItem.search?.label || schemaItem.label }, { componentProps: comonentProps } ) if (searchSchemaItem.api) { searchRequestTask.push(async () => { const res = await (searchSchemaItem.api as () => AxiosPromise)() if (res) { const index = findIndex(allSchemas.searchSchema, (v: FormSchema) => { return v.field === searchSchemaItem.field }) if (index !== -1) { allSchemas.searchSchema[index]!.componentProps!.options = filterOptions( res, searchSchemaItem.componentProps.optionsAlias?.labelField ) } } }) } // 删除不必要的字段 delete searchSchemaItem.show searchSchema.push(searchSchemaItem) } }) for (const task of searchRequestTask) { task() } return searchSchema } // 过滤 table 结构 const filterTableSchema = (crudSchema: CrudSchema[]): TableColumn[] => { const tableColumns = treeMap(crudSchema, { conversion: (schema: CrudSchema) => { if (schema?.isTable !== false && schema?.table?.show !== false) { // add by 芋艿:增加对 dict 字典数据的支持 if (!schema.formatter && schema.dictType) { schema.formatter = (_: Recordable, __: TableColumn, cellValue: any) => { return h(DictTag, { type: schema.dictType!, // ! 表示一定不为空 value: cellValue }) } } return { ...schema.table, ...schema } } } }) // 第一次过滤会有 undefined 所以需要二次过滤 return filter(tableColumns as TableColumn[], (data) => { if (data.children === void 0) { delete data.children } return !!data.field }) } // 过滤 form 结构 const filterFormSchema = (crudSchema: CrudSchema[], allSchemas: AllSchemas): FormSchema[] => { const formSchema: FormSchema[] = [] // 获取字典列表队列 const formRequestTask: Array<() => Promise> = [] eachTree(crudSchema, (schemaItem: CrudSchema) => { // 判断是否显示 if (schemaItem?.isForm !== false && schemaItem?.form?.show !== false) { let component = schemaItem?.form?.component || 'Input' let defaultValue: any = '' if (schemaItem.form?.value) { defaultValue = schemaItem.form?.value } else { if (component === 'InputNumber') { defaultValue = 0 } } let comonentProps: ComponentProps = {} if (schemaItem.dictType) { const options: ComponentOptions[] = [] if (schemaItem.dictClass && schemaItem.dictClass === 'number') { getIntDictOptions(schemaItem.dictType).forEach((dict) => { options.push(dict) }) } else if (schemaItem.dictClass && schemaItem.dictClass === 'boolean') { getBoolDictOptions(schemaItem.dictType).forEach((dict) => { options.push(dict) }) } else { getDictOptions(schemaItem.dictType).forEach((dict) => { options.push(dict) }) } comonentProps = { options: options } if (!(schemaItem.form && schemaItem.form.component)) component = 'Select' } // updated by AKing: 解决了当使用默认的dict选项时,form中事件不能触发的问题 const formSchemaItem = merge( { // 默认为 input component, value: defaultValue, ...schemaItem.form, field: schemaItem.field, label: schemaItem.form?.label || schemaItem.label }, { componentProps: comonentProps } ) if (formSchemaItem.api) { formRequestTask.push(async () => { const res = await (formSchemaItem.api as () => AxiosPromise)() if (res) { const index = findIndex(allSchemas.formSchema, (v: FormSchema) => { return v.field === formSchemaItem.field }) if (index !== -1) { allSchemas.formSchema[index]!.componentProps!.options = filterOptions( res, formSchemaItem.componentProps.optionsAlias?.labelField ) } } }) } // 删除不必要的字段 delete formSchemaItem.show formSchema.push(formSchemaItem) } }) for (const task of formRequestTask) { task() } return formSchema } // 过滤 descriptions 结构 const filterDescriptionsSchema = (crudSchema: CrudSchema[]): DescriptionsSchema[] => { const descriptionsSchema: FormSchema[] = [] eachTree(crudSchema, (schemaItem: CrudSchema) => { // 判断是否显示 if (schemaItem?.isDetail !== false && schemaItem.detail?.show !== false) { const descriptionsSchemaItem = { ...schemaItem.detail, field: schemaItem.field, label: schemaItem.detail?.label || schemaItem.label } if (schemaItem.dictType) { descriptionsSchemaItem.dictType = schemaItem.dictType } if (schemaItem.detail?.dateFormat || schemaItem.formatter == 'formatDate') { // 优先使用 detail 下的配置,如果没有默认为 YYYY-MM-DD HH:mm:ss descriptionsSchemaItem.dateFormat = schemaItem?.detail?.dateFormat ? schemaItem?.detail?.dateFormat : 'YYYY-MM-DD HH:mm:ss' } // 删除不必要的字段 delete descriptionsSchemaItem.show descriptionsSchema.push(descriptionsSchemaItem) } }) return descriptionsSchema } // 给options添加国际化 const filterOptions = (options: Recordable, labelField?: string) => { return options?.map((v: Recordable) => { if (labelField) { v['labelField'] = t(v.labelField) } else { v['label'] = t(v.label) } return v }) } // 将 tableColumns 指定 fields 放到最前面 export const sortTableColumns = (tableColumns: TableColumn[], field: string) => { const fieldIndex = tableColumns.findIndex((item) => item.field === field) const fieldColumn = cloneDeep(tableColumns[fieldIndex]) tableColumns.splice(fieldIndex, 1) // 添加到开头 tableColumns.unshift(fieldColumn) } ================================================ FILE: yshop-drink-vue3/src/hooks/web/useDesign.ts ================================================ import variables from '@/styles/global.module.scss' export const useDesign = () => { const scssVariables = variables /** * @param scope 类名 * @returns 返回空间名-类名 */ const getPrefixCls = (scope: string) => { return `${scssVariables.namespace}-${scope}` } return { variables: scssVariables, getPrefixCls } } ================================================ FILE: yshop-drink-vue3/src/hooks/web/useEmitt.ts ================================================ import mitt from 'mitt' interface Option { name: string // 事件名称 callback: Fn // 回调 } const emitter = mitt() export const useEmitt = (option?: Option) => { if (option) { emitter.on(option.name, option.callback) onBeforeUnmount(() => { emitter.off(option.name) }) } return { emitter } } ================================================ FILE: yshop-drink-vue3/src/hooks/web/useForm.ts ================================================ import type { Form, FormExpose } from '@/components/Form' import type { ElForm } from 'element-plus' import type { FormProps } from '@/components/Form/src/types' import { FormSchema, FormSetPropsType } from '@/types/form' export const useForm = (props?: FormProps) => { // From实例 const formRef = ref() // ElForm实例 const elFormRef = ref>() /** * @param ref Form实例 * @param elRef ElForm实例 */ const register = (ref: typeof Form & FormExpose, elRef: ComponentRef) => { formRef.value = ref elFormRef.value = elRef } const getForm = async () => { await nextTick() const form = unref(formRef) if (!form) { console.error('The form is not registered. Please use the register method to register') } return form } // 一些内置的方法 const methods: { setProps: (props: Recordable) => void setValues: (data: Recordable) => void getFormData: () => Promise setSchema: (schemaProps: FormSetPropsType[]) => void addSchema: (formSchema: FormSchema, index?: number) => void delSchema: (field: string) => void } = { setProps: async (props: FormProps = {}) => { const form = await getForm() form?.setProps(props) if (props.model) { form?.setValues(props.model) } }, setValues: async (data: Recordable) => { const form = await getForm() form?.setValues(data) }, /** * @param schemaProps 需要设置的schemaProps */ setSchema: async (schemaProps: FormSetPropsType[]) => { const form = await getForm() form?.setSchema(schemaProps) }, /** * @param formSchema 需要新增数据 * @param index 在哪里新增 */ addSchema: async (formSchema: FormSchema, index?: number) => { const form = await getForm() form?.addSchema(formSchema, index) }, /** * @param field 删除哪个数据 */ delSchema: async (field: string) => { const form = await getForm() form?.delSchema(field) }, /** * @returns form data */ getFormData: async (): Promise => { const form = await getForm() return form?.formModel as T } } props && methods.setProps(props) return { register, elFormRef, methods } } ================================================ FILE: yshop-drink-vue3/src/hooks/web/useGuide.ts ================================================ import { Config, driver } from 'driver.js' import 'driver.js/dist/driver.css' import { useDesign } from '@/hooks/web/useDesign' import { useI18n } from '@/hooks/web/useI18n' const { t } = useI18n() const { variables } = useDesign() export const useGuide = (options?: Config) => { const driverObj = driver( options || { showProgress: true, nextBtnText: t('common.nextLabel'), prevBtnText: t('common.prevLabel'), doneBtnText: t('common.doneLabel'), steps: [ { element: `#${variables.namespace}-menu`, popover: { title: t('common.menu'), description: t('common.menuDes'), side: 'right' } }, { element: `#${variables.namespace}-tool-header`, popover: { title: t('common.tool'), description: t('common.toolDes'), side: 'left' } }, { element: `#${variables.namespace}-tags-view`, popover: { title: t('common.tagsView'), description: t('common.tagsViewDes'), side: 'bottom' } } ] } ) return { ...driverObj } } ================================================ FILE: yshop-drink-vue3/src/hooks/web/useI18n.ts ================================================ import { i18n } from '@/plugins/vueI18n' type I18nGlobalTranslation = { (key: string): string (key: string, locale: string): string (key: string, locale: string, list: unknown[]): string (key: string, locale: string, named: Record): string (key: string, list: unknown[]): string (key: string, named: Record): string } type I18nTranslationRestParameters = [string, any] const getKey = (namespace: string | undefined, key: string) => { if (!namespace) { return key } if (key.startsWith(namespace)) { return key } return `${namespace}.${key}` } export const useI18n = ( namespace?: string ): { t: I18nGlobalTranslation } => { const normalFn = { t: (key: string) => { return getKey(namespace, key) } } if (!i18n) { return normalFn } const { t, ...methods } = i18n.global const tFn: I18nGlobalTranslation = (key: string, ...arg: any[]) => { if (!key) return '' if (!key.includes('.') && !namespace) return key //@ts-ignore return t(getKey(namespace, key), ...(arg as I18nTranslationRestParameters)) } return { ...methods, t: tFn } } export const t = (key: string) => key ================================================ FILE: yshop-drink-vue3/src/hooks/web/useIcon.ts ================================================ import { h } from 'vue' import type { VNode } from 'vue' import { Icon } from '@/components/Icon' import { IconTypes } from '@/types/icon' export const useIcon = (props: IconTypes): VNode => { return h(Icon, props) } ================================================ FILE: yshop-drink-vue3/src/hooks/web/useLocale.ts ================================================ import { i18n } from '@/plugins/vueI18n' import { useLocaleStoreWithOut } from '@/store/modules/locale' import { setHtmlPageLang } from '@/plugins/vueI18n/helper' const setI18nLanguage = (locale: LocaleType) => { const localeStore = useLocaleStoreWithOut() if (i18n.mode === 'legacy') { i18n.global.locale = locale } else { ;(i18n.global.locale as any).value = locale } localeStore.setCurrentLocale({ lang: locale }) setHtmlPageLang(locale) } export const useLocale = () => { // Switching the language will change the locale of useI18n // And submit to configuration modification const changeLocale = async (locale: LocaleType) => { const globalI18n = i18n.global const langModule = await import(`../../locales/${locale}.ts`) globalI18n.setLocaleMessage(locale, langModule.default) setI18nLanguage(locale) } return { changeLocale } } ================================================ FILE: yshop-drink-vue3/src/hooks/web/useMessage.ts ================================================ import { ElMessage, ElMessageBox, ElNotification } from 'element-plus' import { useI18n } from './useI18n' export const useMessage = () => { const { t } = useI18n() return { // 消息提示 info(content: string) { ElMessage.info(content) }, // 错误消息 error(content: string) { ElMessage.error(content) }, // 成功消息 success(content: string) { ElMessage.success(content) }, // 警告消息 warning(content: string) { ElMessage.warning(content) }, // 弹出提示 alert(content: string) { ElMessageBox.alert(content, t('common.confirmTitle')) }, // 错误提示 alertError(content: string) { ElMessageBox.alert(content, t('common.confirmTitle'), { type: 'error' }) }, // 成功提示 alertSuccess(content: string) { ElMessageBox.alert(content, t('common.confirmTitle'), { type: 'success' }) }, // 警告提示 alertWarning(content: string) { ElMessageBox.alert(content, t('common.confirmTitle'), { type: 'warning' }) }, // 通知提示 notify(content: string) { ElNotification.info(content) }, // 错误通知 notifyError(content: string) { ElNotification.error(content) }, // 成功通知 notifySuccess(content: string) { ElNotification.success(content) }, // 警告通知 notifyWarning(content: string) { ElNotification.warning(content) }, // 确认窗体 confirm(content: string, tip?: string) { return ElMessageBox.confirm(content, tip ? tip : t('common.confirmTitle'), { confirmButtonText: t('common.ok'), cancelButtonText: t('common.cancel'), type: 'warning' }) }, // 删除窗体 delConfirm(content?: string, tip?: string) { return ElMessageBox.confirm( content ? content : t('common.delMessage'), tip ? tip : t('common.confirmTitle'), { confirmButtonText: t('common.ok'), cancelButtonText: t('common.cancel'), type: 'warning' } ) }, // 导出窗体 exportConfirm(content?: string, tip?: string) { return ElMessageBox.confirm( content ? content : t('common.exportMessage'), tip ? tip : t('common.confirmTitle'), { confirmButtonText: t('common.ok'), cancelButtonText: t('common.cancel'), type: 'warning' } ) }, // 提交内容 prompt(content: string, tip: string) { return ElMessageBox.prompt(content, tip, { confirmButtonText: t('common.ok'), cancelButtonText: t('common.cancel'), type: 'warning' }) } } } ================================================ FILE: yshop-drink-vue3/src/hooks/web/useNProgress.ts ================================================ import { useCssVar } from '@vueuse/core' import type { NProgressOptions } from 'nprogress' import NProgress from 'nprogress' import 'nprogress/nprogress.css' const primaryColor = useCssVar('--el-color-primary', document.documentElement) export const useNProgress = () => { NProgress.configure({ showSpinner: false } as NProgressOptions) const initColor = async () => { await nextTick() const bar = document.getElementById('nprogress')?.getElementsByClassName('bar')[0] as ElRef if (bar) { bar.style.background = unref(primaryColor.value) } } initColor() const start = () => { NProgress.start() } const done = () => { NProgress.done() } return { start, done } } ================================================ FILE: yshop-drink-vue3/src/hooks/web/useNetwork.ts ================================================ import { ref, onBeforeUnmount } from 'vue' const useNetwork = () => { const online = ref(true) const updateNetwork = () => { online.value = navigator.onLine } window.addEventListener('online', updateNetwork) window.addEventListener('offline', updateNetwork) onBeforeUnmount(() => { window.removeEventListener('online', updateNetwork) window.removeEventListener('offline', updateNetwork) }) return { online } } export { useNetwork } ================================================ FILE: yshop-drink-vue3/src/hooks/web/useNow.ts ================================================ import { dateUtil } from '@/utils/dateUtil' import { reactive, toRefs } from 'vue' import { tryOnMounted, tryOnUnmounted } from '@vueuse/core' export const useNow = (immediate = true) => { let timer: IntervalHandle const state = reactive({ year: 0, month: 0, week: '', day: 0, hour: '', minute: '', second: 0, meridiem: '' }) const update = () => { const now = dateUtil() const h = now.format('HH') const m = now.format('mm') const s = now.get('s') state.year = now.get('y') state.month = now.get('M') + 1 state.week = '星期' + ['日', '一', '二', '三', '四', '五', '六'][now.day()] state.day = now.get('date') state.hour = h state.minute = m state.second = s state.meridiem = now.format('A') } function start() { update() clearInterval(timer) timer = setInterval(() => update(), 1000) } function stop() { clearInterval(timer) } tryOnMounted(() => { immediate && start() }) tryOnUnmounted(() => { stop() }) return { ...toRefs(state), start, stop } } ================================================ FILE: yshop-drink-vue3/src/hooks/web/usePageLoading.ts ================================================ import { useAppStoreWithOut } from '@/store/modules/app' const appStore = useAppStoreWithOut() export const usePageLoading = () => { const loadStart = () => { appStore.setPageLoading(true) } const loadDone = () => { appStore.setPageLoading(false) } return { loadStart, loadDone } } ================================================ FILE: yshop-drink-vue3/src/hooks/web/useTable.ts ================================================ import download from '@/utils/download' import { Table, TableExpose } from '@/components/Table' import { ElMessage, ElMessageBox, ElTable } from 'element-plus' import { computed, nextTick, reactive, ref, unref, watch } from 'vue' import type { TableProps } from '@/components/Table/src/types' import { TableSetPropsType } from '@/types/table' const { t } = useI18n() interface ResponseType { list: T[] total?: number } interface UseTableConfig { getListApi: (option: any) => Promise delListApi?: (option: any) => Promise exportListApi?: (option: any) => Promise // 返回数据格式配置 response?: ResponseType // 默认传递的参数 defaultParams?: Recordable props?: TableProps } interface TableObject { pageSize: number currentPage: number total: number tableList: T[] params: any loading: boolean exportLoading: boolean currentRow: Nullable } export const useTable = (config?: UseTableConfig) => { const tableObject = reactive>({ // 页数 pageSize: 10, // 当前页 currentPage: 1, // 总条数 total: 10, // 表格数据 tableList: [], // AxiosConfig 配置 params: { ...(config?.defaultParams || {}) }, // 加载中 loading: true, // 导出加载中 exportLoading: false, // 当前行的数据 currentRow: null }) const paramsObj = computed(() => { return { ...tableObject.params, pageSize: tableObject.pageSize, pageNo: tableObject.currentPage } }) watch( () => tableObject.currentPage, () => { methods.getList() } ) watch( () => tableObject.pageSize, () => { // 当前页不为1时,修改页数后会导致多次调用getList方法 if (tableObject.currentPage === 1) { methods.getList() } else { tableObject.currentPage = 1 methods.getList() } } ) // Table实例 const tableRef = ref() // ElTable实例 const elTableRef = ref>() const register = (ref: typeof Table & TableExpose, elRef: ComponentRef) => { tableRef.value = ref elTableRef.value = elRef } const getTable = async () => { await nextTick() const table = unref(tableRef) if (!table) { console.error('The table is not registered. Please use the register method to register') } return table } const delData = async (ids: string | number | string[] | number[]) => { let idsLength = 1 if (ids instanceof Array) { idsLength = ids.length await Promise.all( ids.map(async (id: string | number) => { await (config?.delListApi && config?.delListApi(id)) }) ) } else { await (config?.delListApi && config?.delListApi(ids)) } ElMessage.success(t('common.delSuccess')) // 计算出临界点 tableObject.currentPage = tableObject.total % tableObject.pageSize === idsLength || tableObject.pageSize === 1 ? tableObject.currentPage > 1 ? tableObject.currentPage - 1 : tableObject.currentPage : tableObject.currentPage await methods.getList() } const methods = { getList: async () => { tableObject.loading = true const res = await config?.getListApi(unref(paramsObj)).finally(() => { tableObject.loading = false }) if (res) { tableObject.tableList = (res as unknown as ResponseType).list tableObject.total = (res as unknown as ResponseType).total ?? 0 } }, setProps: async (props: TableProps = {}) => { const table = await getTable() table?.setProps(props) }, setColumn: async (columnProps: TableSetPropsType[]) => { const table = await getTable() table?.setColumn(columnProps) }, getSelections: async () => { const table = await getTable() return (table?.selections || []) as T[] }, // 与Search组件结合 setSearchParams: (data: Recordable) => { tableObject.params = Object.assign(tableObject.params, { pageSize: tableObject.pageSize, pageNo: 1, ...data }) // 页码不等于1时更新页码重新获取数据,页码等于1时重新获取数据 if (tableObject.currentPage !== 1) { tableObject.currentPage = 1 } else { methods.getList() } }, // 删除数据 delList: async ( ids: string | number | string[] | number[], multiple: boolean, message = true ) => { const tableRef = await getTable() if (multiple) { if (!tableRef?.selections.length) { ElMessage.warning(t('common.delNoData')) return } } if (message) { ElMessageBox.confirm(t('common.delMessage'), t('common.confirmTitle'), { confirmButtonText: t('common.ok'), cancelButtonText: t('common.cancel'), type: 'warning' }).then(async () => { await delData(ids) }) } else { await delData(ids) } }, // 导出列表 exportList: async (fileName: string) => { tableObject.exportLoading = true ElMessageBox.confirm(t('common.exportMessage'), t('common.confirmTitle'), { confirmButtonText: t('common.ok'), cancelButtonText: t('common.cancel'), type: 'warning' }) .then(async () => { const res = await config?.exportListApi?.(unref(paramsObj) as unknown as T) if (res) { download.excel(res as unknown as Blob, fileName) } }) .finally(() => { tableObject.exportLoading = false }) } } config?.props && methods.setProps(config.props) return { register, elTableRef, tableObject, methods, // add by 芋艿:返回 tableMethods 属性,和 tableObject 更统一 tableMethods: methods } } ================================================ FILE: yshop-drink-vue3/src/hooks/web/useTagsView.ts ================================================ import { useTagsViewStoreWithOut } from '@/store/modules/tagsView' import { RouteLocationNormalizedLoaded, useRouter } from 'vue-router' import { computed, nextTick, unref } from 'vue' export const useTagsView = () => { const tagsViewStore = useTagsViewStoreWithOut() const { replace, currentRoute } = useRouter() const selectedTag = computed(() => tagsViewStore.getSelectedTag) const closeAll = (callback?: Fn) => { tagsViewStore.delAllViews() callback?.() } const closeLeft = (callback?: Fn) => { tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded) callback?.() } const closeRight = (callback?: Fn) => { tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded) callback?.() } const closeOther = (callback?: Fn) => { tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded) callback?.() } const closeCurrent = (view?: RouteLocationNormalizedLoaded, callback?: Fn) => { if (view?.meta?.affix) return tagsViewStore.delView(view || unref(currentRoute)) callback?.() } const refreshPage = async (view?: RouteLocationNormalizedLoaded, callback?: Fn) => { tagsViewStore.delCachedView() const { path, query } = view || unref(currentRoute) await nextTick() replace({ path: '/redirect' + path, query: query }) callback?.() } const setTitle = (title: string, path?: string) => { tagsViewStore.setTitle(title, path) } return { closeAll, closeLeft, closeRight, closeOther, closeCurrent, refreshPage, setTitle } } ================================================ FILE: yshop-drink-vue3/src/hooks/web/useTimeAgo.ts ================================================ import { useTimeAgo as useTimeAgoCore, UseTimeAgoMessages } from '@vueuse/core' import { useLocaleStoreWithOut } from '@/store/modules/locale' const TIME_AGO_MESSAGE_MAP: { 'zh-CN': UseTimeAgoMessages en: UseTimeAgoMessages } = { // @ts-ignore 'zh-CN': { justNow: '刚刚', past: (n) => (n.match(/\d/) ? `${n}前` : n), future: (n) => (n.match(/\d/) ? `${n}后` : n), month: (n, past) => (n === 1 ? (past ? '上个月' : '下个月') : `${n} 个月`), year: (n, past) => (n === 1 ? (past ? '去年' : '明年') : `${n} 年`), day: (n, past) => (n === 1 ? (past ? '昨天' : '明天') : `${n} 天`), week: (n, past) => (n === 1 ? (past ? '上周' : '下周') : `${n} 周`), hour: (n) => `${n} 小时`, minute: (n) => `${n} 分钟`, second: (n) => `${n} 秒` }, // @ts-ignore en: { justNow: 'just now', past: (n) => (n.match(/\d/) ? `${n} ago` : n), future: (n) => (n.match(/\d/) ? `in ${n}` : n), month: (n, past) => n === 1 ? (past ? 'last month' : 'next month') : `${n} month${n > 1 ? 's' : ''}`, year: (n, past) => n === 1 ? (past ? 'last year' : 'next year') : `${n} year${n > 1 ? 's' : ''}`, day: (n, past) => (n === 1 ? (past ? 'yesterday' : 'tomorrow') : `${n} day${n > 1 ? 's' : ''}`), week: (n, past) => n === 1 ? (past ? 'last week' : 'next week') : `${n} week${n > 1 ? 's' : ''}`, hour: (n) => `${n} hour${n > 1 ? 's' : ''}`, minute: (n) => `${n} minute${n > 1 ? 's' : ''}`, second: (n) => `${n} second${n > 1 ? 's' : ''}` } } export const useTimeAgo = (time: Date | number | string) => { const localeStore = useLocaleStoreWithOut() const currentLocale = computed(() => localeStore.getCurrentLocale) const timeAgo = useTimeAgoCore(time, { messages: TIME_AGO_MESSAGE_MAP[unref(currentLocale).lang] }) return timeAgo } ================================================ FILE: yshop-drink-vue3/src/hooks/web/useTitle.ts ================================================ import { watch, ref } from 'vue' import { isString } from '@/utils/is' import { useAppStoreWithOut } from '@/store/modules/app' const appStore = useAppStoreWithOut() export const useTitle = (newTitle?: string) => { const { t } = useI18n() const title = ref( newTitle ? `${appStore.getTitle} - ${t(newTitle as string)}` : appStore.getTitle ) watch( title, (n, o) => { if (isString(n) && n !== o && document) { document.title = n } }, { immediate: true } ) return title } ================================================ FILE: yshop-drink-vue3/src/hooks/web/useValidator.ts ================================================ import { useI18n } from '@/hooks/web/useI18n' import { FormItemRule } from 'element-plus' const { t } = useI18n() interface LengthRange { min: number max: number message?: string } export const useValidator = () => { const required = (message?: string): FormItemRule => { return { required: true, message: message || t('common.required') } } const lengthRange = (options: LengthRange): FormItemRule => { const { min, max, message } = options return { min, max, message: message || t('common.lengthRange', { min, max }) } } const notSpace = (message?: string): FormItemRule => { return { validator: (_, val, callback) => { if (val?.indexOf(' ') !== -1) { callback(new Error(message || t('common.notSpace'))) } else { callback() } } } } const notSpecialCharacters = (message?: string): FormItemRule => { return { validator: (_, val, callback) => { if (/[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/gi.test(val)) { callback(new Error(message || t('common.notSpecialCharacters'))) } else { callback() } } } } return { required, lengthRange, notSpace, notSpecialCharacters } } ================================================ FILE: yshop-drink-vue3/src/hooks/web/useWatermark.ts ================================================ const domSymbol = Symbol('watermark-dom') export function useWatermark(appendEl: HTMLElement | null = document.body) { let func: Fn = () => {} const id = domSymbol.toString() const clear = () => { const domId = document.getElementById(id) if (domId) { const el = appendEl el && el.removeChild(domId) } window.removeEventListener('resize', func) } const createWatermark = (str: string) => { clear() const can = document.createElement('canvas') can.width = 300 can.height = 240 const cans = can.getContext('2d') if (cans) { cans.rotate((-20 * Math.PI) / 120) cans.font = '15px Vedana' cans.fillStyle = 'rgba(0, 0, 0, 0.15)' cans.textAlign = 'left' cans.textBaseline = 'middle' cans.fillText(str, can.width / 20, can.height) } const div = document.createElement('div') div.id = id div.style.pointerEvents = 'none' div.style.top = '0px' div.style.left = '0px' div.style.position = 'absolute' div.style.zIndex = '100000000' div.style.width = document.documentElement.clientWidth + 'px' div.style.height = document.documentElement.clientHeight + 'px' div.style.background = 'url(' + can.toDataURL('image/png') + ') left top repeat' const el = appendEl el && el.appendChild(div) return id } function setWatermark(str: string) { createWatermark(str) func = () => { createWatermark(str) } window.addEventListener('resize', func) } return { setWatermark, clear } } ================================================ FILE: yshop-drink-vue3/src/layout/Layout.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/AppView.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/Breadcrumb/index.ts ================================================ import Breadcrumb from './src/Breadcrumb.vue' export { Breadcrumb } ================================================ FILE: yshop-drink-vue3/src/layout/components/Breadcrumb/src/Breadcrumb.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/Breadcrumb/src/helper.ts ================================================ import { pathResolve } from '@/utils/routerHelper' import type { RouteMeta } from 'vue-router' export const filterBreadcrumb = ( routes: AppRouteRecordRaw[], parentPath = '' ): AppRouteRecordRaw[] => { const res: AppRouteRecordRaw[] = [] for (const route of routes) { const meta = route?.meta as RouteMeta if (meta.hidden && !meta.canTo) { continue } const data: AppRouteRecordRaw = !meta.alwaysShow && route.children?.length === 1 ? { ...route.children[0], path: pathResolve(route.path, route.children[0].path) } : { ...route } data.path = pathResolve(parentPath, data.path) if (data.children) { data.children = filterBreadcrumb(data.children, data.path) } if (data) { res.push(data) } } return res } ================================================ FILE: yshop-drink-vue3/src/layout/components/Collapse/index.ts ================================================ import Collapse from './src/Collapse.vue' export { Collapse } ================================================ FILE: yshop-drink-vue3/src/layout/components/Collapse/src/Collapse.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/ContextMenu/index.ts ================================================ import ContextMenu from './src/ContextMenu.vue' import { ElDropdown } from 'element-plus' import type { RouteLocationNormalizedLoaded } from 'vue-router' export interface ContextMenuExpose { elDropdownMenuRef: ComponentRef tagItem: RouteLocationNormalizedLoaded } export { ContextMenu } ================================================ FILE: yshop-drink-vue3/src/layout/components/ContextMenu/src/ContextMenu.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/Footer/index.ts ================================================ import Footer from './src/Footer.vue' export { Footer } ================================================ FILE: yshop-drink-vue3/src/layout/components/Footer/src/Footer.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/LocaleDropdown/index.ts ================================================ import LocaleDropdown from './src/LocaleDropdown.vue' export { LocaleDropdown } ================================================ FILE: yshop-drink-vue3/src/layout/components/LocaleDropdown/src/LocaleDropdown.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/Logo/index.ts ================================================ import Logo from './src/Logo.vue' export { Logo } ================================================ FILE: yshop-drink-vue3/src/layout/components/Logo/src/Logo.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/Menu/index.ts ================================================ import Menu from './src/Menu.vue' export { Menu } ================================================ FILE: yshop-drink-vue3/src/layout/components/Menu/src/Menu.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/Menu/src/components/useRenderMenuItem.tsx ================================================ import { ElSubMenu, ElMenuItem } from 'element-plus' import { hasOneShowingChild } from '../helper' import { isUrl } from '@/utils/is' import { useRenderMenuTitle } from './useRenderMenuTitle' import { pathResolve } from '@/utils/routerHelper' const { renderMenuTitle } = useRenderMenuTitle() export const useRenderMenuItem = () => // allRouters: AppRouteRecordRaw[] = [], { const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => { return routers .filter((v) => !v.meta?.hidden) .map((v) => { const meta = v.meta ?? {} const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v) const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath(allRouters, v.path).join('/') if ( oneShowingChild && (!onlyOneChild?.children || onlyOneChild?.noShowingChildren) && !meta?.alwaysShow ) { return ( {{ default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta) }} ) } else { return ( {{ title: () => renderMenuTitle(meta), default: () => renderMenuItem(v.children!, fullPath) }} ) } }) } return { renderMenuItem } } ================================================ FILE: yshop-drink-vue3/src/layout/components/Menu/src/components/useRenderMenuTitle.tsx ================================================ import type { RouteMeta } from 'vue-router' import { Icon } from '@/components/Icon' import { useI18n } from '@/hooks/web/useI18n' export const useRenderMenuTitle = () => { const renderMenuTitle = (meta: RouteMeta) => { const { t } = useI18n() const { title = 'Please set title', icon } = meta return icon ? ( <> {t(title as string)} ) : ( {t(title as string)} ) } return { renderMenuTitle } } ================================================ FILE: yshop-drink-vue3/src/layout/components/Menu/src/helper.ts ================================================ import type { RouteMeta } from 'vue-router' import { findPath } from '@/utils/tree' type OnlyOneChildType = AppRouteRecordRaw & { noShowingChildren?: boolean } interface HasOneShowingChild { oneShowingChild?: boolean onlyOneChild?: OnlyOneChildType } export const getAllParentPath = (treeData: T[], path: string) => { const menuList = findPath(treeData, (n) => n.path === path) as AppRouteRecordRaw[] return (menuList || []).map((item) => item.path) } export const hasOneShowingChild = ( children: AppRouteRecordRaw[] = [], parent: AppRouteRecordRaw ): HasOneShowingChild => { const onlyOneChild = ref() const showingChildren = children.filter((v) => { const meta = (v.meta ?? {}) as RouteMeta if (meta.hidden) { return false } else { // Temp set(will be used if only has one showing child) onlyOneChild.value = v return true } }) // When there is only one child router, the child router is displayed by default if (showingChildren.length === 1) { return { oneShowingChild: true, onlyOneChild: unref(onlyOneChild) } } // Show parent if there are no child router to display if (!showingChildren.length) { onlyOneChild.value = { ...parent, path: '', noShowingChildren: true } return { oneShowingChild: true, onlyOneChild: unref(onlyOneChild) } } return { oneShowingChild: false, onlyOneChild: unref(onlyOneChild) } } ================================================ FILE: yshop-drink-vue3/src/layout/components/Message/index.ts ================================================ import Message from './src/Message.vue' export { Message } ================================================ FILE: yshop-drink-vue3/src/layout/components/Message/src/Message.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/Screenfull/index.ts ================================================ import Screenfull from './src/Screenfull.vue' export { Screenfull } ================================================ FILE: yshop-drink-vue3/src/layout/components/Screenfull/src/Screenfull.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/Setting/index.ts ================================================ import Setting from './src/Setting.vue' export { Setting } ================================================ FILE: yshop-drink-vue3/src/layout/components/Setting/src/Setting.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/Setting/src/components/ColorRadioPicker.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/Setting/src/components/InterfaceDisplay.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/Setting/src/components/LayoutRadioPicker.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/SizeDropdown/index.ts ================================================ import SizeDropdown from './src/SizeDropdown.vue' export { SizeDropdown } ================================================ FILE: yshop-drink-vue3/src/layout/components/SizeDropdown/src/SizeDropdown.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/TabMenu/index.ts ================================================ import TabMenu from './src/TabMenu.vue' export { TabMenu } ================================================ FILE: yshop-drink-vue3/src/layout/components/TabMenu/src/TabMenu.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/TabMenu/src/helper.ts ================================================ import { getAllParentPath } from '@/layout/components/Menu/src/helper' import type { RouteMeta } from 'vue-router' import { isUrl } from '@/utils/is' import { cloneDeep } from 'lodash-es' export type TabMapTypes = { [key: string]: string[] } export const tabPathMap = reactive({}) export const initTabMap = (routes: AppRouteRecordRaw[]) => { for (const v of routes) { const meta = (v.meta ?? {}) as RouteMeta if (!meta?.hidden) { tabPathMap[v.path] = [] } } } export const filterMenusPath = ( routes: AppRouteRecordRaw[], allRoutes: AppRouteRecordRaw[] ): AppRouteRecordRaw[] => { const res: AppRouteRecordRaw[] = [] for (const v of routes) { let data: Nullable = null const meta = (v.meta ?? {}) as RouteMeta if (!meta.hidden || meta.canTo) { const allParentPath = getAllParentPath(allRoutes, v.path) const fullPath = isUrl(v.path) ? v.path : allParentPath.join('/') data = cloneDeep(v) data.path = fullPath if (v.children && data) { data.children = filterMenusPath(v.children, allRoutes) } if (data) { res.push(data) } if (allParentPath.length && Reflect.has(tabPathMap, allParentPath[0])) { tabPathMap[allParentPath[0]].push(fullPath) } } } return res } ================================================ FILE: yshop-drink-vue3/src/layout/components/TagsView/index.ts ================================================ import TagsView from './src/TagsView.vue' export { TagsView } ================================================ FILE: yshop-drink-vue3/src/layout/components/TagsView/src/TagsView.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/TagsView/src/helper.ts ================================================ import type { RouteMeta, RouteLocationNormalizedLoaded } from 'vue-router' import { pathResolve } from '@/utils/routerHelper' export const filterAffixTags = (routes: AppRouteRecordRaw[], parentPath = '') => { let tags: RouteLocationNormalizedLoaded[] = [] routes.forEach((route) => { const meta = route.meta as RouteMeta const tagPath = pathResolve(parentPath, route.path) if (meta?.affix) { tags.push({ ...route, path: tagPath, fullPath: tagPath } as RouteLocationNormalizedLoaded) } if (route.children) { const tempTags: RouteLocationNormalizedLoaded[] = filterAffixTags(route.children, tagPath) if (tempTags.length >= 1) { tags = [...tags, ...tempTags] } } }) return tags } ================================================ FILE: yshop-drink-vue3/src/layout/components/ThemeSwitch/index.ts ================================================ import ThemeSwitch from './src/ThemeSwitch.vue' export { ThemeSwitch } ================================================ FILE: yshop-drink-vue3/src/layout/components/ThemeSwitch/src/ThemeSwitch.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/ToolHeader.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/UserInfo/index.ts ================================================ import UserInfo from './src/UserInfo.vue' export { UserInfo } ================================================ FILE: yshop-drink-vue3/src/layout/components/UserInfo/src/UserInfo.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/UserInfo/src/components/LockDialog.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/UserInfo/src/components/LockPage.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/layout/components/useRenderLayout.tsx ================================================ import { computed } from 'vue' import { useAppStore } from '@/store/modules/app' import { Menu } from '@/layout/components/Menu' import { TabMenu } from '@/layout/components/TabMenu' import { TagsView } from '@/layout/components/TagsView' import { Logo } from '@/layout/components/Logo' import AppView from './AppView.vue' import ToolHeader from './ToolHeader.vue' import { ElScrollbar } from 'element-plus' import { useDesign } from '@/hooks/web/useDesign' const { getPrefixCls } = useDesign() const prefixCls = getPrefixCls('layout') const appStore = useAppStore() const pageLoading = computed(() => appStore.getPageLoading) // 标签页 const tagsView = computed(() => appStore.getTagsView) // 菜单折叠 const collapse = computed(() => appStore.getCollapse) // logo const logo = computed(() => appStore.logo) // 固定头部 const fixedHeader = computed(() => appStore.getFixedHeader) // 是否是移动端 const mobile = computed(() => appStore.getMobile) // 固定菜单 const fixedMenu = computed(() => appStore.getFixedMenu) export const useRenderLayout = () => { const renderClassic = () => { return ( <>
    {logo.value ? ( ) : undefined}
    {tagsView.value ? ( ) : undefined}
    ) } const renderTopLeft = () => { return ( <>
    {logo.value ? : undefined}
    {tagsView.value ? ( ) : undefined}
    ) } const renderTop = () => { return ( <>
    {logo.value ? : undefined}
    {tagsView.value ? ( ) : undefined}
    ) } const renderCutMenu = () => { return ( <>
    {logo.value ? : undefined}
    {tagsView.value ? ( ) : undefined}
    ) } return { renderClassic, renderTopLeft, renderTop, renderCutMenu } } ================================================ FILE: yshop-drink-vue3/src/locales/en.ts ================================================ export default { common: { inputText: 'Please input', selectText: 'Please select', startTimeText: 'Start time', endTimeText: 'End time', login: 'Login', required: 'This is required', loginOut: 'Login out', document: 'Document', profile: 'User Center', reminder: 'Reminder', loginOutMessage: 'Exit the system?', back: 'Back', ok: 'OK', save: 'Save', cancel: 'Cancel', close: 'Close', reload: 'Reload current', success: 'Success', closeTab: 'Close current', closeTheLeftTab: 'Close left', closeTheRightTab: 'Close right', closeOther: 'Close other', closeAll: 'Close all', prevLabel: 'Prev', nextLabel: 'Next', skipLabel: 'Jump', doneLabel: 'End', menu: 'Menu', menuDes: 'Menu bar rendered in routed structure', collapse: 'Collapse', collapseDes: 'Expand and zoom the menu bar', tagsView: 'Tags view', tagsViewDes: 'Used to record routing history', tool: 'Tool', toolDes: 'Used to set up custom systems', query: 'Query', reset: 'Reset', shrink: 'Put away', expand: 'Expand', confirmTitle: 'System Hint', exportMessage: 'Whether to confirm export data item?', importMessage: 'Whether to confirm import data item?', createSuccess: 'Create Success', updateSuccess: 'Update Success', delMessage: 'Delete the selected data?', delDataMessage: 'Delete the data?', delNoData: 'Please select the data to delete', delSuccess: 'Deleted successfully', index: 'Index', status: 'Status', createTime: 'Create Time', updateTime: 'Update Time', copy: 'Copy', copySuccess: 'Copy Success', copyError: 'Copy Error' }, lock: { lockScreen: 'Lock screen', lock: 'Lock', lockPassword: 'Lock screen password', unlock: 'Click to unlock', backToLogin: 'Back to login', entrySystem: 'Entry the system', placeholder: 'Please enter the lock screen password', message: 'Lock screen password error' }, error: { noPermission: `Sorry, you don't have permission to access this page.`, pageError: 'Sorry, the page you visited does not exist.', networkError: 'Sorry, the server reported an error.', returnToHome: 'Return to home' }, permission: { hasPermission: `Please set the operation permission label value`, hasRole: `Please set the role permission tag value` }, setting: { projectSetting: 'Project setting', theme: 'Theme', layout: 'Layout', systemTheme: 'System theme', menuTheme: 'Menu theme', interfaceDisplay: 'Interface display', breadcrumb: 'Breadcrumb', breadcrumbIcon: 'Breadcrumb icon', collapseMenu: 'Collapse menu', hamburgerIcon: 'Hamburger icon', screenfullIcon: 'Screenfull icon', sizeIcon: 'Size icon', localeIcon: 'Locale icon', messageIcon: 'Message icon', tagsView: 'Tags view', logo: 'Logo', greyMode: 'Grey mode', fixedHeader: 'Fixed header', headerTheme: 'Header theme', cutMenu: 'Cut Menu', copy: 'Copy', clearAndReset: 'Clear cache and reset', copySuccess: 'Copy success', copyFailed: 'Copy failed', footer: 'Footer', uniqueOpened: 'Unique opened', tagsViewIcon: 'Tags view icon', reExperienced: 'Please exit the login experience again', fixedMenu: 'Fixed menu' }, size: { default: 'Default', large: 'Large', small: 'Small' }, login: { welcome: 'Welcome to the system', message: 'Backstage management system', tenantname: 'TenantName', username: 'Username', password: 'Password', code: 'verification code', login: 'Sign in', relogin: 'Sign in again', otherLogin: 'Sign in with', register: 'Register', checkPassword: 'Confirm password', remember: 'Remember me', hasUser: 'Existing account? Go to login', forgetPassword: 'Forget password?', tenantNamePlaceholder: 'Please Enter Tenant Name', usernamePlaceholder: 'Please Enter Username', passwordPlaceholder: 'Please Enter Password', codePlaceholder: 'Please Enter Verification Code', mobileTitle: 'Mobile sign in', mobileNumber: 'Mobile Number', mobileNumberPlaceholder: 'Plaease Enter Mobile Number', backLogin: 'back', getSmsCode: 'Get SMS Code', btnMobile: 'Mobile sign in', btnQRCode: 'QR code sign in', qrcode: 'Scan the QR code to log in', btnRegister: 'Sign up', SmsSendMsg: 'code has been sent' }, captcha: { verification: 'Please complete security verification', slide: 'Swipe right to complete verification', point: 'Please click', success: 'Verification succeeded', fail: 'verification failed' }, router: { login: 'Login', home: 'Home', analysis: 'Analysis', workplace: 'Workplace' }, analysis: { newUser: 'New user', unreadInformation: 'Unread information', transactionAmount: 'Transaction amount', totalShopping: 'Total Shopping', monthlySales: 'Monthly sales', userAccessSource: 'User access source', january: 'January', february: 'February', march: 'March', april: 'April', may: 'May', june: 'June', july: 'July', august: 'August', september: 'September', october: 'October', november: 'November', december: 'December', estimate: 'Estimate', actual: 'Actual', directAccess: 'Airect access', mailMarketing: 'Mail marketing', allianceAdvertising: 'Alliance advertising', videoAdvertising: 'Video advertising', searchEngines: 'Search engines', weeklyUserActivity: 'Weekly user activity', activeQuantity: 'Active quantity', monday: 'Monday', tuesday: 'Tuesday', wednesday: 'Wednesday', thursday: 'Thursday', friday: 'Friday', saturday: 'Saturday', sunday: 'Sunday' }, workplace: { welcome: 'Hello', happyDay: 'Wish you happy every day!', toady: `It's sunny today`, notice: 'Announcement', project: 'Project', access: 'Project access', toDo: 'To do', introduction: 'A serious introduction', shortcutOperation: 'Quick entry', operation: 'Operation', index: 'Index', personal: 'Personal', team: 'Team', quote: 'Quote', contribution: 'Contribution', hot: 'Hot', yield: 'Yield', dynamic: 'Dynamic', push: 'push', follow: 'Follow' }, form: { input: 'Input', inputNumber: 'InputNumber', default: 'Default', icon: 'Icon', mixed: 'Mixed', textarea: 'Textarea', slot: 'Slot', position: 'Position', autocomplete: 'Autocomplete', select: 'Select', selectGroup: 'Select Group', selectV2: 'SelectV2', cascader: 'Cascader', switch: 'Switch', rate: 'Rate', colorPicker: 'Color Picker', transfer: 'Transfer', render: 'Render', radio: 'Radio', button: 'Button', checkbox: 'Checkbox', slider: 'Slider', datePicker: 'Date Picker', shortcuts: 'Shortcuts', today: 'Today', yesterday: 'Yesterday', aWeekAgo: 'A week ago', week: 'Week', year: 'Year', month: 'Month', dates: 'Dates', daterange: 'Date Range', monthrange: 'Month Range', dateTimePicker: 'DateTimePicker', dateTimerange: 'Datetime Range', timePicker: 'Time Picker', timeSelect: 'Time Select', inputPassword: 'input Password', passwordStrength: 'Password Strength', operate: 'operate', change: 'Change', restore: 'Restore', disabled: 'Disabled', disablement: 'Disablement', delete: 'Delete', add: 'Add', setValue: 'Set value', resetValue: 'Reset value', set: 'Set', subitem: 'Subitem', formValidation: 'Form validation', verifyReset: 'Verify reset', remark: 'Remark' }, watermark: { watermark: 'Watermark' }, table: { table: 'Table', index: 'Index', title: 'Title', author: 'Author', createTime: 'Create time', action: 'Action', pagination: 'pagination', reserveIndex: 'Reserve index', restoreIndex: 'Restore index', showSelections: 'Show selections', hiddenSelections: 'Restore selections', showExpandedRows: 'Show expanded rows', hiddenExpandedRows: 'Hidden expanded rows', header: 'Header' }, action: { create: 'Create', add: 'Add', del: 'Delete', delete: 'Delete', edit: 'Edit', update: 'Update', preview: 'Preview', more: 'More', sync: 'Sync', save: 'Save', detail: 'Detail', export: 'Export', import: 'Import', generate: 'Generate', logout: 'Login Out', test: 'Test', typeCreate: 'Dict Type Create', typeUpdate: 'Dict Type Eidt', dataCreate: 'Dict Data Create', dataUpdate: 'Dict Data Eidt', fileUpload: 'File Upload' }, dialog: { dialog: 'Dialog', open: 'Open', close: 'Close' }, sys: { api: { operationFailed: 'Operation failed', errorTip: 'Error Tip', errorMessage: 'The operation failed, the system is abnormal!', timeoutMessage: 'Login timed out, please log in again!', apiTimeoutMessage: 'The interface request timed out, please refresh the page and try again!', apiRequestFailed: 'The interface request failed, please try again later!', networkException: 'network anomaly', networkExceptionMsg: 'Please check if your network connection is normal! The network is abnormal', errMsg401: 'The user does not have permission (token, user name, password error)!', errMsg403: 'The user is authorized, but access is forbidden!', errMsg404: 'Network request error, the resource was not found!', errMsg405: 'Network request error, request method not allowed!', errMsg408: 'Network request timed out!', errMsg500: 'Server error, please contact the administrator!', errMsg501: 'The network is not implemented!', errMsg502: 'Network Error!', errMsg503: 'The service is unavailable, the server is temporarily overloaded or maintained!', errMsg504: 'Network timeout!', errMsg505: 'The http version does not support the request!', errMsg901: 'Demo mode, no write operations are possible!' }, app: { logoutTip: 'Reminder', logoutMessage: 'Confirm to exit the system?', menuLoading: 'Menu loading...' }, exception: { backLogin: 'Back Login', backHome: 'Back Home', subTitle403: "Sorry, you don't have access to this page.", subTitle404: 'Sorry, the page you visited does not exist.', subTitle500: 'Sorry, the server is reporting an error.', noDataTitle: 'No data on the current page.', networkErrorTitle: 'Network Error', networkErrorSubTitle: 'Sorry, Your network connection has been disconnected, please check your network!' }, lock: { unlock: 'Click to unlock', alert: 'Lock screen password error', backToLogin: 'Back to login', entry: 'Enter the system', placeholder: 'Please enter the lock screen password or user password' }, login: { backSignIn: 'Back sign in', mobileSignInFormTitle: 'Mobile sign in', qrSignInFormTitle: 'Qr code sign in', signInFormTitle: 'Sign in', signUpFormTitle: 'Sign up', forgetFormTitle: 'Reset password', signInTitle: 'Backstage management system', signInDesc: 'Enter your personal details and get started!', policy: 'I agree to the xxx Privacy Policy', scanSign: `scanning the code to complete the login`, loginButton: 'Sign in', registerButton: 'Sign up', rememberMe: 'Remember me', forgetPassword: 'Forget Password?', otherSignIn: 'Sign in with', // notify loginSuccessTitle: 'Login successful', loginSuccessDesc: 'Welcome back', // placeholder accountPlaceholder: 'Please input username', passwordPlaceholder: 'Please input password', smsPlaceholder: 'Please input sms code', mobilePlaceholder: 'Please input mobile', policyPlaceholder: 'Register after checking', diffPwd: 'The two passwords are inconsistent', userName: 'Username', password: 'Password', confirmPassword: 'Confirm Password', email: 'Email', smsCode: 'SMS code', mobile: 'Mobile' } }, profile: { user: { title: 'Personal Information', username: 'User Name', nickname: 'Nick Name', mobile: 'Phone Number', email: 'User Mail', dept: 'Department', posts: 'Position', roles: 'Own Role', sex: 'Sex', man: 'Man', woman: 'Woman', createTime: 'Created Date' }, info: { title: 'Basic Information', basicInfo: 'Basic Information', resetPwd: 'Reset Password', userSocial: 'Social Information' }, rules: { nickname: 'Please Enter User Nickname', mail: 'Please Input The Email Address', truemail: 'Please Input The Correct Email Address', phone: 'Please Enter The Phone Number', truephone: 'Please Enter The Correct Phone Number' }, password: { oldPassword: 'Old PassWord', newPassword: 'New Password', confirmPassword: 'Confirm Password', oldPwdMsg: 'Please Enter Old Password', newPwdMsg: 'Please Enter New Password', cfPwdMsg: 'Please Enter Confirm Password', diffPwd: 'The Passwords Entered Twice No Match' } }, cropper: { selectImage: 'Select Image', uploadSuccess: 'Uploaded success!', modalTitle: 'Avatar upload', okText: 'Confirm and upload', btn_reset: 'Reset', btn_rotate_left: 'Counterclockwise rotation', btn_rotate_right: 'Clockwise rotation', btn_scale_x: 'Flip horizontal', btn_scale_y: 'Flip vertical', btn_zoom_in: 'Zoom in', btn_zoom_out: 'Zoom out', preview: 'Preivew' } } ================================================ FILE: yshop-drink-vue3/src/locales/zh-CN.ts ================================================ export default { common: { inputText: '请输入', selectText: '请选择', startTimeText: '开始时间', endTimeText: '结束时间', login: '登录', required: '该项为必填项', loginOut: '退出系统', document: '项目文档', profile: '个人中心', reminder: '温馨提示', loginOutMessage: '是否退出本系统?', back: '返回', ok: '确定', save: '保存', cancel: '取消', close: '关闭', reload: '重新加载', success: '成功', closeTab: '关闭标签页', closeTheLeftTab: '关闭左侧标签页', closeTheRightTab: '关闭右侧标签页', closeOther: '关闭其他标签页', closeAll: '关闭全部标签页', prevLabel: '上一步', nextLabel: '下一步', skipLabel: '跳过', doneLabel: '结束', menu: '菜单', menuDes: '以路由的结构渲染的菜单栏', collapse: '展开缩收', collapseDes: '展开和缩放菜单栏', tagsView: '标签页', tagsViewDes: '用于记录路由历史记录', tool: '工具', toolDes: '用于设置定制系统', query: '查询', reset: '重置', shrink: '收起', expand: '展开', confirmTitle: '系统提示', exportMessage: '是否确认导出数据项?', importMessage: '是否确认导入数据项?', createSuccess: '新增成功', updateSuccess: '修改成功', delMessage: '是否删除所选中数据?', delDataMessage: '是否删除数据?', delNoData: '请选择需要删除的数据', delSuccess: '删除成功', index: '序号', status: '状态', createTime: '创建时间', updateTime: '更新时间', copy: '复制', copySuccess: '复制成功', copyError: '复制失败' }, lock: { lockScreen: '锁定屏幕', lock: '锁定', lockPassword: '锁屏密码', unlock: '点击解锁', backToLogin: '返回登录', entrySystem: '进入系统', placeholder: '请输入锁屏密码', message: '锁屏密码错误' }, error: { noPermission: `抱歉,您无权访问此页面。`, pageError: '抱歉,您访问的页面不存在。', networkError: '抱歉,服务器报告错误。', returnToHome: '返回首页' }, permission: { hasPermission: `请设置操作权限标签值`, hasRole: `请设置角色权限标签值` }, setting: { projectSetting: '项目配置', theme: '主题', layout: '布局', systemTheme: '系统主题', menuTheme: '菜单主题', interfaceDisplay: '界面显示', breadcrumb: '面包屑', breadcrumbIcon: '面包屑图标', collapseMenu: '折叠菜单', hamburgerIcon: '折叠图标', screenfullIcon: '全屏图标', sizeIcon: '尺寸图标', localeIcon: '多语言图标', messageIcon: '消息图标', tagsView: '标签页', logo: '标志', greyMode: '灰色模式', fixedHeader: '固定头部', headerTheme: '头部主题', cutMenu: '切割菜单', copy: '拷贝', clearAndReset: '清除缓存并且重置', copySuccess: '拷贝成功', copyFailed: '拷贝失败', footer: '页脚', uniqueOpened: '菜单手风琴', tagsViewIcon: '标签页图标', reExperienced: '请重新退出登录体验', fixedMenu: '固定菜单' }, size: { default: '默认', large: '大', small: '小' }, login: { welcome: '欢迎使用yshop意象点餐系统', message: '欢迎使用yshop意象点餐系统', tenantname: '租户名称', username: '用户名', password: '密码', code: '验证码', login: '登录', relogin: '重新登录', otherLogin: '其他登录方式', register: '注册', checkPassword: '确认密码', remember: '记住我', hasUser: '已有账号?去登录', forgetPassword: '忘记密码?', tenantNamePlaceholder: '请输入租户名称', usernamePlaceholder: '请输入用户名', passwordPlaceholder: '请输入密码', codePlaceholder: '请输入验证码', mobileTitle: '手机登录', mobileNumber: '手机号码', mobileNumberPlaceholder: '请输入手机号码', backLogin: '返回', getSmsCode: '获取验证码', btnMobile: '手机登录', btnQRCode: '二维码登录', qrcode: '扫描二维码登录', btnRegister: '注册', SmsSendMsg: '验证码已发送' }, captcha: { verification: '请完成安全验证', slide: '向右滑动完成验证', point: '请依次点击', success: '验证成功', fail: '验证失败' }, router: { login: '登录', socialLogin: '社交登录', home: '首页', analysis: '分析页', workplace: '工作台' }, analysis: { newUser: '新增用户', unreadInformation: '未读消息', transactionAmount: '成交金额', totalShopping: '购物总量', monthlySales: '每月销售额', userAccessSource: '用户访问来源', january: '一月', february: '二月', march: '三月', april: '四月', may: '五月', june: '六月', july: '七月', august: '八月', september: '九月', october: '十月', november: '十一月', december: '十二月', estimate: '预计', actual: '实际', directAccess: '直接访问', mailMarketing: '邮件营销', allianceAdvertising: '联盟广告', videoAdvertising: '视频广告', searchEngines: '搜索引擎', weeklyUserActivity: '每周用户活跃量', activeQuantity: '活跃量', monday: '周一', tuesday: '周二', wednesday: '周三', thursday: '周四', friday: '周五', saturday: '周六', sunday: '周日' }, workplace: { welcome: '你好', happyDay: '祝你开心每一天!', toady: '今日晴', notice: '通知公告', project: '项目数', access: '项目访问', toDo: '待办', introduction: '一个正经的简介', shortcutOperation: '快捷入口', operation: '操作', index: '指数', personal: '个人', team: '团队', quote: '引用', contribution: '贡献', hot: '热度', yield: '产量', dynamic: '动态', push: '推送', follow: '关注' }, form: { input: '输入框', inputNumber: '数字输入框', default: '默认', icon: '图标', mixed: '复合型', textarea: '多行文本', slot: '插槽', position: '位置', autocomplete: '自动补全', select: '选择器', selectGroup: '选项分组', selectV2: '虚拟列表选择器', cascader: '级联选择器', switch: '开关', rate: '评分', colorPicker: '颜色选择器', transfer: '穿梭框', render: '渲染器', radio: '单选框', button: '按钮', checkbox: '多选框', slider: '滑块', datePicker: '日期选择器', shortcuts: '快捷选项', today: '今天', yesterday: '昨天', aWeekAgo: '一周前', week: '周', year: '年', month: '月', dates: '日期', daterange: '日期范围', monthrange: '月份范围', dateTimePicker: '日期时间选择器', dateTimerange: '日期时间范围', timePicker: '时间选择器', timeSelect: '时间选择', inputPassword: '密码输入框', passwordStrength: '密码强度', operate: '操作', change: '更改', restore: '还原', disabled: '禁用', disablement: '解除禁用', delete: '删除', add: '添加', setValue: '设置值', resetValue: '重置值', set: '设置', subitem: '子项', formValidation: '表单验证', verifyReset: '验证重置', remark: '备注' }, watermark: { watermark: '水印' }, table: { table: '表格', index: '序号', title: '标题', author: '作者', createTime: '创建时间', action: '操作', pagination: '分页', reserveIndex: '叠加序号', restoreIndex: '还原序号', showSelections: '显示多选', hiddenSelections: '隐藏多选', showExpandedRows: '显示展开行', hiddenExpandedRows: '隐藏展开行', header: '头部' }, action: { setPrint: '配置打印机', check: '审核', buyDetail: '购买记录', order: '订单', qrcode: '二维码', couponRecord: '优惠券领取记录', yue: '积分余额', userDetail: '用户详情', refundOrder: '订单退款', orderRecord: '订单记录', orderDetail: '订单详情', updateOrder: '修改订单', orderSend: '订单发货', remark: '备注', sendInfo: '配送信息', batchCreate: '批量新增', create: '新增', add: '新增', del: '删除', delete: '删除', edit: '编辑', update: '编辑', preview: '预览', more: '更多', sync: '同步', save: '保存', detail: '详情', export: '导出', import: '导入', generate: '生成', logout: '强制退出', test: '测试', typeCreate: '字典类型新增', typeUpdate: '字典类型编辑', dataCreate: '字典数据新增', dataUpdate: '字典数据编辑' }, dialog: { dialog: '弹窗', open: '打开', close: '关闭' }, sys: { api: { operationFailed: '操作失败', errorTip: '错误提示', errorMessage: '操作失败,系统异常!', timeoutMessage: '登录超时,请重新登录!', apiTimeoutMessage: '接口请求超时,请刷新页面重试!', apiRequestFailed: '请求出错,请稍候重试', networkException: '网络异常', networkExceptionMsg: '网络异常,请检查您的网络连接是否正常!', errMsg401: '用户没有权限(令牌、用户名、密码错误)!', errMsg403: '用户得到授权,但是访问是被禁止的。!', errMsg404: '网络请求错误,未找到该资源!', errMsg405: '网络请求错误,请求方法未允许!', errMsg408: '网络请求超时!', errMsg500: '服务器错误,请联系管理员!', errMsg501: '网络未实现!', errMsg502: '网络错误!', errMsg503: '服务不可用,服务器暂时过载或维护!', errMsg504: '网络超时!', errMsg505: 'http版本不支持该请求!', errMsg901: '演示模式,无法进行写操作!' }, app: { logoutTip: '温馨提醒', logoutMessage: '是否确认退出系统?', menuLoading: '菜单加载中...' }, exception: { backLogin: '返回登录', backHome: '返回首页', subTitle403: '抱歉,您无权访问此页面。', subTitle404: '抱歉,您访问的页面不存在。', subTitle500: '抱歉,服务器报告错误。', noDataTitle: '当前页无数据', networkErrorTitle: '网络错误', networkErrorSubTitle: '抱歉,您的网络连接已断开,请检查您的网络!' }, lock: { unlock: '点击解锁', alert: '锁屏密码错误', backToLogin: '返回登录', entry: '进入系统', placeholder: '请输入锁屏密码或者用户密码' }, login: { backSignIn: '返回', signInFormTitle: '登录', ssoFormTitle: '三方授权', mobileSignInFormTitle: '手机登录', qrSignInFormTitle: '二维码登录', signUpFormTitle: '注册', forgetFormTitle: '重置密码', signInTitle: '开箱即用的中后台管理系统', signInDesc: '输入您的个人详细信息开始使用!', policy: '我同意xxx隐私政策', scanSign: `扫码后点击"确认",即可完成登录`, loginButton: '登录', registerButton: '注册', rememberMe: '记住我', forgetPassword: '忘记密码?', otherSignIn: '其他登录方式', // notify loginSuccessTitle: '登录成功', loginSuccessDesc: '欢迎回来', // placeholder accountPlaceholder: '请输入账号', passwordPlaceholder: '请输入密码', smsPlaceholder: '请输入验证码', mobilePlaceholder: '请输入手机号码', policyPlaceholder: '勾选后才能注册', diffPwd: '两次输入密码不一致', userName: '账号', password: '密码', confirmPassword: '确认密码', email: '邮箱', smsCode: '短信验证码', mobile: '手机号码' } }, profile: { user: { title: '个人信息', username: '用户名称', nickname: '用户昵称', mobile: '手机号码', email: '用户邮箱', dept: '所属部门', posts: '所属岗位', roles: '所属角色', sex: '性别', man: '男', woman: '女', createTime: '创建日期' }, info: { title: '基本信息', basicInfo: '基本资料', resetPwd: '修改密码', userSocial: '社交信息' }, rules: { nickname: '请输入用户昵称', mail: '请输入邮箱地址', truemail: '请输入正确的邮箱地址', phone: '请输入正确的手机号码', truephone: '请输入正确的手机号码' }, password: { oldPassword: '旧密码', newPassword: '新密码', confirmPassword: '确认密码', oldPwdMsg: '请输入旧密码', newPwdMsg: '请输入新密码', cfPwdMsg: '请输入确认密码', pwdRules: '长度在 6 到 20 个字符', diffPwd: '两次输入密码不一致' } }, cropper: { selectImage: '选择图片', uploadSuccess: '上传成功', modalTitle: '头像上传', okText: '确认并上传', btn_reset: '重置', btn_rotate_left: '逆时针旋转', btn_rotate_right: '顺时针旋转', btn_scale_x: '水平翻转', btn_scale_y: '垂直翻转', btn_zoom_in: '放大', btn_zoom_out: '缩小', preview: '预览' }, 'OAuth 2.0': 'OAuth 2.0' // 避免菜单名是 OAuth 2.0 时,一直 warn 报错 } ================================================ FILE: yshop-drink-vue3/src/main.ts ================================================ // 引入unocss css import '@/plugins/unocss' // 导入全局的svg图标 import '@/plugins/svgIcon' // 初始化多语言 import { setupI18n } from '@/plugins/vueI18n' // 引入状态管理 import { setupStore } from '@/store' // 全局组件 import { setupGlobCom } from '@/components' // 引入 element-plus import { setupElementPlus } from '@/plugins/elementPlus' // 引入 form-create import { setupFormCreate } from '@/plugins/formCreate' // 引入全局样式 import '@/styles/index.scss' // 引入动画 import '@/plugins/animate.css' // 路由 import router, { setupRouter } from '@/router' // 权限 import { setupAuth } from '@/directives' import { createApp } from 'vue' import App from './App.vue' import './permission' import '@/plugins/tongji' // 百度统计 import Logger from '@/utils/Logger' import VueUeditorWrap from 'vue-ueditor-wrap' import VueDOMPurifyHTML from 'vue-dompurify-html' // 解决v-html 的安全隐患 // 创建实例 const setupAll = async () => { const app = createApp(App) app.use(VueUeditorWrap) await setupI18n(app) setupStore(app) setupGlobCom(app) setupElementPlus(app) setupFormCreate(app) setupRouter(app) setupAuth(app) await router.isReady() app.use(VueDOMPurifyHTML) app.mount('#app') } setupAll() Logger.prettyPrimary(`欢迎使用`, import.meta.env.VITE_APP_TITLE) ================================================ FILE: yshop-drink-vue3/src/permission.ts ================================================ import router from './router' import type { RouteRecordRaw } from 'vue-router' import { isRelogin } from '@/config/axios/service' import { getAccessToken } from '@/utils/auth' import { useTitle } from '@/hooks/web/useTitle' import { useNProgress } from '@/hooks/web/useNProgress' import { usePageLoading } from '@/hooks/web/usePageLoading' import { useDictStoreWithOut } from '@/store/modules/dict' import { useUserStoreWithOut } from '@/store/modules/user' import { usePermissionStoreWithOut } from '@/store/modules/permission' const { start, done } = useNProgress() const { loadStart, loadDone } = usePageLoading() const parseURL = ( url: string | null | undefined ): { basePath: string; paramsObject: { [key: string]: string } } => { // 如果输入为 null 或 undefined,返回空字符串和空对象 if (url == null) { return { basePath: '', paramsObject: {} } } // 找到问号 (?) 的位置,它之前是基础路径,之后是查询参数 const questionMarkIndex = url.indexOf('?') let basePath = url const paramsObject: { [key: string]: string } = {} // 如果找到了问号,说明有查询参数 if (questionMarkIndex !== -1) { // 获取 basePath basePath = url.substring(0, questionMarkIndex) // 从 URL 中获取查询字符串部分 const queryString = url.substring(questionMarkIndex + 1) // 使用 URLSearchParams 遍历参数 const searchParams = new URLSearchParams(queryString) searchParams.forEach((value, key) => { // 封装进 paramsObject 对象 paramsObject[key] = value }) } // 返回 basePath 和 paramsObject return { basePath, paramsObject } } // 路由不重定向白名单 const whiteList = [ '/login', '/social-login', '/auth-redirect', '/bind', '/register', '/oauthLogin/gitee' ] // 路由加载前 router.beforeEach(async (to, from, next) => { start() loadStart() if (getAccessToken()) { if (to.path === '/login') { next({ path: '/' }) } else { // 获取所有字典 const dictStore = useDictStoreWithOut() const userStore = useUserStoreWithOut() const permissionStore = usePermissionStoreWithOut() if (!dictStore.getIsSetDict) { await dictStore.setDictMap() } if (!userStore.getIsSetUser) { isRelogin.show = true await userStore.setUserInfoAction() isRelogin.show = false // 后端过滤菜单 await permissionStore.generateRoutes() permissionStore.getAddRouters.forEach((route) => { router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表 }) const redirectPath = from.query.redirect || to.path // 修复跳转时不带参数的问题 const redirect = decodeURIComponent(redirectPath as string) const { basePath, paramsObject: query } = parseURL(redirect) const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect, query } next(nextData) } else { next() } } } else { if (whiteList.indexOf(to.path) !== -1) { next() } else { next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页 } } }) router.afterEach((to) => { useTitle(to?.meta?.title as string) done() // 结束Progress loadDone() }) ================================================ FILE: yshop-drink-vue3/src/plugins/animate.css/index.ts ================================================ import 'animate.css' ================================================ FILE: yshop-drink-vue3/src/plugins/echarts/index.ts ================================================ import * as echarts from 'echarts/core' import { BarChart, FunnelChart, GaugeChart, LineChart, MapChart, PictorialBarChart, PieChart, RadarChart } from 'echarts/charts' import { AriaComponent, GridComponent, LegendComponent, ParallelComponent, PolarComponent, TitleComponent, ToolboxComponent, TooltipComponent, VisualMapComponent } from 'echarts/components' import { CanvasRenderer } from 'echarts/renderers' echarts.use([ LegendComponent, TitleComponent, TooltipComponent, ToolboxComponent, GridComponent, PolarComponent, AriaComponent, ParallelComponent, VisualMapComponent, BarChart, LineChart, PieChart, MapChart, CanvasRenderer, PictorialBarChart, RadarChart, GaugeChart, FunnelChart ]) export default echarts ================================================ FILE: yshop-drink-vue3/src/plugins/elementPlus/index.ts ================================================ import type { App } from 'vue' // 需要全局引入一些组件,如ElScrollbar,不然一些下拉项样式有问题 import { ElLoading, ElScrollbar, ElButton } from 'element-plus' const plugins = [ElLoading] const components = [ElScrollbar, ElButton] export const setupElementPlus = (app: App) => { plugins.forEach((plugin) => { app.use(plugin) }) components.forEach((component) => { app.component(component.name, component) }) } ================================================ FILE: yshop-drink-vue3/src/plugins/formCreate/index.ts ================================================ import type { App } from 'vue' // 👇使用 form-create 需额外全局引入 element plus 组件 import { ElAlert, ElAside, ElContainer, ElDivider, ElHeader, ElMain, ElPopconfirm, ElTable, ElTableColumn, ElTabPane, ElTabs, ElTransfer } from 'element-plus' import FcDesigner from '@form-create/designer' import formCreate from '@form-create/element-ui' import install from '@form-create/element-ui/auto-import' //======================= 自定义组件 ======================= import { UploadFile, UploadImg, UploadImgs } from '@/components/UploadFile' import { useApiSelect } from '@/components/FormCreate' import { Editor } from '@/components/Editor' import DictSelect from '@/components/FormCreate/src/components/DictSelect.vue' const UserSelect = useApiSelect({ name: 'UserSelect', labelField: 'nickname', valueField: 'id', url: '/system/user/simple-list' }) const DeptSelect = useApiSelect({ name: 'DeptSelect', labelField: 'name', valueField: 'id', url: '/system/dept/simple-list' }) const ApiSelect = useApiSelect({ name: 'ApiSelect' }) const components = [ ElAside, ElPopconfirm, ElHeader, ElMain, ElContainer, ElDivider, ElTransfer, ElAlert, ElTabs, ElTable, ElTableColumn, ElTabPane, UploadImg, UploadImgs, UploadFile, DictSelect, UserSelect, DeptSelect, ApiSelect, Editor ] // 参考 http://www.form-create.com/v3/element-ui/auto-import.html 文档 export const setupFormCreate = (app: App) => { components.forEach((component) => { app.component(component.name, component) }) formCreate.use(install) app.use(formCreate) app.use(FcDesigner) } ================================================ FILE: yshop-drink-vue3/src/plugins/svgIcon/index.ts ================================================ import 'virtual:svg-icons-register' import '@purge-icons/generated' ================================================ FILE: yshop-drink-vue3/src/plugins/tongji/index.ts ================================================ import router from '@/router' // 用于 router push window._hmt = window._hmt || [] // HM_ID const HM_ID = import.meta.env.VITE_APP_BAIDU_CODE ;(function () { // 有值的时候,才开启 if (!HM_ID) { return } const hm = document.createElement('script') hm.src = 'https://hm.baidu.com/hm.js?' + HM_ID const s = document.getElementsByTagName('script')[0] s.parentNode.insertBefore(hm, s) })() router.afterEach(function (to) { if (!HM_ID) { return } _hmt.push(['_trackPageview', to.fullPath]) }) ================================================ FILE: yshop-drink-vue3/src/plugins/unocss/index.ts ================================================ import 'virtual:uno.css' ================================================ FILE: yshop-drink-vue3/src/plugins/vueI18n/helper.ts ================================================ export const setHtmlPageLang = (locale: LocaleType) => { document.querySelector('html')?.setAttribute('lang', locale) } ================================================ FILE: yshop-drink-vue3/src/plugins/vueI18n/index.ts ================================================ import type { App } from 'vue' import { createI18n } from 'vue-i18n' import { useLocaleStoreWithOut } from '@/store/modules/locale' import type { I18n, I18nOptions } from 'vue-i18n' import { setHtmlPageLang } from './helper' export let i18n: ReturnType const createI18nOptions = async (): Promise => { const localeStore = useLocaleStoreWithOut() const locale = localeStore.getCurrentLocale const localeMap = localeStore.getLocaleMap const defaultLocal = await import(`../../locales/${locale.lang}.ts`) const message = defaultLocal.default ?? {} setHtmlPageLang(locale.lang) localeStore.setCurrentLocale({ lang: locale.lang // elLocale: elLocal }) return { legacy: false, locale: locale.lang, fallbackLocale: locale.lang, messages: { [locale.lang]: message }, availableLocales: localeMap.map((v) => v.lang), sync: true, silentTranslationWarn: true, missingWarn: false, silentFallbackWarn: true } } export const setupI18n = async (app: App) => { const options = await createI18nOptions() i18n = createI18n(options) as I18n app.use(i18n) } ================================================ FILE: yshop-drink-vue3/src/router/index.ts ================================================ import type { App } from 'vue' import type { RouteRecordRaw } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router' import remainingRouter from './modules/remaining' // 创建路由实例 const router = createRouter({ history: createWebHistory(), // createWebHashHistory URL带#,createWebHistory URL不带# strict: true, routes: remainingRouter as RouteRecordRaw[], scrollBehavior: () => ({ left: 0, top: 0 }) }) export const resetRouter = (): void => { const resetWhiteNameList = ['Redirect', 'Login', 'NoFind', 'Root'] router.getRoutes().forEach((route) => { const { name } = route if (name && !resetWhiteNameList.includes(name as string)) { router.hasRoute(name) && router.removeRoute(name) } }) } export const setupRouter = (app: App) => { app.use(router) } export default router ================================================ FILE: yshop-drink-vue3/src/router/modules/remaining.ts ================================================ import { Layout } from '@/utils/routerHelper' const { t } = useI18n() /** * redirect: noredirect 当设置 noredirect 的时候该路由在面包屑导航中不可被点击 * name:'router-name' 设定路由的名字,一定要填写不然使用时会出现各种问题 * meta : { hidden: true 当设置 true 的时候该路由不会再侧边栏出现 如404,login等页面(默认 false) alwaysShow: true 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式, 只有一个时,会将那个子路由当做根路由显示在侧边栏, 若你想不管路由下面的 children 声明的个数都显示你的根路由, 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则, 一直显示根路由(默认 false) title: 'title' 设置该路由在侧边栏和面包屑中展示的名字 icon: 'svg-name' 设置该路由的图标 noCache: true 如果设置为true,则不会被 缓存(默认 false) breadcrumb: false 如果设置为false,则不会在breadcrumb面包屑中显示(默认 true) affix: true 如果设置为true,则会一直固定在tag项中(默认 false) noTagsView: true 如果设置为true,则不会出现在tag中(默认 false) activeMenu: '/dashboard' 显示高亮的路由路径 followAuth: '/dashboard' 跟随哪个路由进行权限过滤 canTo: true 设置为true即使hidden为true,也依然可以进行路由跳转(默认 false) } **/ const remainingRouter: AppRouteRecordRaw[] = [ { path: '/redirect', component: Layout, name: 'Redirect', children: [ { path: '/redirect/:path(.*)', name: 'Redirect1', component: () => import('@/views/Redirect/Redirect.vue'), meta: {} } ], meta: { hidden: true, noTagsView: true } }, { path: '/', component: Layout, redirect: '/index', name: 'Home', meta: {}, children: [ { path: 'index', component: () => import('@/views/Home/Index.vue'), name: 'Index', meta: { title: t('router.home'), icon: 'ep:home-filled', noCache: false, affix: true } } ] }, { path: '/user', component: Layout, name: 'UserInfo', meta: { hidden: true }, children: [ { path: 'profile', component: () => import('@/views/Profile/Index.vue'), name: 'Profile', meta: { canTo: true, hidden: true, noTagsView: false, icon: 'ep:user', title: t('common.profile') } }, { path: 'notify-message', component: () => import('@/views/system/notify/my/index.vue'), name: 'MyNotifyMessage', meta: { canTo: true, hidden: true, noTagsView: false, icon: 'ep:message', title: '我的站内信' } } ] }, { path: '/dict', component: Layout, name: 'dict', meta: { hidden: true }, children: [ { path: 'type/data/:dictType', component: () => import('@/views/system/dict/data/index.vue'), name: 'SystemDictData', meta: { title: '字典数据', noCache: true, hidden: true, canTo: true, icon: '', activeMenu: '/system/dict' } } ] }, { path: '/codegen', component: Layout, name: 'CodegenEdit', meta: { hidden: true }, children: [ { path: 'edit', component: () => import('@/views/infra/codegen/EditTable.vue'), name: 'InfraCodegenEditTable', meta: { noCache: true, hidden: true, canTo: true, icon: 'ep:edit', title: '修改生成配置', activeMenu: 'infra/codegen/index' } } ] }, { path: '/job', component: Layout, name: 'JobL', meta: { hidden: true }, children: [ { path: 'job-log', component: () => import('@/views/infra/job/logger/index.vue'), name: 'InfraJobLog', meta: { noCache: true, hidden: true, canTo: true, icon: 'ep:edit', title: '调度日志', activeMenu: 'infra/job/index' } } ] }, { path: '/login', component: () => import('@/views/Login/Login.vue'), name: 'Login', meta: { hidden: true, title: t('router.login'), noTagsView: true } }, { path: '/sso', component: () => import('@/views/Login/Login.vue'), name: 'SSOLogin', meta: { hidden: true, title: t('router.login'), noTagsView: true } }, { path: '/social-login', component: () => import('@/views/Login/SocialLogin.vue'), name: 'SocialLogin', meta: { hidden: true, title: t('router.socialLogin'), noTagsView: true } }, { path: '/403', component: () => import('@/views/Error/403.vue'), name: 'NoAccess', meta: { hidden: true, title: '403', noTagsView: true } }, { path: '/404', component: () => import('@/views/Error/404.vue'), name: 'NoFound', meta: { hidden: true, title: '404', noTagsView: true } }, { path: '/500', component: () => import('@/views/Error/500.vue'), name: 'Error', meta: { hidden: true, title: '500', noTagsView: true } }, { path: '/yshop/materia/index', component: () => import('@/components/Materials/src/editorMaterials.vue'), name: 'EditorMaterials', meta: { noCache: true, hidden: true, // canTo: true, title: '上传图片', }, } ] export default remainingRouter ================================================ FILE: yshop-drink-vue3/src/store/index.ts ================================================ import type { App } from 'vue' import { createPinia } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' const store = createPinia() store.use(piniaPluginPersistedstate) export const setupStore = (app: App) => { app.use(store) } export { store } ================================================ FILE: yshop-drink-vue3/src/store/modules/app.ts ================================================ import { defineStore } from 'pinia' import { store } from '../index' import { setCssVar, humpToUnderline } from '@/utils' import { ElMessage } from 'element-plus' import { CACHE_KEY, useCache } from '@/hooks/web/useCache' import { ElementPlusSize } from '@/types/elementPlus' import { LayoutType } from '@/types/layout' import { ThemeTypes } from '@/types/theme' const { wsCache } = useCache() interface AppState { breadcrumb: boolean breadcrumbIcon: boolean collapse: boolean uniqueOpened: boolean hamburger: boolean screenfull: boolean search: boolean size: boolean locale: boolean message: boolean tagsView: boolean tagsViewIcon: boolean logo: boolean fixedHeader: boolean greyMode: boolean pageLoading: boolean layout: LayoutType title: string userInfo: string isDark: boolean currentSize: ElementPlusSize sizeMap: ElementPlusSize[] mobile: boolean footer: boolean theme: ThemeTypes fixedMenu: boolean } export const useAppStore = defineStore('app', { state: (): AppState => { return { userInfo: 'userInfo', // 登录信息存储字段-建议每个项目换一个字段,避免与其他项目冲突 sizeMap: ['default', 'large', 'small'], mobile: false, // 是否是移动端 title: import.meta.env.VITE_APP_TITLE, // 标题 pageLoading: false, // 路由跳转loading breadcrumb: true, // 面包屑 breadcrumbIcon: true, // 面包屑图标 collapse: false, // 折叠菜单 uniqueOpened: true, // 是否只保持一个子菜单的展开 hamburger: true, // 折叠图标 screenfull: true, // 全屏图标 search: true, // 搜索图标 size: true, // 尺寸图标 locale: true, // 多语言图标 message: true, // 消息图标 tagsView: true, // 标签页 tagsViewIcon: true, // 是否显示标签图标 logo: true, // logo fixedHeader: true, // 固定toolheader footer: true, // 显示页脚 greyMode: false, // 是否开始灰色模式,用于特殊悼念日 fixedMenu: wsCache.get('fixedMenu') || false, // 是否固定菜单 layout: wsCache.get(CACHE_KEY.LAYOUT) || 'classic', // layout布局 isDark: wsCache.get(CACHE_KEY.IS_DARK) || false, // 是否是暗黑模式 currentSize: wsCache.get('default') || 'default', // 组件尺寸 theme: wsCache.get(CACHE_KEY.THEME) || { // 主题色 elColorPrimary: '#409eff', // 左侧菜单边框颜色 leftMenuBorderColor: 'inherit', // 左侧菜单背景颜色 leftMenuBgColor: '#001529', // 左侧菜单浅色背景颜色 leftMenuBgLightColor: '#0f2438', // 左侧菜单选中背景颜色 leftMenuBgActiveColor: 'var(--el-color-primary)', // 左侧菜单收起选中背景颜色 leftMenuCollapseBgActiveColor: 'var(--el-color-primary)', // 左侧菜单字体颜色 leftMenuTextColor: '#bfcbd9', // 左侧菜单选中字体颜色 leftMenuTextActiveColor: '#fff', // logo字体颜色 logoTitleTextColor: '#fff', // logo边框颜色 logoBorderColor: 'inherit', // 头部背景颜色 topHeaderBgColor: '#fff', // 头部字体颜色 topHeaderTextColor: 'inherit', // 头部悬停颜色 topHeaderHoverColor: '#f6f6f6', // 头部边框颜色 topToolBorderColor: '#eee' } } }, getters: { getBreadcrumb(): boolean { return this.breadcrumb }, getBreadcrumbIcon(): boolean { return this.breadcrumbIcon }, getCollapse(): boolean { return this.collapse }, getUniqueOpened(): boolean { return this.uniqueOpened }, getHamburger(): boolean { return this.hamburger }, getScreenfull(): boolean { return this.screenfull }, getSize(): boolean { return this.size }, getLocale(): boolean { return this.locale }, getMessage(): boolean { return this.message }, getTagsView(): boolean { return this.tagsView }, getTagsViewIcon(): boolean { return this.tagsViewIcon }, getLogo(): boolean { return this.logo }, getFixedHeader(): boolean { return this.fixedHeader }, getGreyMode(): boolean { return this.greyMode }, getFixedMenu(): boolean { return this.fixedMenu }, getPageLoading(): boolean { return this.pageLoading }, getLayout(): LayoutType { return this.layout }, getTitle(): string { return this.title }, getUserInfo(): string { return this.userInfo }, getIsDark(): boolean { return this.isDark }, getCurrentSize(): ElementPlusSize { return this.currentSize }, getSizeMap(): ElementPlusSize[] { return this.sizeMap }, getMobile(): boolean { return this.mobile }, getTheme(): ThemeTypes { return this.theme }, getFooter(): boolean { return this.footer } }, actions: { setBreadcrumb(breadcrumb: boolean) { this.breadcrumb = breadcrumb }, setBreadcrumbIcon(breadcrumbIcon: boolean) { this.breadcrumbIcon = breadcrumbIcon }, setCollapse(collapse: boolean) { this.collapse = collapse }, setUniqueOpened(uniqueOpened: boolean) { this.uniqueOpened = uniqueOpened }, setHamburger(hamburger: boolean) { this.hamburger = hamburger }, setScreenfull(screenfull: boolean) { this.screenfull = screenfull }, setSize(size: boolean) { this.size = size }, setLocale(locale: boolean) { this.locale = locale }, setMessage(message: boolean) { this.message = message }, setTagsView(tagsView: boolean) { this.tagsView = tagsView }, setTagsViewIcon(tagsViewIcon: boolean) { this.tagsViewIcon = tagsViewIcon }, setLogo(logo: boolean) { this.logo = logo }, setFixedHeader(fixedHeader: boolean) { this.fixedHeader = fixedHeader }, setGreyMode(greyMode: boolean) { this.greyMode = greyMode }, setFixedMenu(fixedMenu: boolean) { wsCache.set('fixedMenu', fixedMenu) this.fixedMenu = fixedMenu }, setPageLoading(pageLoading: boolean) { this.pageLoading = pageLoading }, setLayout(layout: LayoutType) { if (this.mobile && layout !== 'classic') { ElMessage.warning('移动端模式下不支持切换其他布局') return } this.layout = layout wsCache.set(CACHE_KEY.LAYOUT, this.layout) }, setTitle(title: string) { this.title = title }, setIsDark(isDark: boolean) { this.isDark = isDark if (this.isDark) { document.documentElement.classList.add('dark') document.documentElement.classList.remove('light') } else { document.documentElement.classList.add('light') document.documentElement.classList.remove('dark') } wsCache.set(CACHE_KEY.IS_DARK, this.isDark) }, setCurrentSize(currentSize: ElementPlusSize) { this.currentSize = currentSize wsCache.set('currentSize', this.currentSize) }, setMobile(mobile: boolean) { this.mobile = mobile }, setTheme(theme: ThemeTypes) { this.theme = Object.assign(this.theme, theme) wsCache.set(CACHE_KEY.THEME, this.theme) }, setCssVarTheme() { for (const key in this.theme) { setCssVar(`--${humpToUnderline(key)}`, this.theme[key]) } }, setFooter(footer: boolean) { this.footer = footer } }, persist: false }) export const useAppStoreWithOut = () => { return useAppStore(store) } ================================================ FILE: yshop-drink-vue3/src/store/modules/dict.ts ================================================ import { defineStore } from 'pinia' import { store } from '../index' // @ts-ignore import { DictDataVO } from '@/api/system/dict/types' import { CACHE_KEY, useCache } from '@/hooks/web/useCache' const { wsCache } = useCache('sessionStorage') import { getSimpleDictDataList } from '@/api/system/dict/dict.data' export interface DictValueType { value: any label: string clorType?: string cssClass?: string } export interface DictTypeType { dictType: string dictValue: DictValueType[] } export interface DictState { dictMap: Map isSetDict: boolean } export const useDictStore = defineStore('dict', { state: (): DictState => ({ dictMap: new Map(), isSetDict: false }), getters: { getDictMap(): Recordable { const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE) if (dictMap) { this.dictMap = dictMap } return this.dictMap }, getIsSetDict(): boolean { return this.isSetDict } }, actions: { async setDictMap() { const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE) if (dictMap) { this.dictMap = dictMap this.isSetDict = true } else { const res = await getSimpleDictDataList() // 设置数据 const dictDataMap = new Map() res.forEach((dictData: DictDataVO) => { // 获得 dictType 层级 const enumValueObj = dictDataMap[dictData.dictType] if (!enumValueObj) { dictDataMap[dictData.dictType] = [] } // 处理 dictValue 层级 dictDataMap[dictData.dictType].push({ value: dictData.value, label: dictData.label, colorType: dictData.colorType, cssClass: dictData.cssClass }) }) this.dictMap = dictDataMap this.isSetDict = true wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 }) // 60 秒 过期 } }, getDictByType(type: string) { if (!this.isSetDict) { this.setDictMap() } return this.dictMap[type] }, async resetDict() { wsCache.delete(CACHE_KEY.DICT_CACHE) const res = await getSimpleDictDataList() // 设置数据 const dictDataMap = new Map() res.forEach((dictData: DictDataVO) => { // 获得 dictType 层级 const enumValueObj = dictDataMap[dictData.dictType] if (!enumValueObj) { dictDataMap[dictData.dictType] = [] } // 处理 dictValue 层级 dictDataMap[dictData.dictType].push({ value: dictData.value, label: dictData.label, colorType: dictData.colorType, cssClass: dictData.cssClass }) }) this.dictMap = dictDataMap this.isSetDict = true wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 }) // 60 秒 过期 } } }) export const useDictStoreWithOut = () => { return useDictStore(store) } ================================================ FILE: yshop-drink-vue3/src/store/modules/locale.ts ================================================ import { defineStore } from 'pinia' import { store } from '../index' import zhCn from 'element-plus/es/locale/lang/zh-cn' import en from 'element-plus/es/locale/lang/en' import { CACHE_KEY, useCache } from '@/hooks/web/useCache' import { LocaleDropdownType } from '@/types/localeDropdown' const { wsCache } = useCache() const elLocaleMap = { 'zh-CN': zhCn, en: en } interface LocaleState { currentLocale: LocaleDropdownType localeMap: LocaleDropdownType[] } export const useLocaleStore = defineStore('locales', { state: (): LocaleState => { return { currentLocale: { lang: wsCache.get(CACHE_KEY.LANG) || 'zh-CN', elLocale: elLocaleMap[wsCache.get(CACHE_KEY.LANG) || 'zh-CN'] }, // 多语言 localeMap: [ { lang: 'zh-CN', name: '简体中文' }, { lang: 'en', name: 'English' } ] } }, getters: { getCurrentLocale(): LocaleDropdownType { return this.currentLocale }, getLocaleMap(): LocaleDropdownType[] { return this.localeMap } }, actions: { setCurrentLocale(localeMap: LocaleDropdownType) { // this.locale = Object.assign(this.locale, localeMap) this.currentLocale.lang = localeMap?.lang this.currentLocale.elLocale = elLocaleMap[localeMap?.lang] wsCache.set(CACHE_KEY.LANG, localeMap?.lang) } } }) export const useLocaleStoreWithOut = () => { return useLocaleStore(store) } ================================================ FILE: yshop-drink-vue3/src/store/modules/lock.ts ================================================ import { defineStore } from 'pinia' import { store } from '@/store' interface lockInfo { isLock?: boolean password?: string } interface LockState { lockInfo: lockInfo } export const useLockStore = defineStore('lock', { state: (): LockState => { return { lockInfo: { // isLock: false, // 是否锁定屏幕 // password: '' // 锁屏密码 } } }, getters: { getLockInfo(): lockInfo { return this.lockInfo } }, actions: { setLockInfo(lockInfo: lockInfo) { this.lockInfo = lockInfo }, resetLockInfo() { this.lockInfo = {} }, unLock(password: string) { if (this.lockInfo?.password === password) { this.resetLockInfo() return true } else { return false } } }, persist: true }) export const useLockStoreWithOut = () => { return useLockStore(store) } ================================================ FILE: yshop-drink-vue3/src/store/modules/permission.ts ================================================ import { defineStore } from 'pinia' import { store } from '@/store' import { cloneDeep } from 'lodash-es' import remainingRouter from '@/router/modules/remaining' import { flatMultiLevelRoutes, generateRoute } from '@/utils/routerHelper' import { CACHE_KEY, useCache } from '@/hooks/web/useCache' const { wsCache } = useCache() export interface PermissionState { routers: AppRouteRecordRaw[] addRouters: AppRouteRecordRaw[] menuTabRouters: AppRouteRecordRaw[] } export const usePermissionStore = defineStore('permission', { state: (): PermissionState => ({ routers: [], addRouters: [], menuTabRouters: [] }), getters: { getRouters(): AppRouteRecordRaw[] { return this.routers }, getAddRouters(): AppRouteRecordRaw[] { return flatMultiLevelRoutes(cloneDeep(this.addRouters)) }, getMenuTabRouters(): AppRouteRecordRaw[] { return this.menuTabRouters } }, actions: { async generateRoutes(): Promise { return new Promise(async (resolve) => { // 获得菜单列表,它在登录的时候,setUserInfoAction 方法中已经进行获取 let res: AppCustomRouteRecordRaw[] = [] if (wsCache.get(CACHE_KEY.ROLE_ROUTERS)) { res = wsCache.get(CACHE_KEY.ROLE_ROUTERS) as AppCustomRouteRecordRaw[] } const routerMap: AppRouteRecordRaw[] = generateRoute(res) // 动态路由,404一定要放到最后面 this.addRouters = routerMap.concat([ { path: '/:path(.*)*', redirect: '/404', name: '404Page', meta: { hidden: true, breadcrumb: false } } ]) // 渲染菜单的所有路由 this.routers = cloneDeep(remainingRouter).concat(routerMap) resolve() }) }, setMenuTabRouters(routers: AppRouteRecordRaw[]): void { this.menuTabRouters = routers } }, persist: false }) export const usePermissionStoreWithOut = () => { return usePermissionStore(store) } ================================================ FILE: yshop-drink-vue3/src/store/modules/simpleWorkflow.ts ================================================ import { store } from '../index' import { defineStore } from 'pinia' export const useWorkFlowStore = defineStore('simpleWorkflow', { state: () => ({ tableId: '', isTried: false, promoterDrawer: false, flowPermission1: {}, approverDrawer: false, approverConfig1: {}, copyerDrawer: false, copyerConfig1: {}, conditionDrawer: false, conditionsConfig1: { conditionNodes: [] } }), actions: { setTableId(payload) { this.tableId = payload }, setIsTried(payload) { this.isTried = payload }, setPromoter(payload) { this.promoterDrawer = payload }, setFlowPermission(payload) { this.flowPermission1 = payload }, setApprover(payload) { this.approverDrawer = payload }, setApproverConfig(payload) { this.approverConfig1 = payload }, setCopyer(payload) { this.copyerDrawer = payload }, setCopyerConfig(payload) { this.copyerConfig1 = payload }, setCondition(payload) { this.conditionDrawer = payload }, setConditionsConfig(payload) { this.conditionsConfig1 = payload } } }) export const useWorkFlowStoreWithOut = () => { return useWorkFlowStore(store) } ================================================ FILE: yshop-drink-vue3/src/store/modules/tagsView.ts ================================================ import router from '@/router' import type { RouteLocationNormalizedLoaded } from 'vue-router' import { getRawRoute } from '@/utils/routerHelper' import { defineStore } from 'pinia' import { store } from '../index' import { findIndex } from '@/utils' export interface TagsViewState { visitedViews: RouteLocationNormalizedLoaded[] cachedViews: Set } export const useTagsViewStore = defineStore('tagsView', { state: (): TagsViewState => ({ visitedViews: [], cachedViews: new Set() }), getters: { getVisitedViews(): RouteLocationNormalizedLoaded[] { return this.visitedViews }, getCachedViews(): string[] { return Array.from(this.cachedViews) } }, actions: { // 新增缓存和tag addView(view: RouteLocationNormalizedLoaded): void { this.addVisitedView(view) this.addCachedView() }, // 新增tag addVisitedView(view: RouteLocationNormalizedLoaded) { if (this.visitedViews.some((v) => v.path === view.path)) return if (view.meta?.noTagsView) return this.visitedViews.push( Object.assign({}, view, { title: view.meta?.title || 'no-name' }) ) }, // 新增缓存 addCachedView() { const cacheMap: Set = new Set() for (const v of this.visitedViews) { const item = getRawRoute(v) const needCache = !item.meta?.noCache if (!needCache) { continue } const name = item.name as string cacheMap.add(name) } if (Array.from(this.cachedViews).sort().toString() === Array.from(cacheMap).sort().toString()) return this.cachedViews = cacheMap }, // 删除某个 delView(view: RouteLocationNormalizedLoaded) { this.delVisitedView(view) this.delCachedView() }, // 删除tag delVisitedView(view: RouteLocationNormalizedLoaded) { for (const [i, v] of this.visitedViews.entries()) { if (v.path === view.path) { this.visitedViews.splice(i, 1) break } } }, // 删除缓存 delCachedView() { const route = router.currentRoute.value const index = findIndex(this.getCachedViews, (v) => v === route.name) if (index > -1) { this.cachedViews.delete(this.getCachedViews[index]) } }, // 删除所有缓存和tag delAllViews() { this.delAllVisitedViews() this.delCachedView() }, // 删除所有tag delAllVisitedViews() { // const affixTags = this.visitedViews.filter((tag) => tag.meta.affix) this.visitedViews = [] }, // 删除其他 delOthersViews(view: RouteLocationNormalizedLoaded) { this.delOthersVisitedViews(view) this.addCachedView() }, // 删除其他tag delOthersVisitedViews(view: RouteLocationNormalizedLoaded) { this.visitedViews = this.visitedViews.filter((v) => { return v?.meta?.affix || v.path === view.path }) }, // 删除左侧 delLeftViews(view: RouteLocationNormalizedLoaded) { const index = findIndex( this.visitedViews, (v) => v.path === view.path ) if (index > -1) { this.visitedViews = this.visitedViews.filter((v, i) => { return v?.meta?.affix || v.path === view.path || i > index }) this.addCachedView() } }, // 删除右侧 delRightViews(view: RouteLocationNormalizedLoaded) { const index = findIndex( this.visitedViews, (v) => v.path === view.path ) if (index > -1) { this.visitedViews = this.visitedViews.filter((v, i) => { return v?.meta?.affix || v.path === view.path || i < index }) this.addCachedView() } }, updateVisitedView(view: RouteLocationNormalizedLoaded) { for (let v of this.visitedViews) { if (v.path === view.path) { v = Object.assign(v, view) break } } } }, persist: false }) export const useTagsViewStoreWithOut = () => { return useTagsViewStore(store) } ================================================ FILE: yshop-drink-vue3/src/store/modules/user.ts ================================================ import { store } from '@/store' import { defineStore } from 'pinia' import { getAccessToken, removeToken } from '@/utils/auth' import { CACHE_KEY, useCache, deleteUserCache } from '@/hooks/web/useCache' import { getInfo, loginOut } from '@/api/login' const { wsCache } = useCache() interface UserVO { id: number avatar: string nickname: string deptId: number } interface UserInfoVO { // USER 缓存 permissions: string[] roles: string[] isSetUser: boolean user: UserVO } export const useUserStore = defineStore('admin-user', { state: (): UserInfoVO => ({ permissions: [], roles: [], isSetUser: false, user: { id: 0, avatar: '', nickname: '', deptId: 0 } }), getters: { getPermissions(): string[] { return this.permissions }, getRoles(): string[] { return this.roles }, getIsSetUser(): boolean { return this.isSetUser }, getUser(): UserVO { return this.user } }, actions: { async setUserInfoAction() { if (!getAccessToken()) { this.resetState() return null } let userInfo = wsCache.get(CACHE_KEY.USER) if (!userInfo) { userInfo = await getInfo() } this.permissions = userInfo.permissions this.roles = userInfo.roles this.user = userInfo.user this.isSetUser = true wsCache.set(CACHE_KEY.USER, userInfo) wsCache.set(CACHE_KEY.ROLE_ROUTERS, userInfo.menus) }, async setUserAvatarAction(avatar: string) { const userInfo = wsCache.get(CACHE_KEY.USER) // NOTE: 是否需要像`setUserInfoAction`一样判断`userInfo != null` this.user.avatar = avatar userInfo.user.avatar = avatar wsCache.set(CACHE_KEY.USER, userInfo) }, async setUserNicknameAction(nickname: string) { const userInfo = wsCache.get(CACHE_KEY.USER) // NOTE: 是否需要像`setUserInfoAction`一样判断`userInfo != null` this.user.nickname = nickname userInfo.user.nickname = nickname wsCache.set(CACHE_KEY.USER, userInfo) }, async loginOut() { await loginOut() removeToken() deleteUserCache() // 删除用户缓存 this.resetState() }, resetState() { this.permissions = [] this.roles = [] this.isSetUser = false this.user = { id: 0, avatar: '', nickname: '', deptId: 0 } } } }) export const useUserStoreWithOut = () => { return useUserStore(store) } ================================================ FILE: yshop-drink-vue3/src/styles/FormCreate/index.scss ================================================ // 使用字体图标来源 https://fontello.com/ @font-face { font-family: 'fc-icon'; src: url('@/styles/FormCreate/fonts/fontello.woff') format('woff'); } .icon-doc-text:before { content: '\f0f6'; } .icon-server:before { content: '\f233'; } .icon-address-card-o:before { content: '\f2bc'; } .icon-user-o:before { content: '\f2c0'; } ================================================ FILE: yshop-drink-vue3/src/styles/global.module.scss ================================================ @import './variables.scss'; // 导出变量 :export { namespace: $namespace; elNamespace: $elNamespace; } ================================================ FILE: yshop-drink-vue3/src/styles/index.scss ================================================ @import './var.css'; @import './FormCreate/index.scss'; @import 'element-plus/theme-chalk/dark/css-vars.css'; .reset-margin [class*='el-icon'] + span { margin-left: 2px !important; } // 解决抽屉弹出时,body宽度变化的问题 .el-popup-parent--hidden { width: 100% !important; } // 解决表格内容超过表格总宽度后,横向滚动条前端顶不到表格边缘的问题 .el-scrollbar__bar { display: flex; justify-content: flex-start; } /* nprogress 适配 element-plus 的主题色 */ #nprogress { & .bar { background-color: var(--el-color-primary) !important; } & .peg { box-shadow: 0 0 10px var(--el-color-primary), 0 0 5px var(--el-color-primary) !important; } & .spinner-icon { border-top-color: var(--el-color-primary); border-left-color: var(--el-color-primary); } } ================================================ FILE: yshop-drink-vue3/src/styles/theme.scss ================================================ // .text-color { // color: var(--el-text-color-regular); // } // .dark .dark\:text-color { // color: rgba(255, 255, 255, var(--dark-text-color)); // } ================================================ FILE: yshop-drink-vue3/src/styles/var.css ================================================ :root { --login-bg-color: #293146; --left-menu-max-width: 200px; --left-menu-min-width: 64px; --left-menu-bg-color: #001529; --left-menu-bg-light-color: #0f2438; --left-menu-bg-active-color: var(--el-color-primary); --left-menu-text-color: #bfcbd9; --left-menu-text-active-color: #fff; --left-menu-collapse-bg-active-color: var(--el-color-primary); /* left menu end */ /* logo start */ --logo-height: 50px; --logo-title-text-color: #fff; /* logo end */ /* header start */ --top-header-bg-color: '#fff'; --top-header-text-color: 'inherit'; --top-header-hover-color: #f6f6f6; --top-tool-height: var(--logo-height); --top-tool-p-x: 0; --tags-view-height: 35px; /* header start */ /* tab menu start */ --tab-menu-max-width: 80px; --tab-menu-min-width: 30px; --tab-menu-collapse-height: 36px; /* tab menu end */ --app-content-padding: 20px; --app-content-bg-color: #f5f7f9; --app-footer-height: 50px; --transition-time-02: 0.2s; } .dark { --app-content-bg-color: var(--el-bg-color); } html, body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } ================================================ FILE: yshop-drink-vue3/src/styles/variables.scss ================================================ // 命名空间 $namespace: v; // el命名空间 $elNamespace: el; ================================================ FILE: yshop-drink-vue3/src/types/components.d.ts ================================================ export type ComponentName = | 'Radio' | 'RadioButton' | 'Checkbox' | 'CheckboxButton' | 'Input' | 'Autocomplete' | 'InputNumber' | 'Select' | 'Cascader' | 'Switch' | 'Slider' | 'TimePicker' | 'DatePicker' | 'Rate' | 'ColorPicker' | 'Transfer' | 'Divider' | 'TimeSelect' | 'SelectV2' | 'TreeSelect' | 'InputPassword' | 'Editor' | 'UploadImg' | 'UploadImgs' | 'UploadFile' export type ColProps = { span?: number xs?: number sm?: number md?: number lg?: number xl?: number tag?: string } export type ComponentOptions = { label?: string value?: FormValueType disabled?: boolean key?: string | number children?: ComponentOptions[] options?: ComponentOptions[] } & Recordable export type ComponentOptionsAlias = { labelField?: string valueField?: string } export type ComponentProps = { optionsAlias?: ComponentOptionsAlias options?: ComponentOptions[] optionsSlot?: boolean } & Recordable ================================================ FILE: yshop-drink-vue3/src/types/configGlobal.d.ts ================================================ import { ElementPlusSize } from './elementPlus' export interface ConfigGlobalTypes { size?: ElementPlusSize } ================================================ FILE: yshop-drink-vue3/src/types/contextMenu.d.ts ================================================ export type contextMenuSchema = { disabled?: boolean divided?: boolean icon?: string label: string command?: (item: contextMenuSchema) => void } ================================================ FILE: yshop-drink-vue3/src/types/descriptions.d.ts ================================================ export interface DescriptionsSchema { span?: number // 占多少分 field: string // 字段名 label?: string // label名 mappedField?: string // 字段映射 width?: string | number minWidth?: string | number align?: 'left' | 'center' | 'right' labelAlign?: 'left' | 'center' | 'right' className?: string labelClassName?: string dateFormat?: string // add by 星语:支持时间的格式化 dictType?: string // add by 星语:支持 dict 字典数据 } ================================================ FILE: yshop-drink-vue3/src/types/elementPlus.d.ts ================================================ export type ElementPlusSize = 'default' | 'small' | 'large' export type ElementPlusInfoType = 'success' | 'info' | 'warning' | 'danger' ================================================ FILE: yshop-drink-vue3/src/types/form.d.ts ================================================ import type { CSSProperties } from 'vue' import { ColProps, ComponentProps, ComponentName } from '@/types/components' import type { AxiosPromise } from 'axios' export type FormSetPropsType = { field: string path: string value: any } export type FormValueType = string | number | string[] | number[] | boolean | undefined | null export type FormItemProps = { labelWidth?: string | number required?: boolean rules?: Recordable error?: string showMessage?: boolean inlineMessage?: boolean style?: CSSProperties } export type FormSchema = { // 唯一值 field: string // 标题 label?: string // 提示 labelMessage?: string // col组件属性 colProps?: ColProps // 表单组件属性,slots对应的是表单组件的插槽,规则:${field}-xxx,具体可以查看element-plus文档 componentProps?: { slots?: Recordable } & ComponentProps // formItem组件属性 formItemProps?: FormItemProps // 渲染的组件 component?: ComponentName // 初始值 value?: FormValueType // 是否隐藏 hidden?: boolean // 远程加载下拉项 api?: () => AxiosPromise } ================================================ FILE: yshop-drink-vue3/src/types/icon.d.ts ================================================ export interface IconTypes { size?: number color?: string icon: string } ================================================ FILE: yshop-drink-vue3/src/types/infoTip.d.ts ================================================ export interface TipSchema { label: string keys?: string[] } ================================================ FILE: yshop-drink-vue3/src/types/layout.d.ts ================================================ export type LayoutType = 'classic' | 'topLeft' | 'top' | 'cutMenu' ================================================ FILE: yshop-drink-vue3/src/types/localeDropdown.d.ts ================================================ export interface Language { el: Recordable name: string } export interface LocaleDropdownType { lang: LocaleType name?: string elLocale?: Language } ================================================ FILE: yshop-drink-vue3/src/types/qrcode.d.ts ================================================ export interface QrcodeLogo { src?: string logoSize?: number bgColor?: string borderSize?: number crossOrigin?: string borderRadius?: number logoRadius?: number } ================================================ FILE: yshop-drink-vue3/src/types/table.d.ts ================================================ export type TableColumn = { field: string label?: string width?: number | string fixed?: 'left' | 'right' children?: TableColumn[] } & Recordable export type VxeTableColumn = { field: string title?: string children?: TableColumn[] } & Recordable export type TableSlotDefault = { row: Recordable column: TableColumn $index: number } & Recordable export interface Pagination { small?: boolean background?: boolean pageSize?: number defaultPageSize?: number total?: number pageCount?: number pagerCount?: number currentPage?: number defaultCurrentPage?: number layout?: string pageSizes?: number[] popperClass?: string prevText?: string nextText?: string disabled?: boolean hideOnSinglePage?: boolean } export interface TableSetPropsType { field: string path: string value: any } ================================================ FILE: yshop-drink-vue3/src/types/theme.d.ts ================================================ export type ThemeTypes = { elColorPrimary?: string leftMenuBorderColor?: string leftMenuBgColor?: string leftMenuBgLightColor?: string leftMenuBgActiveColor?: string leftMenuCollapseBgActiveColor?: string leftMenuTextColor?: string leftMenuTextActiveColor?: string logoTitleTextColor?: string logoBorderColor?: string topHeaderBgColor?: string topHeaderTextColor?: string topHeaderHoverColor?: string topToolBorderColor?: string } ================================================ FILE: yshop-drink-vue3/src/utils/Logger.ts ================================================ const isArray = function (obj: any): boolean { return Object.prototype.toString.call(obj) === '[object Array]' } const Logger = () => {} Logger.typeColor = function (type: string) { let color = '' switch (type) { case 'primary': color = '#2d8cf0' break case 'success': color = '#19be6b' break case 'info': color = '#909399' break case 'warn': color = '#ff9900' break case 'error': color = '#f03f14' break default: color = '#35495E' break } return color } Logger.print = function (type = 'default', text: any, back = false) { if (typeof text === 'object') { // 如果是對象則調用打印對象方式 isArray(text) ? console.table(text) : console.dir(text) return } if (back) { // 如果是打印帶背景圖的 console.log( `%c ${text} `, `background:${Logger.typeColor(type)}; padding: 2px; border-radius: 4px; color: #fff;` ) } else { console.log( `%c ${text} `, `border: 1px solid ${Logger.typeColor(type)}; padding: 2px; border-radius: 4px; color: ${Logger.typeColor(type)};` ) } } Logger.printBack = function (type = 'primary', text) { this.print(type, text, true) } Logger.pretty = function (type = 'primary', title, text) { if (typeof text === 'object') { console.group('Console Group', title) console.log( `%c ${title}`, `background:${Logger.typeColor(type)};border:1px solid ${Logger.typeColor(type)}; padding: 1px; border-radius: 4px; color: #fff;` ) isArray(text) ? console.table(text) : console.dir(text) console.groupEnd() return } console.log( `%c ${title} %c ${text} %c`, `background:${Logger.typeColor(type)};border:1px solid ${Logger.typeColor(type)}; padding: 1px; border-radius: 4px 0 0 4px; color: #fff;`, `border:1px solid ${Logger.typeColor(type)}; padding: 1px; border-radius: 0 4px 4px 0; color: ${Logger.typeColor(type)};`, 'background:transparent' ) } Logger.prettyPrimary = function (title, ...text) { text.forEach((t) => this.pretty('primary', title, t)) } Logger.prettySuccess = function (title, ...text) { text.forEach((t) => this.pretty('success', title, t)) } Logger.prettyWarn = function (title, ...text) { text.forEach((t) => this.pretty('warn', title, t)) } Logger.prettyError = function (title, ...text) { text.forEach((t) => this.pretty('error', title, t)) } Logger.prettyInfo = function (title, ...text) { text.forEach((t) => this.pretty('info', title, t)) } export default Logger ================================================ FILE: yshop-drink-vue3/src/utils/auth.ts ================================================ import { useCache, CACHE_KEY } from '@/hooks/web/useCache' import { TokenType } from '@/api/login/types' import { decrypt, encrypt } from '@/utils/jsencrypt' const { wsCache } = useCache() const AccessTokenKey = 'ACCESS_TOKEN' const RefreshTokenKey = 'REFRESH_TOKEN' // 获取token export const getAccessToken = () => { // 此处与TokenKey相同,此写法解决初始化时Cookies中不存在TokenKey报错 return wsCache.get(AccessTokenKey) ? wsCache.get(AccessTokenKey) : wsCache.get('ACCESS_TOKEN') } // 刷新token export const getRefreshToken = () => { return wsCache.get(RefreshTokenKey) } // 设置token export const setToken = (token: TokenType) => { wsCache.set(RefreshTokenKey, token.refreshToken) wsCache.set(AccessTokenKey, token.accessToken) } // 删除token export const removeToken = () => { wsCache.delete(AccessTokenKey) wsCache.delete(RefreshTokenKey) } /** 格式化token(jwt格式) */ export const formatToken = (token: string): string => { return 'Bearer ' + token } // ========== 账号相关 ========== export type LoginFormType = { tenantName: string username: string password: string rememberMe: boolean } export const getLoginForm = () => { const loginForm: LoginFormType = wsCache.get(CACHE_KEY.LoginForm) if (loginForm) { loginForm.password = decrypt(loginForm.password) as string } return loginForm } export const setLoginForm = (loginForm: LoginFormType) => { loginForm.password = encrypt(loginForm.password) as string wsCache.set(CACHE_KEY.LoginForm, loginForm, { exp: 30 * 24 * 60 * 60 }) } export const removeLoginForm = () => { wsCache.delete(CACHE_KEY.LoginForm) } // ========== 租户相关 ========== export const getTenantId = () => { return wsCache.get(CACHE_KEY.TenantId) } export const setTenantId = (username: string) => { wsCache.set(CACHE_KEY.TenantId, username) } ================================================ FILE: yshop-drink-vue3/src/utils/color.ts ================================================ /** * 判断是否 十六进制颜色值. * 输入形式可为 #fff000 #f00 * * @param String color 十六进制颜色值 * @return Boolean */ export const isHexColor = (color: string) => { const reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-f]{6})$/ return reg.test(color) } /** * RGB 颜色值转换为 十六进制颜色值. * r, g, 和 b 需要在 [0, 255] 范围内 * * @return String 类似#ff00ff * @param r * @param g * @param b */ export const rgbToHex = (r: number, g: number, b: number) => { // tslint:disable-next-line:no-bitwise const hex = ((r << 16) | (g << 8) | b).toString(16) return '#' + new Array(Math.abs(hex.length - 7)).join('0') + hex } /** * Transform a HEX color to its RGB representation * @param {string} hex The color to transform * @returns The RGB representation of the passed color */ export const hexToRGB = (hex: string, opacity?: number) => { let sHex = hex.toLowerCase() if (isHexColor(hex)) { if (sHex.length === 4) { let sColorNew = '#' for (let i = 1; i < 4; i += 1) { sColorNew += sHex.slice(i, i + 1).concat(sHex.slice(i, i + 1)) } sHex = sColorNew } const sColorChange: number[] = [] for (let i = 1; i < 7; i += 2) { sColorChange.push(parseInt('0x' + sHex.slice(i, i + 2))) } return opacity ? 'RGBA(' + sColorChange.join(',') + ',' + opacity + ')' : 'RGB(' + sColorChange.join(',') + ')' } return sHex } export const colorIsDark = (color: string) => { if (!isHexColor(color)) return const [r, g, b] = hexToRGB(color) .replace(/(?:\(|\)|rgb|RGB)*/g, '') .split(',') .map((item) => Number(item)) return r * 0.299 + g * 0.578 + b * 0.114 < 192 } /** * Darkens a HEX color given the passed percentage * @param {string} color The color to process * @param {number} amount The amount to change the color by * @returns {string} The HEX representation of the processed color */ export const darken = (color: string, amount: number) => { color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color amount = Math.trunc((255 * amount) / 100) return `#${subtractLight(color.substring(0, 2), amount)}${subtractLight( color.substring(2, 4), amount )}${subtractLight(color.substring(4, 6), amount)}` } /** * Lightens a 6 char HEX color according to the passed percentage * @param {string} color The color to change * @param {number} amount The amount to change the color by * @returns {string} The processed color represented as HEX */ export const lighten = (color: string, amount: number) => { color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color amount = Math.trunc((255 * amount) / 100) return `#${addLight(color.substring(0, 2), amount)}${addLight( color.substring(2, 4), amount )}${addLight(color.substring(4, 6), amount)}` } /* Suma el porcentaje indicado a un color (RR, GG o BB) hexadecimal para aclararlo */ /** * Sums the passed percentage to the R, G or B of a HEX color * @param {string} color The color to change * @param {number} amount The amount to change the color by * @returns {string} The processed part of the color */ const addLight = (color: string, amount: number) => { const cc = parseInt(color, 16) + amount const c = cc > 255 ? 255 : cc return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}` } /** * Calculates luminance of an rgb color * @param {number} r red * @param {number} g green * @param {number} b blue */ const luminanace = (r: number, g: number, b: number) => { const a = [r, g, b].map((v) => { v /= 255 return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4) }) return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722 } /** * Calculates contrast between two rgb colors * @param {string} rgb1 rgb color 1 * @param {string} rgb2 rgb color 2 */ const contrast = (rgb1: string[], rgb2: number[]) => { return ( (luminanace(~~rgb1[0], ~~rgb1[1], ~~rgb1[2]) + 0.05) / (luminanace(rgb2[0], rgb2[1], rgb2[2]) + 0.05) ) } /** * Determines what the best text color is (black or white) based con the contrast with the background * @param hexColor - Last selected color by the user */ export const calculateBestTextColor = (hexColor: string) => { const rgbColor = hexToRGB(hexColor.substring(1)) const contrastWithBlack = contrast(rgbColor.split(','), [0, 0, 0]) return contrastWithBlack >= 12 ? '#000000' : '#FFFFFF' } /** * Subtracts the indicated percentage to the R, G or B of a HEX color * @param {string} color The color to change * @param {number} amount The amount to change the color by * @returns {string} The processed part of the color */ const subtractLight = (color: string, amount: number) => { const cc = parseInt(color, 16) - amount const c = cc < 0 ? 0 : cc return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}` } // 预设颜色 export const PREDEFINE_COLORS = [ '#ff4500', '#ff8c00', '#ffd700', '#90ee90', '#00ced1', '#1e90ff', '#c71585', '#409EFF', '#909399', '#C0C4CC', '#b7390b', '#ff7800', '#fad400', '#5b8c5f', '#00babd', '#1f73c3', '#711f57' ] ================================================ FILE: yshop-drink-vue3/src/utils/constants.ts ================================================ /** * Created by yshop源码 * * 枚举类 */ // ========== COMMON 模块 ========== // 全局通用状态枚举 export const CommonStatusEnum = { ENABLE: 0, // 开启 DISABLE: 1 // 禁用 } // 全局用户类型枚举 export const UserTypeEnum = { MEMBER: 1, // 会员 ADMIN: 2 // 管理员 } // ========== SYSTEM 模块 ========== /** * 菜单的类型枚举 */ export const SystemMenuTypeEnum = { DIR: 1, // 目录 MENU: 2, // 菜单 BUTTON: 3 // 按钮 } /** * 角色的类型枚举 */ export const SystemRoleTypeEnum = { SYSTEM: 1, // 内置角色 CUSTOM: 2 // 自定义角色 } /** * 数据权限的范围枚举 */ export const SystemDataScopeEnum = { ALL: 1, // 全部数据权限 DEPT_CUSTOM: 2, // 指定部门数据权限 DEPT_ONLY: 3, // 部门数据权限 DEPT_AND_CHILD: 4, // 部门及以下数据权限 DEPT_SELF: 5 // 仅本人数据权限 } /** * 用户的社交平台的类型枚举 */ export const SystemUserSocialTypeEnum = { DINGTALK: { title: '钉钉', type: 20, source: 'dingtalk', img: 'https://s1.ax1x.com/2022/05/22/OzMDRs.png' }, WECHAT_ENTERPRISE: { title: '企业微信', type: 30, source: 'wechat_enterprise', img: 'https://s1.ax1x.com/2022/05/22/OzMrzn.png' } } // ========== INFRA 模块 ========== /** * 代码生成模板类型 */ export const InfraCodegenTemplateTypeEnum = { CRUD: 1, // 基础 CRUD TREE: 2, // 树形 CRUD SUB: 3 // 主子表 CRUD } /** * 任务状态的枚举 */ export const InfraJobStatusEnum = { INIT: 0, // 初始化中 NORMAL: 1, // 运行中 STOP: 2 // 暂停运行 } /** * API 异常数据的处理状态 */ export const InfraApiErrorLogProcessStatusEnum = { INIT: 0, // 未处理 DONE: 1, // 已处理 IGNORE: 2 // 已忽略 } // ========== PAY 模块 ========== /** * 支付渠道枚举 */ export const PayChannelEnum = { WX_PUB: { code: 'wx_pub', name: '微信 JSAPI 支付' }, WX_LITE: { code: 'wx_lite', name: '微信小程序支付' }, WX_APP: { code: 'wx_app', name: '微信 APP 支付' }, WX_BAR: { code: 'wx_bar', name: '微信条码支付' }, ALIPAY_PC: { code: 'alipay_pc', name: '支付宝 PC 网站支付' }, ALIPAY_WAP: { code: 'alipay_wap', name: '支付宝 WAP 网站支付' }, ALIPAY_APP: { code: 'alipay_app', name: '支付宝 APP 支付' }, ALIPAY_QR: { code: 'alipay_qr', name: '支付宝扫码支付' }, ALIPAY_BAR: { code: 'alipay_bar', name: '支付宝条码支付' }, WALLET: { code: 'wallet', name: '钱包支付' }, MOCK: { code: 'mock', name: '模拟支付' } } /** * 支付的展示模式每局 */ export const PayDisplayModeEnum = { URL: { mode: 'url' }, IFRAME: { mode: 'iframe' }, FORM: { mode: 'form' }, QR_CODE: { mode: 'qr_code' }, APP: { mode: 'app' } } /** * 支付类型枚举 */ export const PayType = { WECHAT: 'WECHAT', ALIPAY: 'ALIPAY', MOCK: 'MOCK' } /** * 支付订单状态枚举 */ export const PayOrderStatusEnum = { WAITING: { status: 0, name: '未支付' }, SUCCESS: { status: 10, name: '已支付' }, CLOSED: { status: 20, name: '未支付' } } // ========== MALL - 商品模块 ========== /** * 商品 SPU 状态 */ export const ProductSpuStatusEnum = { RECYCLE: { status: -1, name: '回收站' }, DISABLE: { status: 0, name: '下架' }, ENABLE: { status: 1, name: '上架' } } // ========== MALL - 营销模块 ========== /** * 优惠劵模板的有限期类型的枚举 */ export const CouponTemplateValidityTypeEnum = { DATE: { type: 1, name: '固定日期可用' }, TERM: { type: 2, name: '领取之后可用' } } /** * 优惠劵模板的领取方式的枚举 */ export const CouponTemplateTakeTypeEnum = { USER: { type: 1, name: '直接领取' }, ADMIN: { type: 2, name: '指定发放' }, REGISTER: { type: 3, name: '新人券' } } /** * 营销的商品范围枚举 */ export const PromotionProductScopeEnum = { ALL: { scope: 1, name: '通用劵' }, SPU: { scope: 2, name: '商品劵' }, CATEGORY: { scope: 3, name: '品类劵' } } /** * 营销的条件类型枚举 */ export const PromotionConditionTypeEnum = { PRICE: { type: 10, name: '满 N 元' }, COUNT: { type: 20, name: '满 N 件' } } /** * 优惠类型枚举 */ export const PromotionDiscountTypeEnum = { PRICE: { type: 1, name: '满减' }, PERCENT: { type: 2, name: '折扣' } } // ========== MALL - 交易模块 ========== /** * 分销关系绑定模式枚举 */ export const BrokerageBindModeEnum = { ANYTIME: { mode: 1, name: '首次绑定' }, REGISTER: { mode: 2, name: '注册绑定' }, OVERRIDE: { mode: 3, name: '覆盖绑定' } } /** * 分佣模式枚举 */ export const BrokerageEnabledConditionEnum = { ALL: { condition: 1, name: '人人分销' }, ADMIN: { condition: 2, name: '指定分销' } } /** * 佣金记录业务类型枚举 */ export const BrokerageRecordBizTypeEnum = { ORDER: { type: 1, name: '获得推广佣金' }, WITHDRAW: { type: 2, name: '提现申请' } } /** * 佣金提现状态枚举 */ export const BrokerageWithdrawStatusEnum = { AUDITING: { status: 0, name: '审核中' }, AUDIT_SUCCESS: { status: 10, name: '审核通过' }, AUDIT_FAIL: { status: 20, name: '审核不通过' }, WITHDRAW_SUCCESS: { status: 11, name: '提现成功' }, WITHDRAW_FAIL: { status: 21, name: '提现失败' } } /** * 佣金提现类型枚举 */ export const BrokerageWithdrawTypeEnum = { WALLET: { type: 1, name: '钱包' }, BANK: { type: 2, name: '银行卡' }, WECHAT: { type: 3, name: '微信' }, ALIPAY: { type: 4, name: '支付宝' } } /** * 配送方式枚举 */ export const DeliveryTypeEnum = { EXPRESS: { type: 1, name: '快递发货' }, PICK_UP: { type: 2, name: '到店自提' } } /** * 交易订单 - 状态 */ export const TradeOrderStatusEnum = { UNPAID: { status: 0, name: '待支付' }, UNDELIVERED: { status: 10, name: '待发货' }, DELIVERED: { status: 20, name: '已发货' }, COMPLETED: { status: 30, name: '已完成' }, CANCELED: { status: 40, name: '已取消' } } // ========== ERP - 企业资源计划 ========== export const ErpBizType = { PURCHASE_ORDER: 10, PURCHASE_IN: 11, PURCHASE_RETURN: 12, SALE_ORDER: 20, SALE_OUT: 21, SALE_RETURN: 22 } ================================================ FILE: yshop-drink-vue3/src/utils/dateUtil.ts ================================================ /** * Independent time operation tool to facilitate subsequent switch to dayjs */ // TODO 芋艿:【锁屏】可能后面删除掉 import dayjs from 'dayjs' const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss' const DATE_FORMAT = 'YYYY-MM-DD' export function formatToDateTime(date?: dayjs.ConfigType, format = DATE_TIME_FORMAT): string { return dayjs(date).format(format) } export function formatToDate(date?: dayjs.ConfigType, format = DATE_FORMAT): string { return dayjs(date).format(format) } export const dateUtil = dayjs ================================================ FILE: yshop-drink-vue3/src/utils/dict.ts ================================================ /** * 数据字典工具类 */ import {useDictStoreWithOut} from '@/store/modules/dict' import {ElementPlusInfoType} from '@/types/elementPlus' const dictStore = useDictStoreWithOut() /** * 获取 dictType 对应的数据字典数组 * * @param dictType 数据类型 * @returns {*|Array} 数据字典数组 */ export interface DictDataType { dictType: string label: string value: string | number | boolean colorType: ElementPlusInfoType | '' cssClass: string } export interface NumberDictDataType extends DictDataType { value: number } export const getDictOptions = (dictType: string) => { return dictStore.getDictByType(dictType) || [] } export const getIntDictOptions = (dictType: string): NumberDictDataType[] => { // 获得通用的 DictDataType 列表 const dictOptions: DictDataType[] = getDictOptions(dictType) // 转换成 number 类型的 NumberDictDataType 类型 // why 需要特殊转换:避免 IDEA 在 v-for="dict in getIntDictOptions(...)" 时,el-option 的 key 会告警 const dictOption: NumberDictDataType[] = [] dictOptions.forEach((dict: DictDataType) => { dictOption.push({ ...dict, value: parseInt(dict.value + '') }) }) return dictOption } export const getStrDictOptions = (dictType: string) => { const dictOption: DictDataType[] = [] const dictOptions: DictDataType[] = getDictOptions(dictType) dictOptions.forEach((dict: DictDataType) => { dictOption.push({ ...dict, value: dict.value + '' }) }) return dictOption } export const getBoolDictOptions = (dictType: string) => { const dictOption: DictDataType[] = [] const dictOptions: DictDataType[] = getDictOptions(dictType) dictOptions.forEach((dict: DictDataType) => { dictOption.push({ ...dict, value: dict.value + '' === 'true' }) }) return dictOption } /** * 获取指定字典类型的指定值对应的字典对象 * @param dictType 字典类型 * @param value 字典值 * @return DictDataType 字典对象 */ export const getDictObj = (dictType: string, value: any): DictDataType | undefined => { const dictOptions: DictDataType[] = getDictOptions(dictType) for (const dict of dictOptions) { if (dict.value === value + '') { return dict } } } /** * 获得字典数据的文本展示 * * @param dictType 字典类型 * @param value 字典数据的值 * @return 字典名称 */ export const getDictLabel = (dictType: string, value: any): string => { const dictOptions: DictDataType[] = getDictOptions(dictType) const dictLabel = ref('') dictOptions.forEach((dict: DictDataType) => { if (dict.value === value + '') { dictLabel.value = dict.label } }) return dictLabel.value } export enum DICT_TYPE { USER_TYPE = 'user_type', COMMON_STATUS = 'common_status', TERMINAL = 'terminal', // 终端 DATE_INTERVAL = 'date_interval', // 数据间隔 // ========== SYSTEM 模块 ========== SYSTEM_USER_SEX = 'system_user_sex', SYSTEM_MENU_TYPE = 'system_menu_type', SYSTEM_ROLE_TYPE = 'system_role_type', SYSTEM_DATA_SCOPE = 'system_data_scope', SYSTEM_NOTICE_TYPE = 'system_notice_type', SYSTEM_LOGIN_TYPE = 'system_login_type', SYSTEM_LOGIN_RESULT = 'system_login_result', SYSTEM_SMS_CHANNEL_CODE = 'system_sms_channel_code', SYSTEM_SMS_TEMPLATE_TYPE = 'system_sms_template_type', SYSTEM_SMS_SEND_STATUS = 'system_sms_send_status', SYSTEM_SMS_RECEIVE_STATUS = 'system_sms_receive_status', SYSTEM_ERROR_CODE_TYPE = 'system_error_code_type', SYSTEM_OAUTH2_GRANT_TYPE = 'system_oauth2_grant_type', SYSTEM_MAIL_SEND_STATUS = 'system_mail_send_status', SYSTEM_NOTIFY_TEMPLATE_TYPE = 'system_notify_template_type', SYSTEM_SOCIAL_TYPE = 'system_social_type', // ========== INFRA 模块 ========== INFRA_BOOLEAN_STRING = 'infra_boolean_string', INFRA_JOB_STATUS = 'infra_job_status', INFRA_JOB_LOG_STATUS = 'infra_job_log_status', INFRA_API_ERROR_LOG_PROCESS_STATUS = 'infra_api_error_log_process_status', INFRA_CONFIG_TYPE = 'infra_config_type', INFRA_CODEGEN_TEMPLATE_TYPE = 'infra_codegen_template_type', INFRA_CODEGEN_FRONT_TYPE = 'infra_codegen_front_type', INFRA_CODEGEN_SCENE = 'infra_codegen_scene', INFRA_FILE_STORAGE = 'infra_file_storage', INFRA_OPERATE_TYPE = 'infra_operate_type', // ========== BPM 模块 ========== BPM_MODEL_FORM_TYPE = 'bpm_model_form_type', BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy', BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status', BPM_TASK_STATUS = 'bpm_task_status', BPM_OA_LEAVE_TYPE = 'bpm_oa_leave_type', BPM_PROCESS_LISTENER_TYPE = 'bpm_process_listener_type', BPM_PROCESS_LISTENER_VALUE_TYPE = 'bpm_process_listener_value_type', // ========== PAY 模块 ========== PAY_CHANNEL_CODE = 'pay_channel_code', // 支付渠道编码类型 PAY_ORDER_STATUS = 'pay_order_status', // 商户支付订单状态 PAY_REFUND_STATUS = 'pay_refund_status', // 退款订单状态 PAY_NOTIFY_STATUS = 'pay_notify_status', // 商户支付回调状态 PAY_NOTIFY_TYPE = 'pay_notify_type', // 商户支付回调状态 PAY_TRANSFER_STATUS = 'pay_transfer_status', // 转账订单状态 PAY_TRANSFER_TYPE = 'pay_transfer_type', // 转账订单状态 // ========== MP 模块 ========== MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型 MP_MESSAGE_TYPE = 'mp_message_type', // 消息类型 // ========== Member 会员模块 ========== MEMBER_POINT_BIZ_TYPE = 'member_point_biz_type', // 积分的业务类型 MEMBER_EXPERIENCE_BIZ_TYPE = 'member_experience_biz_type', // 会员经验业务类型 // ========== MALL - 商品模块 ========== PRODUCT_SPU_STATUS = 'product_spu_status', //商品状态 // ========== MALL - 交易模块 ========== EXPRESS_CHARGE_MODE = 'trade_delivery_express_charge_mode', //快递的计费方式 TRADE_AFTER_SALE_STATUS = 'trade_after_sale_status', // 售后 - 状态 TRADE_AFTER_SALE_WAY = 'trade_after_sale_way', // 售后 - 方式 TRADE_AFTER_SALE_TYPE = 'trade_after_sale_type', // 售后 - 类型 TRADE_ORDER_TYPE = 'trade_order_type', // 订单 - 类型 TRADE_ORDER_STATUS = 'trade_order_status', // 订单 - 状态 TRADE_ORDER_ITEM_AFTER_SALE_STATUS = 'trade_order_item_after_sale_status', // 订单项 - 售后状态 TRADE_DELIVERY_TYPE = 'trade_delivery_type', // 配送方式 BROKERAGE_ENABLED_CONDITION = 'brokerage_enabled_condition', // 分佣模式 BROKERAGE_BIND_MODE = 'brokerage_bind_mode', // 分销关系绑定模式 BROKERAGE_BANK_NAME = 'brokerage_bank_name', // 佣金提现银行 BROKERAGE_WITHDRAW_TYPE = 'brokerage_withdraw_type', // 佣金提现类型 BROKERAGE_RECORD_BIZ_TYPE = 'brokerage_record_biz_type', // 佣金业务类型 BROKERAGE_RECORD_STATUS = 'brokerage_record_status', // 佣金状态 BROKERAGE_WITHDRAW_STATUS = 'brokerage_withdraw_status', // 佣金提现状态 // ========== MALL - 营销模块 ========== PROMOTION_DISCOUNT_TYPE = 'promotion_discount_type', // 优惠类型 PROMOTION_PRODUCT_SCOPE = 'promotion_product_scope', // 营销的商品范围 PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE = 'promotion_coupon_template_validity_type', // 优惠劵模板的有限期类型 PROMOTION_COUPON_STATUS = 'promotion_coupon_status', // 优惠劵的状态 PROMOTION_COUPON_TAKE_TYPE = 'promotion_coupon_take_type', // 优惠劵的领取方式 PROMOTION_ACTIVITY_STATUS = 'promotion_activity_status', // 优惠活动的状态 PROMOTION_CONDITION_TYPE = 'promotion_condition_type', // 营销的条件类型枚举 PROMOTION_BARGAIN_RECORD_STATUS = 'promotion_bargain_record_status', // 砍价记录的状态 PROMOTION_COMBINATION_RECORD_STATUS = 'promotion_combination_record_status', // 拼团记录的状态 PROMOTION_BANNER_POSITION = 'promotion_banner_position', // banner 定位 // ========== CRM - 客户管理模块 ========== CRM_AUDIT_STATUS = 'crm_audit_status', // CRM 审批状态 CRM_BIZ_TYPE = 'crm_biz_type', // CRM 业务类型 CRM_BUSINESS_END_STATUS_TYPE = 'crm_business_end_status_type', // CRM 商机结束状态类型 CRM_RECEIVABLE_RETURN_TYPE = 'crm_receivable_return_type', // CRM 回款的还款方式 CRM_CUSTOMER_INDUSTRY = 'crm_customer_industry', // CRM 客户所属行业 CRM_CUSTOMER_LEVEL = 'crm_customer_level', // CRM 客户级别 CRM_CUSTOMER_SOURCE = 'crm_customer_source', // CRM 客户来源 CRM_PRODUCT_STATUS = 'crm_product_status', // CRM 商品状态 CRM_PERMISSION_LEVEL = 'crm_permission_level', // CRM 数据权限的级别 CRM_PRODUCT_UNIT = 'crm_product_unit', // CRM 产品单位 CRM_FOLLOW_UP_TYPE = 'crm_follow_up_type', // CRM 跟进方式 // ========== ERP - 企业资源计划模块 ========== ERP_AUDIT_STATUS = 'erp_audit_status', // ERP 审批状态 ERP_STOCK_RECORD_BIZ_TYPE = 'erp_stock_record_biz_type' // 库存明细的业务类型 } ================================================ FILE: yshop-drink-vue3/src/utils/domUtils.ts ================================================ import { isServer } from './is' const ieVersion = isServer ? 0 : Number((document as any).documentMode) const SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g const MOZ_HACK_REGEXP = /^moz([A-Z])/ export interface ViewportOffsetResult { left: number top: number right: number bottom: number rightIncludeBody: number bottomIncludeBody: number } /* istanbul ignore next */ const trim = function (string: string) { return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '') } /* istanbul ignore next */ const camelCase = function (name: string) { return name .replace(SPECIAL_CHARS_REGEXP, function (_, __, letter, offset) { return offset ? letter.toUpperCase() : letter }) .replace(MOZ_HACK_REGEXP, 'Moz$1') } /* istanbul ignore next */ export function hasClass(el: Element, cls: string) { if (!el || !cls) return false if (cls.indexOf(' ') !== -1) { throw new Error('className should not contain space.') } if (el.classList) { return el.classList.contains(cls) } else { return (' ' + el.className + ' ').indexOf(' ' + cls + ' ') > -1 } } /* istanbul ignore next */ export function addClass(el: Element, cls: string) { if (!el) return let curClass = el.className const classes = (cls || '').split(' ') for (let i = 0, j = classes.length; i < j; i++) { const clsName = classes[i] if (!clsName) continue if (el.classList) { el.classList.add(clsName) } else if (!hasClass(el, clsName)) { curClass += ' ' + clsName } } if (!el.classList) { el.className = curClass } } /* istanbul ignore next */ export function removeClass(el: Element, cls: string) { if (!el || !cls) return const classes = cls.split(' ') let curClass = ' ' + el.className + ' ' for (let i = 0, j = classes.length; i < j; i++) { const clsName = classes[i] if (!clsName) continue if (el.classList) { el.classList.remove(clsName) } else if (hasClass(el, clsName)) { curClass = curClass.replace(' ' + clsName + ' ', ' ') } } if (!el.classList) { el.className = trim(curClass) } } export function getBoundingClientRect(element: Element): DOMRect | number { if (!element || !element.getBoundingClientRect) { return 0 } return element.getBoundingClientRect() } /** * 获取当前元素的left、top偏移 * left:元素最左侧距离文档左侧的距离 * top:元素最顶端距离文档顶端的距离 * right:元素最右侧距离文档右侧的距离 * bottom:元素最底端距离文档底端的距离 * rightIncludeBody:元素最左侧距离文档右侧的距离 * bottomIncludeBody:元素最底端距离文档最底部的距离 * * @description: */ export function getViewportOffset(element: Element): ViewportOffsetResult { const doc = document.documentElement const docScrollLeft = doc.scrollLeft const docScrollTop = doc.scrollTop const docClientLeft = doc.clientLeft const docClientTop = doc.clientTop const pageXOffset = window.pageXOffset const pageYOffset = window.pageYOffset const box = getBoundingClientRect(element) const { left: retLeft, top: rectTop, width: rectWidth, height: rectHeight } = box as DOMRect const scrollLeft = (pageXOffset || docScrollLeft) - (docClientLeft || 0) const scrollTop = (pageYOffset || docScrollTop) - (docClientTop || 0) const offsetLeft = retLeft + pageXOffset const offsetTop = rectTop + pageYOffset const left = offsetLeft - scrollLeft const top = offsetTop - scrollTop const clientWidth = window.document.documentElement.clientWidth const clientHeight = window.document.documentElement.clientHeight return { left: left, top: top, right: clientWidth - rectWidth - left, bottom: clientHeight - rectHeight - top, rightIncludeBody: clientWidth - left, bottomIncludeBody: clientHeight - top } } /* istanbul ignore next */ export const on = function ( element: HTMLElement | Document | Window, event: string, handler: EventListenerOrEventListenerObject ): void { if (element && event && handler) { element.addEventListener(event, handler, false) } } /* istanbul ignore next */ export const off = function ( element: HTMLElement | Document | Window, event: string, handler: any ): void { if (element && event && handler) { element.removeEventListener(event, handler, false) } } /* istanbul ignore next */ export const once = function (el: HTMLElement, event: string, fn: EventListener): void { const listener = function (this: any, ...args: unknown[]) { if (fn) { // @ts-ignore fn.apply(this, args) } off(el, event, listener) } on(el, event, listener) } /* istanbul ignore next */ export const getStyle = ieVersion < 9 ? function (element: Element | any, styleName: string) { if (isServer) return if (!element || !styleName) return null styleName = camelCase(styleName) if (styleName === 'float') { styleName = 'styleFloat' } try { switch (styleName) { case 'opacity': try { return element.filters.item('alpha').opacity / 100 } catch (e) { return 1.0 } default: return element.style[styleName] || element.currentStyle ? element.currentStyle[styleName] : null } } catch (e) { return element.style[styleName] } } : function (element: Element | any, styleName: string) { if (isServer) return if (!element || !styleName) return null styleName = camelCase(styleName) if (styleName === 'float') { styleName = 'cssFloat' } try { const computed = (document as any).defaultView.getComputedStyle(element, '') return element.style[styleName] || computed ? computed[styleName] : null } catch (e) { return element.style[styleName] } } /* istanbul ignore next */ export function setStyle(element: Element | any, styleName: any, value: any) { if (!element || !styleName) return if (typeof styleName === 'object') { for (const prop in styleName) { if (Object.prototype.hasOwnProperty.call(styleName, prop)) { setStyle(element, prop, styleName[prop]) } } } else { styleName = camelCase(styleName) if (styleName === 'opacity' && ieVersion < 9) { element.style.filter = isNaN(value) ? '' : 'alpha(opacity=' + value * 100 + ')' } else { element.style[styleName] = value } } } /* istanbul ignore next */ export const isScroll = (el: Element, vertical: any) => { if (isServer) return const determinedDirection = vertical !== null || vertical !== undefined const overflow = determinedDirection ? vertical ? getStyle(el, 'overflow-y') : getStyle(el, 'overflow-x') : getStyle(el, 'overflow') return overflow.match(/(scroll|auto)/) } /* istanbul ignore next */ export const getScrollContainer = (el: Element, vertical?: any) => { if (isServer) return let parent: any = el while (parent) { if ([window, document, document.documentElement].includes(parent)) { return window } if (isScroll(parent, vertical)) { return parent } parent = parent.parentNode } return parent } /* istanbul ignore next */ export const isInContainer = (el: Element, container: any) => { if (isServer || !el || !container) return false const elRect = el.getBoundingClientRect() let containerRect if ([window, document, document.documentElement, null, undefined].includes(container)) { containerRect = { top: 0, right: window.innerWidth, bottom: window.innerHeight, left: 0 } } else { containerRect = container.getBoundingClientRect() } return ( elRect.top < containerRect.bottom && elRect.bottom > containerRect.top && elRect.right > containerRect.left && elRect.left < containerRect.right ) } ================================================ FILE: yshop-drink-vue3/src/utils/download.ts ================================================ const download0 = (data: Blob, fileName: string, mineType: string) => { // 创建 blob const blob = new Blob([data], { type: mineType }) // 创建 href 超链接,点击进行下载 window.URL = window.URL || window.webkitURL const href = URL.createObjectURL(blob) const downA = document.createElement('a') downA.href = href downA.download = fileName downA.click() // 销毁超连接 window.URL.revokeObjectURL(href) } const download = { // 下载 Excel 方法 excel: (data: Blob, fileName: string) => { download0(data, fileName, 'application/vnd.ms-excel') }, // 下载 Word 方法 word: (data: Blob, fileName: string) => { download0(data, fileName, 'application/msword') }, // 下载 Zip 方法 zip: (data: Blob, fileName: string) => { download0(data, fileName, 'application/zip') }, // 下载 Html 方法 html: (data: Blob, fileName: string) => { download0(data, fileName, 'text/html') }, // 下载 Markdown 方法 markdown: (data: Blob, fileName: string) => { download0(data, fileName, 'text/markdown') } } export default download ================================================ FILE: yshop-drink-vue3/src/utils/filt.ts ================================================ export const openWindow = ( url: string, opt?: { target?: '_self' | '_blank' | string noopener?: boolean noreferrer?: boolean } ) => { const { target = '__blank', noopener = true, noreferrer = true } = opt || {} const feature: string[] = [] noopener && feature.push('noopener=yes') noreferrer && feature.push('noreferrer=yes') window.open(url, target, feature.join(',')) } /** * @description: base64 to blob */ export const dataURLtoBlob = (base64Buf: string): Blob => { const arr = base64Buf.split(',') const typeItem = arr[0] const mime = typeItem.match(/:(.*?);/)![1] const bstr = window.atob(arr[1]) let n = bstr.length const u8arr = new Uint8Array(n) while (n--) { u8arr[n] = bstr.charCodeAt(n) } return new Blob([u8arr], { type: mime }) } /** * img url to base64 * @param url */ export const urlToBase64 = (url: string, mineType?: string): Promise => { return new Promise((resolve, reject) => { let canvas = document.createElement('CANVAS') as Nullable const ctx = canvas!.getContext('2d') const img = new Image() img.crossOrigin = '' img.onload = function () { if (!canvas || !ctx) { return reject() } canvas.height = img.height canvas.width = img.width ctx.drawImage(img, 0, 0) const dataURL = canvas.toDataURL(mineType || 'image/png') canvas = null resolve(dataURL) } img.src = url }) } /** * Download online pictures * @param url * @param filename * @param mime * @param bom */ export const downloadByOnlineUrl = ( url: string, filename: string, mime?: string, bom?: BlobPart ) => { urlToBase64(url).then((base64) => { downloadByBase64(base64, filename, mime, bom) }) } /** * Download pictures based on base64 * @param buf * @param filename * @param mime * @param bom */ export const downloadByBase64 = (buf: string, filename: string, mime?: string, bom?: BlobPart) => { const base64Buf = dataURLtoBlob(buf) downloadByData(base64Buf, filename, mime, bom) } /** * Download according to the background interface file stream * @param {*} data * @param {*} filename * @param {*} mime * @param {*} bom */ export const downloadByData = (data: BlobPart, filename: string, mime?: string, bom?: BlobPart) => { const blobData = typeof bom !== 'undefined' ? [bom, data] : [data] const blob = new Blob(blobData, { type: mime || 'application/octet-stream' }) const blobURL = window.URL.createObjectURL(blob) const tempLink = document.createElement('a') tempLink.style.display = 'none' tempLink.href = blobURL tempLink.setAttribute('download', filename) if (typeof tempLink.download === 'undefined') { tempLink.setAttribute('target', '_blank') } document.body.appendChild(tempLink) tempLink.click() document.body.removeChild(tempLink) window.URL.revokeObjectURL(blobURL) } /** * Download file according to file address * @param {*} sUrl */ export const downloadByUrl = ({ url, target = '_blank', fileName }: { url: string target?: '_self' | '_blank' fileName?: string }): boolean => { const isChrome = window.navigator.userAgent.toLowerCase().indexOf('chrome') > -1 const isSafari = window.navigator.userAgent.toLowerCase().indexOf('safari') > -1 if (/(iP)/g.test(window.navigator.userAgent)) { console.error('Your browser does not support download!') return false } if (isChrome || isSafari) { const link = document.createElement('a') link.href = url link.target = target if (link.download !== undefined) { link.download = fileName || url.substring(url.lastIndexOf('/') + 1, url.length) } if (document.createEvent) { const e = document.createEvent('MouseEvents') e.initEvent('click', true, true) link.dispatchEvent(e) return true } } if (url.indexOf('?') === -1) { url += '?download' } openWindow(url, { target }) return true } ================================================ FILE: yshop-drink-vue3/src/utils/formCreate.ts ================================================ /** * 针对 https://github.com/xaboy/form-create-designer 封装的工具类 */ // 编码表单 Conf export const encodeConf = (designerRef: object) => { // @ts-ignore return JSON.stringify(designerRef.value.getOption()) } // 编码表单 Fields export const encodeFields = (designerRef: object) => { // @ts-ignore const rule = designerRef.value.getRule() const fields: string[] = [] rule.forEach((item) => { fields.push(JSON.stringify(item)) }) return fields } // 解码表单 Fields export const decodeFields = (fields: string[]) => { const rule: object[] = [] fields.forEach((item) => { rule.push(JSON.parse(item)) }) return rule } // 设置表单的 Conf 和 Fields,适用 FcDesigner 场景 export const setConfAndFields = (designerRef: object, conf: string, fields: string) => { // @ts-ignore designerRef.value.setOption(JSON.parse(conf)) // @ts-ignore designerRef.value.setRule(decodeFields(fields)) } // 设置表单的 Conf 和 Fields,适用 form-create 场景 export const setConfAndFields2 = ( detailPreview: object, conf: string, fields: string[], value?: object ) => { if (isRef(detailPreview)) { detailPreview = detailPreview.value } // @ts-ignore detailPreview.option = JSON.parse(conf) // @ts-ignore detailPreview.rule = decodeFields(fields) if (value) { // @ts-ignore detailPreview.value = value } } ================================================ FILE: yshop-drink-vue3/src/utils/formRules.ts ================================================ const { t } = useI18n() // 必填项 export const required = { required: true, message: t('common.required') } ================================================ FILE: yshop-drink-vue3/src/utils/formatTime.ts ================================================ import dayjs from 'dayjs' import type { TableColumnCtx } from 'element-plus' /** * 日期快捷选项适用于 el-date-picker */ export const defaultShortcuts = [ { text: '今天', value: () => { return new Date() } }, { text: '昨天', value: () => { const date = new Date() date.setTime(date.getTime() - 3600 * 1000 * 24) return [date, date] } }, { text: '最近七天', value: () => { const date = new Date() date.setTime(date.getTime() - 3600 * 1000 * 24 * 7) return [date, new Date()] } }, { text: '最近 30 天', value: () => { const date = new Date() date.setTime(date.getTime() - 3600 * 1000 * 24 * 30) return [date, new Date()] } }, { text: '本月', value: () => { const date = new Date() date.setDate(1) // 设置为当前月的第一天 return [date, new Date()] } }, { text: '今年', value: () => { const date = new Date() return [new Date(`${date.getFullYear()}-01-01`), date] } } ] /** * 时间日期转换 * @param date 当前时间,new Date() 格式 * @param format 需要转换的时间格式字符串 * @description format 字符串随意,如 `YYYY-mm、YYYY-mm-dd` * @description format 季度:"YYYY-mm-dd HH:MM:SS QQQQ" * @description format 星期:"YYYY-mm-dd HH:MM:SS WWW" * @description format 几周:"YYYY-mm-dd HH:MM:SS ZZZ" * @description format 季度 + 星期 + 几周:"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ" * @returns 返回拼接后的时间字符串 */ export function formatDate(date: Date, format?: string): string { // 日期不存在,则返回空 if (!date) { return '' } // 日期存在,则进行格式化 return date ? dayjs(date).format(format ?? 'YYYY-MM-DD HH:mm:ss') : '' } /** * 获取当前的日期+时间 */ export function getNowDateTime() { return dayjs() } /** * 获取当前日期是第几周 * @param dateTime 当前传入的日期值 * @returns 返回第几周数字值 */ export function getWeek(dateTime: Date): number { const temptTime = new Date(dateTime.getTime()) // 周几 const weekday = temptTime.getDay() || 7 // 周1+5天=周六 temptTime.setDate(temptTime.getDate() - weekday + 1 + 5) let firstDay = new Date(temptTime.getFullYear(), 0, 1) const dayOfWeek = firstDay.getDay() let spendDay = 1 if (dayOfWeek != 0) spendDay = 7 - dayOfWeek + 1 firstDay = new Date(temptTime.getFullYear(), 0, 1 + spendDay) const d = Math.ceil((temptTime.valueOf() - firstDay.valueOf()) / 86400000) return Math.ceil(d / 7) } /** * 将时间转换为 `几秒前`、`几分钟前`、`几小时前`、`几天前` * @param param 当前时间,new Date() 格式或者字符串时间格式 * @param format 需要转换的时间格式字符串 * @description param 10秒: 10 * 1000 * @description param 1分: 60 * 1000 * @description param 1小时: 60 * 60 * 1000 * @description param 24小时:60 * 60 * 24 * 1000 * @description param 3天: 60 * 60* 24 * 1000 * 3 * @returns 返回拼接后的时间字符串 */ export function formatPast(param: string | Date, format = 'YYYY-mm-dd HH:MM:SS'): string { // 传入格式处理、存储转换值 let t: any, s: number // 获取js 时间戳 let time: number = new Date().getTime() // 是否是对象 typeof param === 'string' || 'object' ? (t = new Date(param).getTime()) : (t = param) // 当前时间戳 - 传入时间戳 time = Number.parseInt(`${time - t}`) if (time < 10000) { // 10秒内 return '刚刚' } else if (time < 60000 && time >= 10000) { // 超过10秒少于1分钟内 s = Math.floor(time / 1000) return `${s}秒前` } else if (time < 3600000 && time >= 60000) { // 超过1分钟少于1小时 s = Math.floor(time / 60000) return `${s}分钟前` } else if (time < 86400000 && time >= 3600000) { // 超过1小时少于24小时 s = Math.floor(time / 3600000) return `${s}小时前` } else if (time < 259200000 && time >= 86400000) { // 超过1天少于3天内 s = Math.floor(time / 86400000) return `${s}天前` } else { // 超过3天 const date = typeof param === 'string' || 'object' ? new Date(param) : param return formatDate(date, format) } } /** * 时间问候语 * @param param 当前时间,new Date() 格式 * @description param 调用 `formatAxis(new Date())` 输出 `上午好` * @returns 返回拼接后的时间字符串 */ export function formatAxis(param: Date): string { const hour: number = new Date(param).getHours() if (hour < 6) return '凌晨好' else if (hour < 9) return '早上好' else if (hour < 12) return '上午好' else if (hour < 14) return '中午好' else if (hour < 17) return '下午好' else if (hour < 19) return '傍晚好' else if (hour < 22) return '晚上好' else return '夜里好' } /** * 将毫秒,转换成时间字符串。例如说,xx 分钟 * * @param ms 毫秒 * @returns {string} 字符串 */ export function formatPast2(ms: number): string { const day = Math.floor(ms / (24 * 60 * 60 * 1000)) const hour = Math.floor(ms / (60 * 60 * 1000) - day * 24) const minute = Math.floor(ms / (60 * 1000) - day * 24 * 60 - hour * 60) const second = Math.floor(ms / 1000 - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60) if (day > 0) { return day + ' 天' + hour + ' 小时 ' + minute + ' 分钟' } if (hour > 0) { return hour + ' 小时 ' + minute + ' 分钟' } if (minute > 0) { return minute + ' 分钟' } if (second > 0) { return second + ' 秒' } else { return 0 + ' 秒' } } /** * element plus 的时间 Formatter 实现,使用 YYYY-MM-DD HH:mm:ss 格式 * * @param row 行数据 * @param column 字段 * @param cellValue 字段值 */ export function dateFormatter(_row: any, _column: TableColumnCtx, cellValue: any): string { return cellValue ? formatDate(cellValue) : '' } /** * element plus 的时间 Formatter 实现,使用 HH:mm:ss 格式 * * @param row 行数据 * @param column 字段 * @param cellValue 字段值 */ export function dateFormatter2(_row: any, _column: TableColumnCtx, cellValue: any): string { return cellValue ? formatDate(cellValue, 'HH:mm:ss') : '' } /** * element plus 的时间 Formatter 实现,使用 YYYY-MM-DD 格式 * * @param row 行数据 * @param column 字段 * @param cellValue 字段值 */ export function dateFormatter3(_row: any, _column: TableColumnCtx, cellValue: any): string { return cellValue ? formatDate(cellValue, 'YYYY-MM-DD') : '' } /** * 设置起始日期,时间为00:00:00 * @param param 传入日期 * @returns 带时间00:00:00的日期 */ export function beginOfDay(param: Date): Date { return new Date(param.getFullYear(), param.getMonth(), param.getDate(), 0, 0, 0) } /** * 设置结束日期,时间为23:59:59 * @param param 传入日期 * @returns 带时间23:59:59的日期 */ export function endOfDay(param: Date): Date { return new Date(param.getFullYear(), param.getMonth(), param.getDate(), 23, 59, 59) } /** * 计算两个日期间隔天数 * @param param1 日期1 * @param param2 日期2 */ export function betweenDay(param1: Date, param2: Date): number { param1 = convertDate(param1) param2 = convertDate(param2) // 计算差值 return Math.floor((param2.getTime() - param1.getTime()) / (24 * 3600 * 1000)) } /** * 日期计算 * @param param1 日期 * @param param2 添加的时间 */ export function addTime(param1: Date, param2: number): Date { param1 = convertDate(param1) return new Date(param1.getTime() + param2) } /** * 日期转换 * @param param 日期 */ export function convertDate(param: Date | string): Date { if (typeof param === 'string') { return new Date(param) } return param } /** * 指定的两个日期, 是否为同一天 * @param a 日期 A * @param b 日期 B */ export function isSameDay(a: dayjs.ConfigType, b: dayjs.ConfigType): boolean { if (!a || !b) return false const aa = dayjs(a) const bb = dayjs(b) return aa.year() == bb.year() && aa.month() == bb.month() && aa.day() == bb.day() } /** * 获取一天的开始时间、截止时间 * @param date 日期 * @param days 天数 */ export function getDayRange( date: dayjs.ConfigType, days: number ): [dayjs.ConfigType, dayjs.ConfigType] { const day = dayjs(date).add(days, 'd') return getDateRange(day, day) } /** * 获取最近7天的开始时间、截止时间 */ export function getLast7Days(): [dayjs.ConfigType, dayjs.ConfigType] { const lastWeekDay = dayjs().subtract(7, 'd') const yesterday = dayjs().subtract(1, 'd') return getDateRange(lastWeekDay, yesterday) } /** * 获取最近30天的开始时间、截止时间 */ export function getLast30Days(): [dayjs.ConfigType, dayjs.ConfigType] { const lastMonthDay = dayjs().subtract(30, 'd') const yesterday = dayjs().subtract(1, 'd') return getDateRange(lastMonthDay, yesterday) } /** * 获取最近1年的开始时间、截止时间 */ export function getLast1Year(): [dayjs.ConfigType, dayjs.ConfigType] { const lastYearDay = dayjs().subtract(1, 'y') const yesterday = dayjs().subtract(1, 'd') return getDateRange(lastYearDay, yesterday) } /** * 获取指定日期的开始时间、截止时间 * @param beginDate 开始日期 * @param endDate 截止日期 */ export function getDateRange( beginDate: dayjs.ConfigType, endDate: dayjs.ConfigType ): [string, string] { return [ dayjs(beginDate).startOf('d').format('YYYY-MM-DD HH:mm:ss'), dayjs(endDate).endOf('d').format('YYYY-MM-DD HH:mm:ss') ] } ================================================ FILE: yshop-drink-vue3/src/utils/formatter.ts ================================================ import { floatToFixed2 } from '@/utils' // 格式化金额【分转元】 // @ts-ignore export const fenToYuanFormat = (_, __, cellValue: any, ___) => { return `¥${floatToFixed2(cellValue)}` } ================================================ FILE: yshop-drink-vue3/src/utils/index.ts ================================================ import { toNumber } from 'lodash-es' /** * * @param component 需要注册的组件 * @param alias 组件别名 * @returns any */ export const withInstall = (component: T, alias?: string) => { const comp = component as any comp.install = (app: any) => { app.component(comp.name || comp.displayName, component) if (alias) { app.config.globalProperties[alias] = component } } return component as T & Plugin } /** * @param str 需要转下划线的驼峰字符串 * @returns 字符串下划线 */ export const humpToUnderline = (str: string): string => { return str.replace(/([A-Z])/g, '-$1').toLowerCase() } /** * @param str 需要转驼峰的下划线字符串 * @returns 字符串驼峰 */ export const underlineToHump = (str: string): string => { if (!str) return '' return str.replace(/\-(\w)/g, (_, letter: string) => { return letter.toUpperCase() }) } /** * 驼峰转横杠 */ export const humpToDash = (str: string): string => { return str.replace(/([A-Z])/g, '-$1').toLowerCase() } export const setCssVar = (prop: string, val: any, dom = document.documentElement) => { dom.style.setProperty(prop, val) } /** * 查找数组对象的某个下标 * @param {Array} ary 查找的数组 * @param {Functon} fn 判断的方法 */ // eslint-disable-next-line export const findIndex = (ary: Array, fn: Fn): number => { if (ary.findIndex) { return ary.findIndex(fn) } let index = -1 ary.some((item: T, i: number, ary: Array) => { const ret: T = fn(item, i, ary) if (ret) { index = i return ret } }) return index } export const trim = (str: string) => { return str.replace(/(^\s*)|(\s*$)/g, '') } /** * @param {Date | number | string} time 需要转换的时间 * @param {String} fmt 需要转换的格式 如 yyyy-MM-dd、yyyy-MM-dd HH:mm:ss */ export function formatTime(time: Date | number | string, fmt: string) { if (!time) return '' else { const date = new Date(time) const o = { 'M+': date.getMonth() + 1, 'd+': date.getDate(), 'H+': date.getHours(), 'm+': date.getMinutes(), 's+': date.getSeconds(), 'q+': Math.floor((date.getMonth() + 3) / 3), S: date.getMilliseconds() } if (/(y+)/.test(fmt)) { fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length)) } for (const k in o) { if (new RegExp('(' + k + ')').test(fmt)) { fmt = fmt.replace( RegExp.$1, RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length) ) } } return fmt } } /** * 生成随机字符串 */ export function toAnyString() { const str: string = 'xxxxx-xxxxx-4xxxx-yxxxx-xxxxx'.replace(/[xy]/g, (c: string) => { const r: number = (Math.random() * 16) | 0 const v: number = c === 'x' ? r : (r & 0x3) | 0x8 return v.toString() }) return str } /** * 首字母大写 */ export function firstUpperCase(str: string) { return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase()) } export const generateUUID = () => { if (typeof crypto === 'object') { if (typeof crypto.randomUUID === 'function') { return crypto.randomUUID() } if (typeof crypto.getRandomValues === 'function' && typeof Uint8Array === 'function') { const callback = (c: any) => { const num = Number(c) return (num ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))).toString( 16 ) } return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, callback) } } let timestamp = new Date().getTime() let performanceNow = (typeof performance !== 'undefined' && performance.now && performance.now() * 1000) || 0 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { let random = Math.random() * 16 if (timestamp > 0) { random = (timestamp + random) % 16 | 0 timestamp = Math.floor(timestamp / 16) } else { random = (performanceNow + random) % 16 | 0 performanceNow = Math.floor(performanceNow / 16) } return (c === 'x' ? random : (random & 0x3) | 0x8).toString(16) }) } /** * element plus 的文件大小 Formatter 实现 * * @param row 行数据 * @param column 字段 * @param cellValue 字段值 */ // @ts-ignore export const fileSizeFormatter = (row, column, cellValue) => { const fileSize = cellValue const unitArr = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] const srcSize = parseFloat(fileSize) const index = Math.floor(Math.log(srcSize) / Math.log(1024)) const size = srcSize / Math.pow(1024, index) const sizeStr = size.toFixed(2) //保留的小数位数 return sizeStr + ' ' + unitArr[index] } /** * 将值复制到目标对象,且以目标对象属性为准,例:target: {a:1} source:{a:2,b:3} 结果为:{a:2} * @param target 目标对象 * @param source 源对象 */ export const copyValueToTarget = (target: any, source: any) => { const newObj = Object.assign({}, target, source) // 删除多余属性 Object.keys(newObj).forEach((key) => { // 如果不是target中的属性则删除 if (Object.keys(target).indexOf(key) === -1) { delete newObj[key] } }) // 更新目标对象值 Object.assign(target, newObj) } /** * 获取链接的参数值 * @param key 参数键名 * @param urlStr 链接地址,默认为当前浏览器的地址 */ export const getUrlValue = (key: string, urlStr: string = location.href): string => { if (!urlStr || !key) return '' const url = new URL(decodeURIComponent(urlStr)) return url.searchParams.get(key) ?? '' } /** * 获取链接的参数值(值类型) * @param key 参数键名 * @param urlStr 链接地址,默认为当前浏览器的地址 */ export const getUrlNumberValue = (key: string, urlStr: string = location.href): number => { return toNumber(getUrlValue(key, urlStr)) } /** * 构建排序字段 * @param prop 字段名称 * @param order 顺序 */ export const buildSortingField = ({ prop, order }) => { return { field: prop, order: order === 'ascending' ? 'asc' : 'desc' } } // ========== NumberUtils 数字方法 ========== /** * 数组求和 * * @param values 数字数组 * @return 求和结果,默认为 0 */ export const getSumValue = (values: number[]): number => { return values.reduce((prev, curr) => { const value = Number(curr) if (!Number.isNaN(value)) { return prev + curr } else { return prev } }, 0) } // ========== 通用金额方法 ========== /** * 将一个整数转换为分数保留两位小数 * @param num */ export const formatToFraction = (num: number | string | undefined): string => { if (typeof num === 'undefined') return '0.00' const parsedNumber = typeof num === 'string' ? parseFloat(num) : num return (parsedNumber / 100.0).toFixed(2) } /** * 将一个数转换为 1.00 这样 * 数据呈现的时候使用 * * @param num 整数 */ // TODO @芋艿:看看怎么融合掉 export const floatToFixed2 = (num: number | string | undefined): string => { let str = '0.00' if (typeof num === 'undefined') { return str } const f = formatToFraction(num) const decimalPart = f.toString().split('.')[1] const len = decimalPart ? decimalPart.length : 0 switch (len) { case 0: str = f.toString() + '.00' break case 1: str = f.toString() + '0' break case 2: str = f.toString() break } return str } /** * 将一个分数转换为整数 * @param num */ // TODO @芋艿:看看怎么融合掉 export const convertToInteger = (num: number | string | undefined): number => { if (typeof num === 'undefined') return 0 const parsedNumber = typeof num === 'string' ? parseFloat(num) : num // TODO 分转元后还有小数则四舍五入 return Math.round(parsedNumber * 100) } /** * 元转分 */ export const yuanToFen = (amount: string | number): number => { return convertToInteger(amount) } /** * 分转元 */ export const fenToYuan = (price: string | number): string => { return formatToFraction(price) } /** * 计算环比 * * @param value 当前数值 * @param reference 对比数值 */ export const calculateRelativeRate = (value?: number, reference?: number) => { // 防止除0 if (!reference) return 0 return ((100 * ((value || 0) - reference)) / reference).toFixed(0) } // ========== ERP 专属方法 ========== const ERP_COUNT_DIGIT = 3 const ERP_PRICE_DIGIT = 2 /** * 【ERP】格式化 Input 数字 * * 例如说:库存数量 * * @param num 数量 * @package digit 保留的小数位数 * @return 格式化后的数量 */ export const erpNumberFormatter = (num: number | string | undefined, digit: number) => { if (num == null) { return '' } if (typeof num === 'string') { num = parseFloat(num) } // 如果非 number,则直接返回空串 if (isNaN(num)) { return '' } return num.toFixed(digit) } /** * 【ERP】格式化数量,保留三位小数 * * 例如说:库存数量 * * @param num 数量 * @return 格式化后的数量 */ export const erpCountInputFormatter = (num: number | string | undefined) => { return erpNumberFormatter(num, ERP_COUNT_DIGIT) } // noinspection JSCommentMatchesSignature /** * 【ERP】格式化数量,保留三位小数 * * @param cellValue 数量 * @return 格式化后的数量 */ export const erpCountTableColumnFormatter = (_, __, cellValue: any, ___) => { return erpNumberFormatter(cellValue, ERP_COUNT_DIGIT) } /** * 【ERP】格式化金额,保留二位小数 * * 例如说:库存数量 * * @param num 数量 * @return 格式化后的数量 */ export const erpPriceInputFormatter = (num: number | string | undefined) => { return erpNumberFormatter(num, ERP_PRICE_DIGIT) } // noinspection JSCommentMatchesSignature /** * 【ERP】格式化金额,保留二位小数 * * @param cellValue 数量 * @return 格式化后的数量 */ export const erpPriceTableColumnFormatter = (_, __, cellValue: any, ___) => { return erpNumberFormatter(cellValue, ERP_PRICE_DIGIT) } /** * 【ERP】价格计算,四舍五入保留两位小数 * * @param price 价格 * @param count 数量 * @return 总价格。如果有任一为空,则返回 undefined */ export const erpPriceMultiply = (price: number, count: number) => { if (price == null || count == null) { return undefined } return parseFloat((price * count).toFixed(ERP_PRICE_DIGIT)) } /** * 【ERP】百分比计算,四舍五入保留两位小数 * * 如果 total 为 0,则返回 0 * * @param value 当前值 * @param total 总值 */ export const erpCalculatePercentage = (value: number, total: number) => { if (total === 0) return 0 return ((value / total) * 100).toFixed(2) } /** * 适配 echarts map 的地名 * * @param areaName 地区名称 */ export const areaReplace = (areaName: string) => { if (!areaName) { return areaName } return areaName .replace('维吾尔自治区', '') .replace('壮族自治区', '') .replace('回族自治区', '') .replace('自治区', '') .replace('省', '') } /** * 解析 JSON 字符串 * * @param str */ export function jsonParse(str: string) { try { return JSON.parse(str) } catch (e) { console.error(`str[${str}] 不是一个 JSON 字符串`) return '' } } ================================================ FILE: yshop-drink-vue3/src/utils/is.ts ================================================ // copy to vben-admin const toString = Object.prototype.toString export const is = (val: unknown, type: string) => { return toString.call(val) === `[object ${type}]` } export const isDef = (val?: T): val is T => { return typeof val !== 'undefined' } export const isUnDef = (val?: T): val is T => { return !isDef(val) } export const isObject = (val: any): val is Record => { return val !== null && is(val, 'Object') } export const isEmpty = (val: T): val is T => { if (val === null) { return true } if (isArray(val) || isString(val)) { return val.length === 0 } if (val instanceof Map || val instanceof Set) { return val.size === 0 } if (isObject(val)) { return Object.keys(val).length === 0 } return false } export const isDate = (val: unknown): val is Date => { return is(val, 'Date') } export const isNull = (val: unknown): val is null => { return val === null } export const isNullAndUnDef = (val: unknown): val is null | undefined => { return isUnDef(val) && isNull(val) } export const isNullOrUnDef = (val: unknown): val is null | undefined => { return isUnDef(val) || isNull(val) } export const isNumber = (val: unknown): val is number => { return is(val, 'Number') } export const isPromise = (val: unknown): val is Promise => { return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch) } export const isString = (val: unknown): val is string => { return is(val, 'String') } export const isFunction = (val: unknown): val is Function => { return typeof val === 'function' } export const isBoolean = (val: unknown): val is boolean => { return is(val, 'Boolean') } export const isRegExp = (val: unknown): val is RegExp => { return is(val, 'RegExp') } export const isArray = (val: any): val is Array => { return val && Array.isArray(val) } export const isWindow = (val: any): val is Window => { return typeof window !== 'undefined' && is(val, 'Window') } export const isElement = (val: unknown): val is Element => { return isObject(val) && !!val.tagName } export const isMap = (val: unknown): val is Map => { return is(val, 'Map') } export const isServer = typeof window === 'undefined' export const isClient = !isServer export const isUrl = (path: string): boolean => { const reg = /(((^https?:(?:\/\/)?)(?:[-:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&%@.\w_]*)#?(?:[\w]*))?)$/ return reg.test(path) } export const isDark = (): boolean => { return window.matchMedia('(prefers-color-scheme: dark)').matches } // 是否是图片链接 export const isImgPath = (path: string): boolean => { return /(https?:\/\/|data:image\/).*?\.(png|jpg|jpeg|gif|svg|webp|ico)/gi.test(path) } export const isEmptyVal = (val: any): boolean => { return val === '' || val === null || val === undefined } ================================================ FILE: yshop-drink-vue3/src/utils/jsencrypt.ts ================================================ import { JSEncrypt } from 'jsencrypt' // 密钥对生成 http://web.chacuo.net/netrsakeypair const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' + 'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==' const privateKey = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' + '7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' + 'PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n' + 'kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n' + 'cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n' + 'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' + 'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' + 'UP8iWi1Qw0Y=' // 加密 export const encrypt = (txt: string) => { const encryptor = new JSEncrypt() encryptor.setPublicKey(publicKey) // 设置公钥 return encryptor.encrypt(txt) // 对数据进行加密 } // 解密 export const decrypt = (txt: string) => { const encryptor = new JSEncrypt() encryptor.setPrivateKey(privateKey) // 设置私钥 return encryptor.decrypt(txt) // 对数据进行解密 } ================================================ FILE: yshop-drink-vue3/src/utils/permission.ts ================================================ import { CACHE_KEY, useCache } from '@/hooks/web/useCache' const { t } = useI18n() // 国际化 /** * 字符权限校验 * @param {Array} value 校验值 * @returns {Boolean} */ export function checkPermi(value: string[]) { if (value && value instanceof Array && value.length > 0) { const { wsCache } = useCache() const permissionDatas = value const all_permission = '*:*:*' const permissions = wsCache.get(CACHE_KEY.USER).permissions const hasPermission = permissions.some((permission) => { return all_permission === permission || permissionDatas.includes(permission) }) return !!hasPermission } else { console.error(t('permission.hasPermission')) return false } } /** * 角色权限校验 * @param {string[]} value 校验值 * @returns {Boolean} */ export function checkRole(value: string[]) { if (value && value instanceof Array && value.length > 0) { const { wsCache } = useCache() const permissionRoles = value const super_admin = 'admin' const roles = wsCache.get(CACHE_KEY.USER).roles const hasRole = roles.some((role) => { return super_admin === role || permissionRoles.includes(role) }) return !!hasRole } else { console.error(t('permission.hasRole')) return false } } ================================================ FILE: yshop-drink-vue3/src/utils/propTypes.ts ================================================ import { VueTypeValidableDef, VueTypesInterface, createTypes, toValidableType } from 'vue-types' import { CSSProperties } from 'vue' type PropTypes = VueTypesInterface & { readonly style: VueTypeValidableDef } const newPropTypes = createTypes({ func: undefined, bool: undefined, string: undefined, number: undefined, object: undefined, integer: undefined }) as PropTypes class propTypes extends newPropTypes { static get style() { return toValidableType('style', { type: [String, Object] }) } } export { propTypes } ================================================ FILE: yshop-drink-vue3/src/utils/routerHelper.ts ================================================ import type { RouteLocationNormalized, Router, RouteRecordNormalized } from 'vue-router' import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' import { isUrl } from '@/utils/is' import { cloneDeep, omit } from 'lodash-es' import qs from 'qs' const modules = import.meta.glob('../views/**/*.{vue,tsx}') /** * 注册一个异步组件 * @param componentPath 例:/bpm/oa/leave/detail */ export const registerComponent = (componentPath: string) => { for (const item in modules) { if (item.includes(componentPath)) { // 使用异步组件的方式来动态加载组件 // @ts-ignore return defineAsyncComponent(modules[item]) } } } /* Layout */ export const Layout = () => import('@/layout/Layout.vue') export const getParentLayout = () => { return () => new Promise((resolve) => { resolve({ name: 'ParentLayout' }) }) } // 按照路由中meta下的rank等级升序来排序路由 export const ascending = (arr: any[]) => { arr.forEach((v) => { if (v?.meta?.rank === null) v.meta.rank = undefined if (v?.meta?.rank === 0) { if (v.name !== 'home' && v.path !== '/') { console.warn('rank only the home page can be 0') } } }) return arr.sort((a: { meta: { rank: number } }, b: { meta: { rank: number } }) => { return a?.meta?.rank - b?.meta?.rank }) } export const getRawRoute = (route: RouteLocationNormalized): RouteLocationNormalized => { if (!route) return route const { matched, ...opt } = route return { ...opt, matched: (matched ? matched.map((item) => ({ meta: item.meta, name: item.name, path: item.path })) : undefined) as RouteRecordNormalized[] } } // 后端控制路由生成 export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecordRaw[] => { const res: AppRouteRecordRaw[] = [] const modulesRoutesKeys = Object.keys(modules) for (const route of routes) { // 1. 生成 meta 菜单元数据 const meta = { title: route.name, icon: route.icon, hidden: !route.visible, noCache: !route.keepAlive, alwaysShow: route.children && route.children.length === 1 && (route.alwaysShow !== undefined ? route.alwaysShow : true) } as any // 特殊逻辑:如果后端配置的 MenuDO.component 包含 ?,则表示需要传递参数 // 此时,我们需要解析参数,并且将参数放到 meta.query 中 // 这样,后续在 Vue 文件中,可以通过 const { currentRoute } = useRouter() 中,通过 meta.query 获取到参数 if (route.component && route.component.indexOf('?') > -1) { const query = route.component.split('?')[1] route.component = route.component.split('?')[0] meta.query = qs.parse(query) } // 2. 生成 data(AppRouteRecordRaw) // 路由地址转首字母大写驼峰,作为路由名称,适配keepAlive let data: AppRouteRecordRaw = { path: route.path.indexOf('?') > -1 ? route.path.split('?')[0] : route.path, name: route.componentName && route.componentName.length > 0 ? route.componentName : toCamelCase(route.path, true), redirect: route.redirect, meta: meta } //处理顶级非目录路由 if (!route.children && route.parentId == 0 && route.component) { data.component = Layout data.meta = {} data.name = toCamelCase(route.path, true) + 'Parent' data.redirect = '' meta.alwaysShow = true const childrenData: AppRouteRecordRaw = { path: '', name: route.componentName && route.componentName.length > 0 ? route.componentName : toCamelCase(route.path, true), redirect: route.redirect, meta: meta } const index = route?.component ? modulesRoutesKeys.findIndex((ev) => ev.includes(route.component)) : modulesRoutesKeys.findIndex((ev) => ev.includes(route.path)) childrenData.component = modules[modulesRoutesKeys[index]] data.children = [childrenData] } else { // 目录 if (route.children) { data.component = Layout data.redirect = getRedirect(route.path, route.children) // 外链 } else if (isUrl(route.path)) { data = { path: '/external-link', component: Layout, meta: { name: route.name }, children: [data] } as AppRouteRecordRaw // 菜单 } else { // 对后端传component组件路径和不传做兼容(如果后端传component组件路径,那么path可以随便写,如果不传,component组件路径会根path保持一致) const index = route?.component ? modulesRoutesKeys.findIndex((ev) => ev.includes(route.component)) : modulesRoutesKeys.findIndex((ev) => ev.includes(route.path)) data.component = modules[modulesRoutesKeys[index]] } if (route.children) { data.children = generateRoute(route.children) } } res.push(data as AppRouteRecordRaw) } return res } export const getRedirect = (parentPath: string, children: AppCustomRouteRecordRaw[]) => { if (!children || children.length == 0) { return parentPath } const path = generateRoutePath(parentPath, children[0].path) // 递归子节点 if (children[0].children) return getRedirect(path, children[0].children) } const generateRoutePath = (parentPath: string, path: string) => { if (parentPath.endsWith('/')) { parentPath = parentPath.slice(0, -1) // 移除默认的 / } if (!path.startsWith('/')) { path = '/' + path } return parentPath + path } export const pathResolve = (parentPath: string, path: string) => { if (isUrl(path)) return path const childPath = path.startsWith('/') || !path ? path : `/${path}` return `${parentPath}${childPath}`.replace(/\/\//g, '/') } // 路由降级 export const flatMultiLevelRoutes = (routes: AppRouteRecordRaw[]) => { const modules: AppRouteRecordRaw[] = cloneDeep(routes) for (let index = 0; index < modules.length; index++) { const route = modules[index] if (!isMultipleRoute(route)) { continue } promoteRouteLevel(route) } return modules } // 层级是否大于2 const isMultipleRoute = (route: AppRouteRecordRaw) => { if (!route || !Reflect.has(route, 'children') || !route.children?.length) { return false } const children = route.children let flag = false for (let index = 0; index < children.length; index++) { const child = children[index] if (child.children?.length) { flag = true break } } return flag } // 生成二级路由 const promoteRouteLevel = (route: AppRouteRecordRaw) => { let router: Router | null = createRouter({ routes: [route as RouteRecordRaw], history: createWebHashHistory() }) const routes = router.getRoutes() addToChildren(routes, route.children || [], route) router = null route.children = route.children?.map((item) => omit(item, 'children')) } // 添加所有子菜单 const addToChildren = ( routes: RouteRecordNormalized[], children: AppRouteRecordRaw[], routeModule: AppRouteRecordRaw ) => { for (let index = 0; index < children.length; index++) { const child = children[index] const route = routes.find((item) => item.name === child.name) if (!route) { continue } routeModule.children = routeModule.children || [] if (!routeModule.children.find((item) => item.name === route.name)) { routeModule.children?.push(route as unknown as AppRouteRecordRaw) } if (child.children?.length) { addToChildren(routes, child.children, routeModule) } } } const toCamelCase = (str: string, upperCaseFirst: boolean) => { str = (str || '') .replace(/-(.)/g, function (group1: string) { return group1.toUpperCase() }) .replaceAll('-', '') if (upperCaseFirst && str) { str = str.charAt(0).toUpperCase() + str.slice(1) } return str } ================================================ FILE: yshop-drink-vue3/src/utils/tree.ts ================================================ interface TreeHelperConfig { id: string children: string pid: string } const DEFAULT_CONFIG: TreeHelperConfig = { id: 'id', children: 'children', pid: 'pid' } export const defaultProps = { children: 'children', label: 'name', value: 'id', isLeaf: 'leaf', emitPath: false // 用于 cascader 组件:在选中节点改变时,是否返回由该节点所在的各级菜单的值所组成的数组,若设置 false,则只返回该节点的值 } const getConfig = (config: Partial) => Object.assign({}, DEFAULT_CONFIG, config) // tree from list export const listToTree = (list: any[], config: Partial = {}): T[] => { const conf = getConfig(config) as TreeHelperConfig const nodeMap = new Map() const result: T[] = [] const { id, children, pid } = conf for (const node of list) { node[children] = node[children] || [] nodeMap.set(node[id], node) } for (const node of list) { const parent = nodeMap.get(node[pid]) ;(parent ? parent.children : result).push(node) } return result } export const treeToList = (tree: any, config: Partial = {}): T => { config = getConfig(config) const { children } = config const result: any = [...tree] for (let i = 0; i < result.length; i++) { if (!result[i][children!]) continue result.splice(i + 1, 0, ...result[i][children!]) } return result } export const findNode = ( tree: any, func: Fn, config: Partial = {} ): T | null => { config = getConfig(config) const { children } = config const list = [...tree] for (const node of list) { if (func(node)) return node node[children!] && list.push(...node[children!]) } return null } export const findNodeAll = ( tree: any, func: Fn, config: Partial = {} ): T[] => { config = getConfig(config) const { children } = config const list = [...tree] const result: T[] = [] for (const node of list) { func(node) && result.push(node) node[children!] && list.push(...node[children!]) } return result } export const findPath = ( tree: any, func: Fn, config: Partial = {} ): T | T[] | null => { config = getConfig(config) const path: T[] = [] const list = [...tree] const visitedSet = new Set() const { children } = config while (list.length) { const node = list[0] if (visitedSet.has(node)) { path.pop() list.shift() } else { visitedSet.add(node) node[children!] && list.unshift(...node[children!]) path.push(node) if (func(node)) { return path } } } return null } export const findPathAll = (tree: any, func: Fn, config: Partial = {}) => { config = getConfig(config) const path: any[] = [] const list = [...tree] const result: any[] = [] const visitedSet = new Set(), { children } = config while (list.length) { const node = list[0] if (visitedSet.has(node)) { path.pop() list.shift() } else { visitedSet.add(node) node[children!] && list.unshift(...node[children!]) path.push(node) func(node) && result.push([...path]) } } return result } export const filter = ( tree: T[], func: (n: T) => boolean, config: Partial = {} ): T[] => { config = getConfig(config) const children = config.children as string function listFilter(list: T[]) { return list .map((node: any) => ({ ...node })) .filter((node) => { node[children] = node[children] && listFilter(node[children]) return func(node) || (node[children] && node[children].length) }) } return listFilter(tree) } export const forEach = ( tree: T[], func: (n: T) => any, config: Partial = {} ): void => { config = getConfig(config) const list: any[] = [...tree] const { children } = config for (let i = 0; i < list.length; i++) { // func 返回true就终止遍历,避免大量节点场景下无意义循环,引起浏览器卡顿 if (func(list[i])) { return } children && list[i][children] && list.splice(i + 1, 0, ...list[i][children]) } } /** * @description: Extract tree specified structure */ export const treeMap = ( treeData: T[], opt: { children?: string; conversion: Fn } ): T[] => { return treeData.map((item) => treeMapEach(item, opt)) } /** * @description: Extract tree specified structure */ export const treeMapEach = ( data: any, { children = 'children', conversion }: { children?: string; conversion: Fn } ) => { const haveChildren = Array.isArray(data[children]) && data[children].length > 0 const conversionData = conversion(data) || {} if (haveChildren) { return { ...conversionData, [children]: data[children].map((i: number) => treeMapEach(i, { children, conversion }) ) } } else { return { ...conversionData } } } /** * 递归遍历树结构 * @param treeDatas 树 * @param callBack 回调 * @param parentNode 父节点 */ export const eachTree = (treeDatas: any[], callBack: Fn, parentNode = {}) => { treeDatas.forEach((element) => { const newNode = callBack(element, parentNode) || element if (element.children) { eachTree(element.children, callBack, newNode) } }) } /** * 构造树型结构数据 * @param {*} data 数据源 * @param {*} id id字段 默认 'id' * @param {*} parentId 父节点字段 默认 'parentId' * @param {*} children 孩子节点字段 默认 'children' */ export const handleTree = (data: any[], id?: string, parentId?: string, children?: string) => { if (!Array.isArray(data)) { console.warn('data must be an array') return [] } const config = { id: id || 'id', parentId: parentId || 'parentId', childrenList: children || 'children' } const childrenListMap = {} const nodeIds = {} const tree: any[] = [] for (const d of data) { const parentId = d[config.parentId] if (childrenListMap[parentId] == null) { childrenListMap[parentId] = [] } nodeIds[d[config.id]] = d childrenListMap[parentId].push(d) } for (const d of data) { const parentId = d[config.parentId] if (nodeIds[parentId] == null) { tree.push(d) } } for (const t of tree) { adaptToChildrenList(t) } function adaptToChildrenList(o) { if (childrenListMap[o[config.id]] !== null) { o[config.childrenList] = childrenListMap[o[config.id]] } if (o[config.childrenList]) { for (const c of o[config.childrenList]) { adaptToChildrenList(c) } } } return tree } /** * 构造树型结构数据 * @param {*} data 数据源 * @param {*} id id字段 默认 'id' * @param {*} parentId 父节点字段 默认 'parentId' * @param {*} children 孩子节点字段 默认 'children' * @param {*} rootId 根Id 默认 0 */ // @ts-ignore export const handleTree2 = (data, id, parentId, children, rootId) => { id = id || 'id' parentId = parentId || 'parentId' // children = children || 'children' rootId = rootId || Math.min( ...data.map((item) => { return item[parentId] }) ) || 0 // 对源数据深度克隆 const cloneData = JSON.parse(JSON.stringify(data)) // 循环所有项 const treeData = cloneData.filter((father) => { const branchArr = cloneData.filter((child) => { // 返回每一项的子级数组 return father[id] === child[parentId] }) branchArr.length > 0 ? (father.children = branchArr) : '' // 返回第一层 return father[parentId] === rootId }) return treeData !== '' ? treeData : data } /** * 校验选中的节点,是否为指定 level * * @param tree 要操作的树结构数据 * @param nodeId 需要判断在什么层级的数据 * @param level 检查的级别, 默认检查到二级 * @return true 是;false 否 */ export const checkSelectedNode = (tree: any[], nodeId: any, level = 2): boolean => { if (typeof tree === 'undefined' || !Array.isArray(tree) || tree.length === 0) { console.warn('tree must be an array') return false } // 校验是否是一级节点 if (tree.some((item) => item.id === nodeId)) { return false } // 递归计数 let count = 1 // 深层次校验 function performAThoroughValidation(arr: any[]): boolean { count += 1 for (const item of arr) { if (item.id === nodeId) { return true } else if (typeof item.children !== 'undefined' && item.children.length !== 0) { if (performAThoroughValidation(item.children)) { return true } } } return false } for (const item of tree) { count = 1 if (performAThoroughValidation(item.children)) { // 找到后对比是否是期望的层级 if (count >= level) { return true } } } return false } /** * 获取节点的完整结构 * @param tree 树数据 * @param nodeId 节点 id */ export const treeToString = (tree: any[], nodeId) => { if (typeof tree === 'undefined' || !Array.isArray(tree) || tree.length === 0) { console.warn('tree must be an array') return '' } // 校验是否是一级节点 const node = tree.find((item) => item.id === nodeId) if (typeof node !== 'undefined') { return node.name } let str = '' function performAThoroughValidation(arr) { for (const item of arr) { if (item.id === nodeId) { str += ` / ${item.name}` return true } else if (typeof item.children !== 'undefined' && item.children.length !== 0) { str += ` / ${item.name}` if (performAThoroughValidation(item.children)) { return true } } } return false } for (const item of tree) { str = `${item.name}` if (performAThoroughValidation(item.children)) { break } } return str } ================================================ FILE: yshop-drink-vue3/src/utils/tsxHelper.ts ================================================ import { Slots } from 'vue' import { isFunction } from '@/utils/is' export const getSlot = (slots: Slots, slot = 'default', data?: Recordable) => { // Reflect.has 判断一个对象是否存在某个属性 if (!slots || !Reflect.has(slots, slot)) { return null } if (!isFunction(slots[slot])) { console.error(`${slot} is not a function!`) return null } const slotFn = slots[slot] if (!slotFn) return null return slotFn(data) } ================================================ FILE: yshop-drink-vue3/src/views/Error/403.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/Error/404.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/Error/500.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/Home/Index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/Home/Index2.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/Home/PanelGroupT.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/Home/echarts-data.ts ================================================ import { EChartsOption } from 'echarts' const { t } = useI18n() export const lineOptions: EChartsOption = { title: { text: t('analysis.monthlySales'), left: 'center' }, xAxis: { data: [ t('analysis.january'), t('analysis.february'), t('analysis.march'), t('analysis.april'), t('analysis.may'), t('analysis.june'), t('analysis.july'), t('analysis.august'), t('analysis.september'), t('analysis.october'), t('analysis.november'), t('analysis.december') ], boundaryGap: false, axisTick: { show: false } }, grid: { left: 20, right: 20, bottom: 20, top: 80, containLabel: true }, tooltip: { trigger: 'axis', axisPointer: { type: 'cross' }, padding: [5, 10] }, yAxis: { axisTick: { show: false } }, legend: { data: [t('analysis.estimate'), t('analysis.actual')], top: 50 }, series: [ { name: t('analysis.estimate'), smooth: true, type: 'line', data: [100, 120, 161, 134, 105, 160, 165, 114, 163, 185, 118, 123], animationDuration: 2800, animationEasing: 'cubicInOut' }, { name: t('analysis.actual'), smooth: true, type: 'line', itemStyle: {}, data: [120, 82, 91, 154, 162, 140, 145, 250, 134, 56, 99, 123], animationDuration: 2800, animationEasing: 'quadraticOut' } ] } export const pieOptions: EChartsOption = { title: { text: t('analysis.userAccessSource'), left: 'center' }, tooltip: { trigger: 'item', formatter: '{a}
    {b} : {c} ({d}%)' }, legend: { orient: 'vertical', left: 'left', data: [ t('analysis.directAccess'), t('analysis.mailMarketing'), t('analysis.allianceAdvertising'), t('analysis.videoAdvertising'), t('analysis.searchEngines') ] }, series: [ { name: t('analysis.userAccessSource'), type: 'pie', radius: '55%', center: ['50%', '60%'], data: [ { value: 335, name: t('analysis.directAccess') }, { value: 310, name: t('analysis.mailMarketing') }, { value: 234, name: t('analysis.allianceAdvertising') }, { value: 135, name: t('analysis.videoAdvertising') }, { value: 1548, name: t('analysis.searchEngines') } ] } ] } export const barOptions: EChartsOption = { title: { text: t('analysis.weeklyUserActivity'), left: 'center' }, tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, grid: { left: 50, right: 20, bottom: 20 }, xAxis: { type: 'category', data: [ t('analysis.monday'), t('analysis.tuesday'), t('analysis.wednesday'), t('analysis.thursday'), t('analysis.friday'), t('analysis.saturday'), t('analysis.sunday') ], axisTick: { alignWithLabel: true } }, yAxis: { type: 'value' }, series: [ { name: t('analysis.activeQuantity'), data: [13253, 34235, 26321, 12340, 24643, 1322, 1324], type: 'bar' } ] } export const radarOption: EChartsOption = { legend: { data: [t('workplace.personal'), t('workplace.team')] }, radar: { // shape: 'circle', indicator: [ { name: t('workplace.quote'), max: 65 }, { name: t('workplace.contribution'), max: 160 }, { name: t('workplace.hot'), max: 300 }, { name: t('workplace.yield'), max: 130 }, { name: t('workplace.follow'), max: 100 } ] }, series: [ { name: `xxx${t('workplace.index')}`, type: 'radar', data: [ { value: [42, 30, 20, 35, 80], name: t('workplace.personal') }, { value: [50, 140, 290, 100, 90], name: t('workplace.team') } ] } ] } export const wordOptions = { series: [ { type: 'wordCloud', gridSize: 2, sizeRange: [12, 50], rotationRange: [-90, 90], shape: 'pentagon', width: 600, height: 400, drawOutOfBound: true, textStyle: { color: function () { return ( 'rgb(' + [ Math.round(Math.random() * 160), Math.round(Math.random() * 160), Math.round(Math.random() * 160) ].join(',') + ')' ) } }, emphasis: { textStyle: { shadowBlur: 10, shadowColor: '#333' } }, data: [ { name: 'Sam S Club', value: 10000, textStyle: { color: 'black' }, emphasis: { textStyle: { color: 'red' } } }, { name: 'Macys', value: 6181 }, { name: 'Amy Schumer', value: 4386 }, { name: 'Jurassic World', value: 4055 }, { name: 'Charter Communications', value: 2467 }, { name: 'Chick Fil A', value: 2244 }, { name: 'Planet Fitness', value: 1898 }, { name: 'Pitch Perfect', value: 1484 }, { name: 'Express', value: 1112 }, { name: 'Home', value: 965 }, { name: 'Johnny Depp', value: 847 }, { name: 'Lena Dunham', value: 582 }, { name: 'Lewis Hamilton', value: 555 }, { name: 'KXAN', value: 550 }, { name: 'Mary Ellen Mark', value: 462 }, { name: 'Farrah Abraham', value: 366 }, { name: 'Rita Ora', value: 360 }, { name: 'Serena Williams', value: 282 }, { name: 'NCAA baseball tournament', value: 273 }, { name: 'Point Break', value: 265 } ] } ] } ================================================ FILE: yshop-drink-vue3/src/views/Home/types.ts ================================================ export type WorkplaceTotal = { project: number access: number todo: number } export type Project = { name: string icon: string message: string personal: string time: Date | number | string } export type Notice = { title: string type: string keys: string[] date: Date | number | string } export type Shortcut = { name: string icon: string url: string } export type RadarData = { personal: number team: number max: number name: string } export type AnalysisTotalTypes = { users: number messages: number moneys: number shoppings: number } export type UserAccessSource = { value: number name: string } export type WeeklyUserActivity = { value: number name: string } export type MonthlySales = { name: string estimate: number actual: number } ================================================ FILE: yshop-drink-vue3/src/views/Login/Login.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/Login/SocialLogin.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/Login/components/LoginForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/Login/components/LoginFormTitle.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/Login/components/MobileForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/Login/components/QrCodeForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/Login/components/RegisterForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/Login/components/SSOLogin.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/Login/components/index.ts ================================================ import LoginForm from './LoginForm.vue' import MobileForm from './MobileForm.vue' import LoginFormTitle from './LoginFormTitle.vue' import RegisterForm from './RegisterForm.vue' import QrCodeForm from './QrCodeForm.vue' import SSOLoginVue from './SSOLogin.vue' export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue } ================================================ FILE: yshop-drink-vue3/src/views/Login/components/useLogin.ts ================================================ import { Ref } from 'vue' export enum LoginStateEnum { LOGIN, REGISTER, RESET_PASSWORD, MOBILE, QR_CODE, SSO } const currentState = ref(LoginStateEnum.LOGIN) export function useLoginState() { function setLoginState(state: LoginStateEnum) { currentState.value = state } const getLoginState = computed(() => currentState.value) function handleBackLogin() { setLoginState(LoginStateEnum.LOGIN) } return { setLoginState, getLoginState, handleBackLogin } } export function useFormValid(formRef: Ref) { async function validForm() { const form = unref(formRef) if (!form) return const data = await form.validate() return data as T } return { validForm } } ================================================ FILE: yshop-drink-vue3/src/views/Profile/Index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/Profile/components/BasicInfo.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/Profile/components/ProfileUser.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/Profile/components/ResetPwd.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/Profile/components/UserAvatar.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/Profile/components/UserSocial.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/Profile/components/index.ts ================================================ import BasicInfo from './BasicInfo.vue' import ProfileUser from './ProfileUser.vue' import ResetPwd from './ResetPwd.vue' import UserAvatarVue from './UserAvatar.vue' import UserSocial from './UserSocial.vue' export { BasicInfo, ProfileUser, ResetPwd, UserAvatarVue, UserSocial } ================================================ FILE: yshop-drink-vue3/src/views/Redirect/Redirect.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/express/ExpressForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/express/ExpressSet.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/express/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/apiAccessLog/ApiAccessLogDetail.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/apiAccessLog/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/apiErrorLog/ApiErrorLogDetail.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/apiErrorLog/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/build/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/codegen/EditTable.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/codegen/ImportTable.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/codegen/PreviewCode.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/codegen/components/BasicInfoForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/codegen/components/ColumInfoForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/codegen/components/GenerateInfoForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/codegen/components/index.ts ================================================ import BasicInfoForm from './BasicInfoForm.vue' import ColumInfoForm from './ColumInfoForm.vue' import GenerateInfoForm from './GenerateInfoForm.vue' export { BasicInfoForm, ColumInfoForm, GenerateInfoForm } ================================================ FILE: yshop-drink-vue3/src/views/infra/codegen/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/config/ConfigForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/config/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/dataSourceConfig/DataSourceConfigForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/dataSourceConfig/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/demo/demo01/Demo01ContactForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/demo/demo01/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/demo/demo02/Demo02CategoryForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/demo/demo02/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/demo/demo03/erp/Demo03StudentForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/demo/demo03/erp/components/Demo03CourseForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/demo/demo03/erp/components/Demo03CourseList.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/demo/demo03/erp/components/Demo03GradeForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/demo/demo03/erp/components/Demo03GradeList.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/demo/demo03/erp/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/demo/demo03/inner/Demo03StudentForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/demo/demo03/inner/components/Demo03CourseForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/demo/demo03/inner/components/Demo03CourseList.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/demo/demo03/inner/components/Demo03GradeForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/demo/demo03/inner/components/Demo03GradeList.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/demo/demo03/inner/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/demo/demo03/normal/Demo03StudentForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/demo/demo03/normal/components/Demo03CourseForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/demo/demo03/normal/components/Demo03GradeForm.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/demo/demo03/normal/index.vue ================================================ ================================================ FILE: yshop-drink-vue3/src/views/infra/druid/index.vue ================================================